UNPKG

studiocms

Version:

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

187 lines (164 loc) 5.61 kB
import { Password, User } from 'studiocms:auth/lib'; import { apiResponseLogger } from 'studiocms:logger'; import { Notifications } from 'studiocms:notifier'; import { SDKCore } from 'studiocms:sdk'; import { type AvailablePermissionRanks, UserPermissionLevel } from '@withstudiocms/auth-kit/types'; import { z } from 'astro/zod'; import { ValidRanks } from '../../../consts.js'; import { AllResponse, createEffectAPIRoutes, createJsonResponse, Effect, genLogger, OptionsResponse, readAPIContextJson, } from '../../../effect.js'; type JSONData = { username: string | undefined; password: string | undefined; email: string | undefined; displayname: string | undefined; rank: AvailablePermissionRanks | undefined; }; export const { POST, OPTIONS, ALL } = createEffectAPIRoutes( { POST: (ctx) => genLogger('studiocms/routes/api/dashboard/create-user.POST')(function* () { const [pass, userHelper, notify, sdk] = yield* Effect.all([ Password, User, Notifications, SDKCore, ]); // Get user data const userData = ctx.locals.StudioCMS.security?.userSessionData; // Check if user is logged in if (!userData?.isLoggedIn) { return apiResponseLogger(403, 'Unauthorized'); } // Check if user has permission const isAuthorized = ctx.locals.StudioCMS.security?.userPermissionLevel.isAdmin; if (!isAuthorized) { return apiResponseLogger(403, 'Unauthorized'); } let { username, password, email, displayname, rank } = yield* readAPIContextJson<JSONData>(ctx); // If the username, password, email, or display name is missing, return an error if (!username) { return apiResponseLogger(400, 'Missing field: Username is required'); } if (!password) { password = yield* sdk.generateRandomPassword(12); } if (!email) { return apiResponseLogger(400, 'Missing field: Email is required'); } if (!displayname) { return apiResponseLogger(400, 'Missing field: Display name is required'); } if (!rank) { return apiResponseLogger(400, 'Missing field: Rank is required'); } // Validate target rank and ensure caller has sufficient privilege if (!ValidRanks.has(rank) || rank === 'unknown') { return apiResponseLogger(400, 'Invalid rank'); } const callerPerm = yield* userHelper.getUserPermissionLevel(userData); const rankToPerm = (r: typeof rank) => { switch (r) { case 'owner': return UserPermissionLevel.owner; case 'admin': return UserPermissionLevel.admin; case 'editor': return UserPermissionLevel.editor; case 'visitor': return UserPermissionLevel.visitor; default: return UserPermissionLevel.unknown; } }; const targetPerm = rankToPerm(rank); // Use explicit weights to avoid relying on enum ordinal const permWeight = (lvl: UserPermissionLevel) => { switch (lvl) { case UserPermissionLevel.owner: return 4; case UserPermissionLevel.admin: return 3; case UserPermissionLevel.editor: return 2; case UserPermissionLevel.visitor: return 1; default: return 0; } }; // Only owners can assign 'owner' if (rank === 'owner' && callerPerm !== UserPermissionLevel.owner) { return createJsonResponse({ error: 'Forbidden' }, { status: 403 }); } // Allow equality for owner→owner; otherwise require caller >= target if (permWeight(callerPerm) < permWeight(targetPerm)) { return createJsonResponse({ error: 'Forbidden' }, { status: 403 }); } // If the email is invalid, return an error const checkEmail = z.coerce .string() .email({ message: 'Email address is invalid' }) .safeParse(email); if (!checkEmail.success) { return apiResponseLogger(400, `Invalid email: ${checkEmail.error.message}`); } const [verifyUsernameResponse, verifyPasswordResponse, { usernameSearch, emailSearch }] = yield* Effect.all([ userHelper.verifyUsernameInput(username), pass.verifyPasswordStrength(password), sdk.AUTH.user.searchUsersForUsernameOrEmail(username, checkEmail.data), ]); // If the username is invalid, return an error if (verifyUsernameResponse !== true) { return apiResponseLogger(400, verifyUsernameResponse); } // If the password is invalid, return an error if (verifyPasswordResponse !== true) { return apiResponseLogger(400, verifyPasswordResponse); } if (usernameSearch.length > 0) { return apiResponseLogger(400, 'Invalid username: Username is already in use'); } if (emailSearch.length > 0) { return apiResponseLogger(400, 'Invalid email: Email is already in use'); } // Create a new user yield* userHelper.createLocalUser(displayname, username, email, password).pipe( Effect.flatMap((newUser) => sdk.UPDATE.permissions({ user: newUser.id, rank: rank, }) ), Effect.tap(() => notify.sendAdminNotification('new_user', username)) ); return apiResponseLogger( 200, JSON.stringify({ username, email, displayname, rank: rank, password }) ); }).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, } ); }, } );