Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { authOptions } from '@lib/auth';
* @swagger
* /api/auth/providers:
* get:
* tags:
* - Auth
* tags: [Auth]
* summary: Retrieve configured authentication providers
* description: By Next Auth
* responses:
Expand All @@ -24,8 +23,7 @@ import { authOptions } from '@lib/auth';
* @swagger
* /api/auth/session:
* get:
* tags:
* - Auth
* tags: [Auth]
* summary: Get current user session
* description: By Next Auth
* responses:
Expand All @@ -43,8 +41,7 @@ import { authOptions } from '@lib/auth';
* @swagger
* /api/auth/csrf:
* get:
* tags:
* - Auth
* tags: [Auth]
* summary: Retrieve CSRF token
* description: By Next Auth
* responses:
Expand All @@ -62,8 +59,7 @@ import { authOptions } from '@lib/auth';
* @swagger
* /api/auth/signin:
* post:
* tags:
* - Auth
* tags: [Auth]
* summary: Sign in using credentials provider
* description: By Next Auth
* requestBody:
Expand All @@ -83,8 +79,7 @@ import { authOptions } from '@lib/auth';
* @swagger
* /api/auth/signout:
* post:
* tags:
* - Auth
* tags: [Auth]
* summary: Sign out current user
* description: By Next Auth
* responses:
Expand All @@ -98,8 +93,7 @@ import { authOptions } from '@lib/auth';
* @swagger
* /api/auth/callback/{provider}:
* get:
* tags:
* - Auth
* tags: [Auth]
* summary: OAuth callback endpoint for a given provider
* description: By Next Auth
* parameters:
Expand Down
115 changes: 30 additions & 85 deletions src/app/api/auth/register/route.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,54 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import prisma from '@lib/prisma';
import { hash } from 'bcrypt';
import 'reflect-metadata';
import type { NextRequest, NextResponse } from 'next/server';
import type { HttpResponseDto } from '@shared/dto/output/responses/abstract/http.response.dto';
import type { RegisterDataDto } from '@shared/dto/output/data/register.data.dto';
import type { RegisterPayloadDto } from '@shared/dto/input/payloads/register.payload.dto';
import { container } from 'tsyringe';
import { handleApiRequest } from '@api/helpers/api/handle-api-request';
import { StatusCodes } from 'http-status-codes';
import type { User } from '@prisma/generated';
/**
* @swagger
* tags:
* - name: Auth
* description: Authentication and user management
*/
import type { UserModelDto } from '@shared/dto/models/user.model.dto';
import { RegisterUseCase } from '@api/usecases/auth/register.usecase';

/**
* @swagger
* /api/auth/register:
* post:
* summary: Register a new user
* tags: [Auth]
* summary: Register a new user
* requestBody:
* description: JSON object containing email and password
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* format: email
* example: user@example.com
* password:
* type: string
* format: password
* example: securePassword123
* $ref: '#/components/schemas/RegisterPayloadDto'
* responses:
* 201:
* description: User created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
* 400:
* description: Bad request (missing email or password)
* $ref: '#/components/schemas/RegisterDataDto'
* 409:
* description: Conflict user already exists
* description: User already exists
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/BusinessErrorDto'
* 500:
* description: Internal server error
*/

/**
* @swagger
* components:
* schemas:
* User:
* type: object
* properties:
* id:
* type: string
* description: Unique identifier generated by the database
* example: 'cl0x1a2b3c4d5e6f7g8h9'
* email:
* type: string
* format: email
* description: User's email address
* example: user@example.com
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpErrorDto'
*/
export async function POST(
req: NextRequest
): Promise<
| NextResponse<{ message: string }>
| NextResponse<{ id: string; email: string }>
> {
try {
const { email, password } = await req.json();
if (!email || !password) {
return NextResponse.json(
{ message: 'Email and password are required' },
{ status: StatusCodes.BAD_REQUEST }
);
}
const exists: User | null = await prisma.user.findUnique({
where: { email },
});
if (exists) {
return NextResponse.json(
{ message: 'User already exists' },
{ status: StatusCodes.CONFLICT }
);
}
const salt: number = parseInt(process.env.BCRYPT_SALT_ROUNDS || '10', 10);
const hashed: string = await hash(password, salt);
const user: User = await prisma.user.create({
data: { email, password: hashed },
});
return NextResponse.json(
{ id: user.id, email: user.email },
{ status: StatusCodes.CREATED }
);
} catch {
return NextResponse.json(
{ message: 'Internal server error' },
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
);
}
request: NextRequest
): Promise<NextResponse<HttpResponseDto<RegisterDataDto>>> {
const payload: RegisterPayloadDto = await request.json();
const registerUseCase: RegisterUseCase = container.resolve(RegisterUseCase);
return await handleApiRequest<RegisterDataDto>(async () => {
const userCreated: UserModelDto = await registerUseCase.handle(payload);
const response: RegisterDataDto = { userCreated };
return response;
}, StatusCodes.CREATED);
}
9 changes: 9 additions & 0 deletions src/app/api/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export const metadata: Metadata = {
description: 'The API of the Locklite password manager',
};

