diff --git a/src/app/api/user/me/password/route.ts b/src/app/api/user/me/password/route.ts new file mode 100644 index 0000000..03fb426 --- /dev/null +++ b/src/app/api/user/me/password/route.ts @@ -0,0 +1,119 @@ +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; +import { updateUserPasswordServerSchema } from '../validation'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { FullUserResponseSchema } from '../../validation'; +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; +import bcrypt from 'bcryptjs'; + +export const PATCH = auth(async function PATCH(req) { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const body = await req.json(); + const parsedBody = updateUserPasswordServerSchema.safeParse(body); + if (!parsedBody.success) + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, + { + success: false, + message: 'Invalid request data', + errors: parsedBody.error.issues, + }, + { status: 400 }, + ); + + const { current_password, new_password } = parsedBody.data; + + const dbUser = await prisma.user.findUnique({ + where: { + id: authCheck.user.id, + }, + include: { + accounts: true, + }, + }); + + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User not found', + }, + { status: 404 }, + ); + + if (!dbUser.password_hash) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User does not have a password set', + }, + { status: 400 }, + ); + + if (dbUser.accounts.length === 0 || dbUser.accounts[0].provider !== 'credentials') + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'Credentials login is not enabled for this user', + }, + { status: 400 }, + ); + + const isCurrentPasswordValid = await bcrypt.compare( + current_password, + dbUser.password_hash || '', + ); + + if (!isCurrentPasswordValid) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'Current password is incorrect', + }, + { status: 401 }, + ); + + const hashedNewPassword = await bcrypt.hash(new_password, 10); + + const updatedUser = await prisma.user.update({ + where: { + id: dbUser.id, + }, + data: { + password_hash: hashedNewPassword, + }, + select: { + id: true, + name: true, + first_name: true, + last_name: true, + email: true, + image: true, + timezone: true, + created_at: true, + updated_at: true, + }, + }); + + return returnZodTypeCheckedResponse(FullUserResponseSchema, { + success: true, + user: updatedUser, + }); +}); diff --git a/src/app/api/user/me/password/swagger.ts b/src/app/api/user/me/password/swagger.ts new file mode 100644 index 0000000..0bc62f0 --- /dev/null +++ b/src/app/api/user/me/password/swagger.ts @@ -0,0 +1,43 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { FullUserResponseSchema } from '../../validation'; +import { updateUserPasswordServerSchema } from '../validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'patch', + path: '/api/user/me/password', + description: 'Update the password of the currently authenticated user', + request: { + body: { + description: 'User password update request body', + required: true, + content: { + 'application/json': { + schema: updateUserPasswordServerSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'User information updated successfully', + content: { + 'application/json': { + schema: FullUserResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['User'], + }); +} diff --git a/src/app/api/user/me/validation.ts b/src/app/api/user/me/validation.ts index 49c6219..7fe04f6 100644 --- a/src/app/api/user/me/validation.ts +++ b/src/app/api/user/me/validation.ts @@ -4,6 +4,7 @@ import { lastNameSchema, newUserEmailServerSchema, newUserNameServerSchema, + passwordSchema, } from '@/app/api/user/validation'; // ---------------------------------------- @@ -19,3 +20,11 @@ export const updateUserServerSchema = zod.object({ image: zod.string().optional(), timezone: zod.string().optional(), }); + +export const updateUserPasswordServerSchema = zod.object({ + current_password: zod.string().min(1, 'Current password is required'), + new_password: passwordSchema, + confirm_new_password: passwordSchema, +}).refine((data) => data.new_password === data.confirm_new_password, { + message: 'New password and confirm new password must match', +});