Skip to content

Commit 5abe423

Browse files
authored
refactor: Register and login API routes (#72)
* docs: place tags in docs * refactor: move register in legacy folder * feat: create register DTOs * feat: create register route * feat: create UserModelDto * feat: create UserAdapter * feat: create UsersRepository * fix: RegisterPayloadDto * feat: create RegisterUseCase * feat: create HashService * feat: test if user already exists * refactor: clean comments * docs: update register responses * fix: doc * fix: import reflect-metadata * fix: update doc properties * refactor: remove legacy register * style: format indents * feat: create SignInDataDto * feat: implement compare hash * feat: implement SignInUseCase * fix: UserModelDto string format * refactor: Create BusinessErrorDto * fix: Implement BusinessErrorDto in HttpResponseDto * fix: linter * fix: refactor tags api docs * fix: typo
1 parent cbfb0a6 commit 5abe423

File tree

21 files changed

+347
-126
lines changed

21 files changed

+347
-126
lines changed

src/app/api/auth/[...nextauth]/route.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { authOptions } from '@lib/auth';
55
* @swagger
66
* /api/auth/providers:
77
* get:
8-
* tags:
9-
* - Auth
8+
* tags: [Auth]
109
* summary: Retrieve configured authentication providers
1110
* description: By Next Auth
1211
* responses:
@@ -24,8 +23,7 @@ import { authOptions } from '@lib/auth';
2423
* @swagger
2524
* /api/auth/session:
2625
* get:
27-
* tags:
28-
* - Auth
26+
* tags: [Auth]
2927
* summary: Get current user session
3028
* description: By Next Auth
3129
* responses:
@@ -43,8 +41,7 @@ import { authOptions } from '@lib/auth';
4341
* @swagger
4442
* /api/auth/csrf:
4543
* get:
46-
* tags:
47-
* - Auth
44+
* tags: [Auth]
4845
* summary: Retrieve CSRF token
4946
* description: By Next Auth
5047
* responses:
@@ -62,8 +59,7 @@ import { authOptions } from '@lib/auth';
6259
* @swagger
6360
* /api/auth/signin:
6461
* post:
65-
* tags:
66-
* - Auth
62+
* tags: [Auth]
6763
* summary: Sign in using credentials provider
6864
* description: By Next Auth
6965
* requestBody:
@@ -83,8 +79,7 @@ import { authOptions } from '@lib/auth';
8379
* @swagger
8480
* /api/auth/signout:
8581
* post:
86-
* tags:
87-
* - Auth
82+
* tags: [Auth]
8883
* summary: Sign out current user
8984
* description: By Next Auth
9085
* responses:
@@ -98,8 +93,7 @@ import { authOptions } from '@lib/auth';
9893
* @swagger
9994
* /api/auth/callback/{provider}:
10095
* get:
101-
* tags:
102-
* - Auth
96+
* tags: [Auth]
10397
* summary: OAuth callback endpoint for a given provider
10498
* description: By Next Auth
10599
* parameters:

src/app/api/auth/register/route.ts

Lines changed: 30 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,54 @@
1-
import type { NextRequest } from 'next/server';
2-
import { NextResponse } from 'next/server';
3-
import prisma from '@lib/prisma';
4-
import { hash } from 'bcrypt';
1+
import 'reflect-metadata';
2+
import type { NextRequest, NextResponse } from 'next/server';
3+
import type { HttpResponseDto } from '@shared/dto/output/responses/abstract/http.response.dto';
4+
import type { RegisterDataDto } from '@shared/dto/output/data/register.data.dto';
5+
import type { RegisterPayloadDto } from '@shared/dto/input/payloads/register.payload.dto';
6+
import { container } from 'tsyringe';
7+
import { handleApiRequest } from '@api/helpers/api/handle-api-request';
58
import { StatusCodes } from 'http-status-codes';
6-
import type { User } from '@prisma/generated';
7-
/**
8-
* @swagger
9-
* tags:
10-
* - name: Auth
11-
* description: Authentication and user management
12-
*/
9+
import type { UserModelDto } from '@shared/dto/models/user.model.dto';
10+
import { RegisterUseCase } from '@api/usecases/auth/register.usecase';
1311

1412
/**
1513
* @swagger
1614
* /api/auth/register:
1715
* post:
18-
* summary: Register a new user
1916
* tags: [Auth]
17+
* summary: Register a new user
2018
* requestBody:
21-
* description: JSON object containing email and password
2219
* required: true
2320
* content:
2421
* application/json:
2522
* schema:
26-
* type: object
27-
* required:
28-
* - email
29-
* - password
30-
* properties:
31-
* email:
32-
* type: string
33-
* format: email
34-
* example: user@example.com
35-
* password:
36-
* type: string
37-
* format: password
38-
* example: securePassword123
23+
* $ref: '#/components/schemas/RegisterPayloadDto'
3924
* responses:
4025
* 201:
4126
* description: User created successfully
4227
* content:
4328
* application/json:
4429
* schema:
45-
* $ref: '#/components/schemas/User'
46-
* 400:
47-
* description: Bad request (missing email or password)
30+
* $ref: '#/components/schemas/RegisterDataDto'
4831
* 409:
49-
* description: Conflict user already exists
32+
* description: User already exists
33+
* content:
34+
* application/json:
35+
* schema:
36+
* $ref: '#/components/schemas/BusinessErrorDto'
5037
* 500:
5138
* description: Internal server error
52-
*/
53-
54-
/**
55-
* @swagger
56-
* components:
57-
* schemas:
58-
* User:
59-
* type: object
60-
* properties:
61-
* id:
62-
* type: string
63-
* description: Unique identifier generated by the database
64-
* example: 'cl0x1a2b3c4d5e6f7g8h9'
65-
* email:
66-
* type: string
67-
* format: email
68-
* description: User's email address
69-
* example: user@example.com
39+
* content:
40+
* application/json:
41+
* schema:
42+
* $ref: '#/components/schemas/HttpErrorDto'
7043
*/
7144
export async function POST(
72-
req: NextRequest
73-
): Promise<
74-
| NextResponse<{ message: string }>
75-
| NextResponse<{ id: string; email: string }>
76-
> {
77-
try {
78-
const { email, password } = await req.json();
79-
if (!email || !password) {
80-
return NextResponse.json(
81-
{ message: 'Email and password are required' },
82-
{ status: StatusCodes.BAD_REQUEST }
83-
);
84-
}
85-
const exists: User | null = await prisma.user.findUnique({
86-
where: { email },
87-
});
88-
if (exists) {
89-
return NextResponse.json(
90-
{ message: 'User already exists' },
91-
{ status: StatusCodes.CONFLICT }
92-
);
93-
}
94-
const salt: number = parseInt(process.env.BCRYPT_SALT_ROUNDS || '10', 10);
95-
const hashed: string = await hash(password, salt);
96-
const user: User = await prisma.user.create({
97-
data: { email, password: hashed },
98-
});
99-
return NextResponse.json(
100-
{ id: user.id, email: user.email },
101-
{ status: StatusCodes.CREATED }
102-
);
103-
} catch {
104-
return NextResponse.json(
105-
{ message: 'Internal server error' },
106-
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
107-
);
108-
}
45+
request: NextRequest
46+
): Promise<NextResponse<HttpResponseDto<RegisterDataDto>>> {
47+
const payload: RegisterPayloadDto = await request.json();
48+
const registerUseCase: RegisterUseCase = container.resolve(RegisterUseCase);
49+
return await handleApiRequest<RegisterDataDto>(async () => {
50+
const userCreated: UserModelDto = await registerUseCase.handle(payload);
51+
const response: RegisterDataDto = { userCreated };
52+
return response;
53+
}, StatusCodes.CREATED);
10954
}

src/app/api/layout.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ export const metadata: Metadata = {
88
description: 'The API of the Locklite password manager',
99
};
1010

11+
/**
12+
* @swagger
13+
* tags:
14+
* - name: Auth
15+
* description: Authentication and user management
16+
* - name: Vaults
17+
* description: Manage password vault entries and encryption.
18+
*/
19+
1120
export default function RootLayout({
1221
children,
1322
}: SharedLayoutProps): JSX.Element {

src/app/api/vaults/[id]/route.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import type { HttpOptions } from '@shared/dto/input/options/abstract/http-option
1111
* @swagger
1212
* /api/vaults/{id}:
1313
* delete:
14-
* tags:
15-
* - Vaults
14+
* tags: [Vaults]
1615
* summary: Delete a vault by ID
1716
* parameters:
1817
* - in: path

src/app/api/vaults/route.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import type { CreateVaultPayloadDto } from '@shared/dto/input/payloads/create-va
1515
* @swagger
1616
* /api/vaults:
1717
* get:
18-
* tags:
19-
* - Vaults
18+
* tags: [Vaults]
2019
* summary: Get my vaults
2120
* responses:
2221
* 200:
@@ -48,8 +47,7 @@ export async function GET(): Promise<
4847
* @swagger
4948
* /api/vaults:
5049
* post:
51-
* tags:
52-
* - Vaults
50+
* tags: [Vaults]
5351
* summary: Create a vault
5452
* requestBody:
5553
* required: true
@@ -69,13 +67,13 @@ export async function GET(): Promise<
6967
* content:
7068
* application/json:
7169
* schema:
72-
* $ref: '#/components/schemas/HttpErrorDto'
70+
* $ref: '#/components/schemas/BusinessErrorDto'
7371
* 422:
7472
* description: The vault label must not exceed 255 characters
7573
* content:
7674
* application/json:
7775
* schema:
78-
* $ref: '#/components/schemas/HttpErrorDto'
76+
* $ref: '#/components/schemas/BusinessErrorDto'
7977
* 500:
8078
* description: Internal Server Error
8179
* content:

src/lib/auth.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import 'reflect-metadata';
12
import type { NextAuthOptions } from 'next-auth';
23
import CredentialsProvider from 'next-auth/providers/credentials';
34
import { PrismaAdapter } from '@next-auth/prisma-adapter';
45
import prisma from '@lib/prisma';
5-
import { compare } from 'bcrypt';
6-
import type { User } from '@prisma/generated';
76
import { RoutesEnum } from '@ui/router/routes.enum';
7+
import { SignInUseCase } from '@api/usecases/auth/signin.usecase';
8+
import { container } from 'tsyringe';
9+
import type { UserModelDto } from '@shared/dto/models/user.model.dto';
10+
import type { SignInPayloadDto } from '@shared/dto/input/payloads/sign-in.payload.dto';
811

912
export const authOptions: NextAuthOptions = {
1013
adapter: PrismaAdapter(prisma),
@@ -16,18 +19,11 @@ export const authOptions: NextAuthOptions = {
1619
email: { label: 'Email', type: 'text' },
1720
password: { label: 'Password', type: 'password' },
1821
},
19-
async authorize(credentials) {
20-
if (!credentials) return null;
21-
const user: User | null = await prisma.user.findUnique({
22-
where: { email: credentials.email },
23-
});
24-
if (!user) return null;
25-
const isValid: boolean = await compare(
26-
credentials.password,
27-
user.password
28-
);
29-
if (!isValid) return null;
30-
return { id: user.id, email: user.email };
22+
async authorize(
23+
credentials: SignInPayloadDto | undefined
24+
): Promise<UserModelDto | null> {
25+
const signInUseCase: SignInUseCase = container.resolve(SignInUseCase);
26+
return await signInUseCase.handle(credentials || null);
3127
},
3228
}),
3329
],
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { injectable } from 'tsyringe';
2+
import { IAdapter } from '@api/adapters/abstract/adapter.interface';
3+
import { User } from '@prisma/generated';
4+
import { UserModelDto } from '@shared/dto/models/user.model.dto';
5+
6+
@injectable()
7+
export class UserAdapter implements IAdapter<User, UserModelDto> {
8+
public getDtoFromEntity(entity: User): UserModelDto {
9+
return {
10+
id: entity.id,
11+
email: entity.email,
12+
// eslint-disable-next-line no-undefined
13+
name: entity.name || undefined,
14+
};
15+
}
16+
17+
public getDtoListFromEntities(entities: User[]): UserModelDto[] {
18+
return entities.map((entity: User) => this.getDtoFromEntity(entity));
19+
}
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { StatusCodes } from 'http-status-codes';
2+
import { BusinessError } from '@shared/errors/business-error';
3+
import { BusinessErrorCodeEnum } from '@shared/errors/business-error-code.enum';
4+
5+
export class UserAlreadyExistsError extends BusinessError {
6+
public constructor(email: string) {
7+
super(
8+
`User with email '${email}' already exists`,
9+
StatusCodes.CONFLICT,
10+
BusinessErrorCodeEnum.USER_ALREADY_EXISTS
11+
);
12+
}
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { injectable } from 'tsyringe';
2+
import { handlePrismaRequest } from '@api/helpers/prisma/handle-prisma-request';
3+
import { User } from '@prisma/generated';
4+
import prisma from '@lib/prisma';
5+
import type { RegisterPayloadDto } from '@shared/dto/input/payloads/register.payload.dto';
6+
7+
@injectable()
8+
export class UsersRepository {
9+
public async findByEmail(email: string): Promise<User | null> {
10+
return await handlePrismaRequest<User | null>(() =>
11+
prisma.user.findUnique({
12+
where: { email },
13+
})
14+
);
15+
}
16+
17+
public async create(payload: RegisterPayloadDto): Promise<User> {
18+
return await handlePrismaRequest<User>(() =>
19+
prisma.user.create({ data: payload })
20+
);
21+
}
22+
}

0 commit comments

Comments
 (0)