/**
* @swagger
* tags:
* - name: Auth
* description: Authentication and user management
* - name: Vaults
* description: Manage password vault entries and encryption.
*/

export default function RootLayout({
children,
}: SharedLayoutProps): JSX.Element {
Expand Down
3 changes: 1 addition & 2 deletions src/app/api/vaults/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import type { HttpOptions } from '@shared/dto/input/options/abstract/http-option
* @swagger
* /api/vaults/{id}:
* delete:
* tags:
* - Vaults
* tags: [Vaults]
* summary: Delete a vault by ID
* parameters:
* - in: path
Expand Down
10 changes: 4 additions & 6 deletions src/app/api/vaults/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import type { CreateVaultPayloadDto } from '@shared/dto/input/payloads/create-va
* @swagger
* /api/vaults:
* get:
* tags:
* - Vaults
* tags: [Vaults]
* summary: Get my vaults
* responses:
* 200:
Expand Down Expand Up @@ -48,8 +47,7 @@ export async function GET(): Promise<
* @swagger
* /api/vaults:
* post:
* tags:
* - Vaults
* tags: [Vaults]
* summary: Create a vault
* requestBody:
* required: true
Expand All @@ -69,13 +67,13 @@ export async function GET(): Promise<
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpErrorDto'
* $ref: '#/components/schemas/BusinessErrorDto'
* 422:
* description: The vault label must not exceed 255 characters
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/HttpErrorDto'
* $ref: '#/components/schemas/BusinessErrorDto'
* 500:
* description: Internal Server Error
* content:
Expand Down
24 changes: 10 additions & 14 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'reflect-metadata';
import type { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import prisma from '@lib/prisma';
import { compare } from 'bcrypt';
import type { User } from '@prisma/generated';
import { RoutesEnum } from '@ui/router/routes.enum';
import { SignInUseCase } from '@api/usecases/auth/signin.usecase';
import { container } from 'tsyringe';
import type { UserModelDto } from '@shared/dto/models/user.model.dto';
import type { SignInPayloadDto } from '@shared/dto/input/payloads/sign-in.payload.dto';

export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
Expand All @@ -16,18 +19,11 @@ export const authOptions: NextAuthOptions = {
email: { label: 'Email', type: 'text' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials) return null;
const user: User | null = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user) return null;
const isValid: boolean = await compare(
credentials.password,
user.password
);
if (!isValid) return null;
return { id: user.id, email: user.email };
async authorize(
credentials: SignInPayloadDto | undefined
): Promise<UserModelDto | null> {
const signInUseCase: SignInUseCase = container.resolve(SignInUseCase);
return await signInUseCase.handle(credentials || null);
},
}),
],
Expand Down
20 changes: 20 additions & 0 deletions src/modules/api/adapters/user.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { injectable } from 'tsyringe';
import { IAdapter } from '@api/adapters/abstract/adapter.interface';
import { User } from '@prisma/generated';
import { UserModelDto } from '@shared/dto/models/user.model.dto';

@injectable()
export class UserAdapter implements IAdapter<User, UserModelDto> {
public getDtoFromEntity(entity: User): UserModelDto {
return {
id: entity.id,
email: entity.email,
// eslint-disable-next-line no-undefined
name: entity.name || undefined,
};
}

public getDtoListFromEntities(entities: User[]): UserModelDto[] {
return entities.map((entity: User) => this.getDtoFromEntity(entity));
}
}
13 changes: 13 additions & 0 deletions src/modules/api/errors/business/auth/user-already-exists.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StatusCodes } from 'http-status-codes';
import { BusinessError } from '@shared/errors/business-error';
import { BusinessErrorCodeEnum } from '@shared/errors/business-error-code.enum';

export class UserAlreadyExistsError extends BusinessError {
public constructor(email: string) {
super(
`User with email '${email}' already exists`,
StatusCodes.CONFLICT,
BusinessErrorCodeEnum.USER_ALREADY_EXISTS
);
}
}
22 changes: 22 additions & 0 deletions src/modules/api/repositories/users.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { injectable } from 'tsyringe';
import { handlePrismaRequest } from '@api/helpers/prisma/handle-prisma-request';
import { User } from '@prisma/generated';
import prisma from '@lib/prisma';
import type { RegisterPayloadDto } from '@shared/dto/input/payloads/register.payload.dto';

@injectable()
export class UsersRepository {
public async findByEmail(email: string): Promise<User | null> {
return await handlePrismaRequest<User | null>(() =>
prisma.user.findUnique({
where: { email },
})
);
}

public async create(payload: RegisterPayloadDto): Promise<User> {
return await handlePrismaRequest<User>(() =>
prisma.user.create({ data: payload })
);
}
}
Loading