UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

221 lines (184 loc) 6.72 kB
/** biome-ignore-all lint/style/noNonNullAssertion: This file uses valid use cases for non-null assertion */ import { Password, User } from 'studiocms:auth/lib'; import { developerConfig } from 'studiocms:config'; import { apiResponseLogger } from 'studiocms:logger'; import { Notifications } from 'studiocms:notifier'; import { SDKCore } from 'studiocms:sdk'; import type { tsUsersUpdate } from 'studiocms:sdk/types'; import { z } from 'astro/zod'; import { AllResponse, createEffectAPIRoutes, createJsonResponse, Effect, genLogger, OptionsResponse, readAPIContextJson, } from '../../../effect.js'; type UserBasicUpdate = Omit<tsUsersUpdate, 'id'>; type UserPasswordUpdate = { currentPassword: string | null; newPassword: string; confirmNewPassword: string; }; type BasicUserProfileUpdate = { mode: 'basic'; data: UserBasicUpdate; }; type PasswordProfileUpdate = { mode: 'password'; data: UserPasswordUpdate; }; type AvatarProfileUpdate = { mode: 'avatar'; }; type UserProfileUpdate = BasicUserProfileUpdate | PasswordProfileUpdate | AvatarProfileUpdate; export const { POST, OPTIONS, ALL } = createEffectAPIRoutes( { POST: (ctx) => genLogger('studiocms/routes/api/dashboard/profile.POST')(function* () { const [pass, userHelper, notify, sdk] = yield* Effect.all([ Password, User, Notifications, SDKCore, ]); // Check if demo mode is enabled if (developerConfig.demoMode !== false) { return apiResponseLogger(403, 'Demo mode is enabled, this action is not allowed.'); } // Get user data const userData = ctx.locals.StudioCMS.security?.userSessionData; // Check if user is logged in if (!userData?.isLoggedIn) { return apiResponseLogger(401, 'Unauthorized'); } // Check if user has permission if (!ctx.locals.StudioCMS.security?.userPermissionLevel.isVisitor) { return apiResponseLogger(403, 'Unauthorized'); } // Get Json Data const userProfileUpdate = yield* readAPIContextJson<UserProfileUpdate>(ctx); switch (userProfileUpdate.mode) { case 'basic': { const { data: r } = userProfileUpdate; const data = r; if (!data.name) { return apiResponseLogger(400, 'Invalid form data, name is required'); } if (!data.email) { return apiResponseLogger(400, 'Invalid form data, email is required'); } if (!data.username) { return apiResponseLogger(400, 'Invalid form data, username is required'); } // If the username is invalid, return an error const verifyUsernameResponse = yield* userHelper.verifyUsernameInput(data.username); if (verifyUsernameResponse !== true) { return apiResponseLogger(400, verifyUsernameResponse); } // If the email is invalid, return an error const checkEmail = z.coerce .string() .email({ message: 'Email address is invalid' }) .safeParse(data.email); if (!checkEmail.success) return apiResponseLogger(400, `Invalid email: ${checkEmail.error.message}`); const { usernameSearch, emailSearch } = yield* sdk.AUTH.user.searchUsersForUsernameOrEmail(data.username, checkEmail.data); if (userData.user?.username !== data.username) { if (usernameSearch.length > 0) return apiResponseLogger(400, 'Invalid username: Username is already in use'); } if (userData.user?.email !== data.email) { if (emailSearch.length > 0) return apiResponseLogger(400, 'Invalid email: Email is already in use'); } yield* sdk.AUTH.user.update(userData.user!.id!, data); return apiResponseLogger(200, 'User profile updated successfully'); } case 'password': { const { data: r } = userProfileUpdate; const data = r; const { currentPassword, newPassword, confirmNewPassword } = data; if (!currentPassword) { if (userData.user?.password) { return apiResponseLogger(400, 'Invalid form data, current password is required'); } } if (!newPassword) { return apiResponseLogger(400, 'Invalid form data, new password is required'); } // Verify the current password matches the stored hash (when one exists) if (currentPassword && userData.user?.password) { const isValid = yield* pass.verifyPasswordHash( userData.user.password, currentPassword ); if (!isValid) { return apiResponseLogger(400, 'Invalid current password'); } } if (!confirmNewPassword) { return apiResponseLogger(400, 'Invalid form data, confirm new password is required'); } if (newPassword !== confirmNewPassword) { return apiResponseLogger( 400, 'Invalid form data, new password and confirm new password do not match' ); } // If the password is invalid, return an error const verifyPasswordResponse = yield* pass.verifyPasswordStrength(newPassword); if (verifyPasswordResponse !== true) { return apiResponseLogger(400, verifyPasswordResponse); } const userUpdate = { password: yield* pass.hashPassword(newPassword), }; if (userData.user) yield* Effect.all([ sdk.AUTH.user.update(userData.user.id, userUpdate), notify.sendUserNotification('account_updated', userData.user.id), notify.sendAdminNotification('user_updated', userData.user.username), ]); return apiResponseLogger(200, 'User password updated successfully'); } case 'avatar': { if (!userData.user?.email) { return apiResponseLogger(400, 'User email required'); } if (userData.user) yield* userHelper .createUserAvatar(userData.user.email) .pipe( Effect.flatMap((newAvatar) => Effect.all([ sdk.AUTH.user.update(userData.user!.id, { avatar: newAvatar }), notify.sendUserNotification('account_updated', userData.user!.id), notify.sendAdminNotification('user_updated', userData.user!.username), ]) ) ); return apiResponseLogger(200, 'User Avatar updated successfully'); } default: return apiResponseLogger(400, 'Invalid form data, mode is required or unsupported'); } }).pipe(Notifications.Provide), OPTIONS: () => Effect.try(() => OptionsResponse({ allowedMethods: ['POST'] })), ALL: () => Effect.try(() => AllResponse()), }, { cors: { methods: ['POST', 'OPTIONS'] }, onError: (error) => { console.error('API Error:', error); return createJsonResponse( { error: 'Internal Server Error' }, { status: 500, } ); }, } );