UNPKG

studiocms

Version:

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

220 lines (188 loc) 6.19 kB
import { Password, User } from 'studiocms:auth/lib'; import { apiResponseLogger } from 'studiocms:logger'; import { Notifications } from 'studiocms:notifier'; import { SDKCore } from 'studiocms:sdk'; import { z } from 'astro/zod'; import { AllResponse, createEffectAPIRoutes, createJsonResponse, Effect, genLogger, OptionsResponse, parseAPIContextJson, Schema, } from '../../../../../effect.js'; import { verifyAuthTokenFromHeader } from '../../utils/auth-token.js'; export class JSONData extends Schema.Class<JSONData>('JSONData')({ username: Schema.Union(Schema.String, Schema.Undefined), password: Schema.Union(Schema.String, Schema.Undefined), email: Schema.Union(Schema.String, Schema.Undefined), displayname: Schema.Union(Schema.String, Schema.Undefined), rank: Schema.Union( Schema.Literal('owner'), Schema.Literal('admin'), Schema.Literal('editor'), Schema.Literal('visitor'), Schema.Undefined ), }) {} export const { ALL, GET, POST, OPTIONS } = createEffectAPIRoutes( { GET: (ctx) => genLogger('studioCMS:rest:v1:users:GET')(function* () { const [sdk, user] = yield* Effect.all([SDKCore, verifyAuthTokenFromHeader(ctx)]); if (user instanceof Response) { return user; } const { rank } = user; if (rank !== 'owner' && rank !== 'admin') { return apiResponseLogger(401, 'Unauthorized'); } const users = yield* sdk.GET.users.all(); let data = users.map( ({ avatar, createdAt, email, id, name, permissionsData, updatedAt, url, username }) => ({ avatar, createdAt, email, id, name, rank: permissionsData?.rank ?? 'unknown', updatedAt, url, username, }) ); if (rank !== 'owner') { data = data.filter((user) => user.rank !== 'owner'); } const searchParams = ctx.url.searchParams; const rankFilter = searchParams.get('rank'); const usernameFilter = searchParams.get('username'); const nameFilter = searchParams.get('name'); const usernameFilterLower = usernameFilter?.toLowerCase(); const nameFilterLower = nameFilter?.toLowerCase(); let filteredData = data; if (rankFilter) { filteredData = filteredData.filter((u) => u.rank === rankFilter); } if (usernameFilterLower) { filteredData = filteredData.filter((u) => (u.username ?? '').toLowerCase().includes(usernameFilterLower) ); } if (nameFilterLower) { filteredData = filteredData.filter((u) => (u.name ?? '').toLowerCase().includes(nameFilterLower) ); } return createJsonResponse(filteredData); }), POST: (ctx) => genLogger('studioCMS:rest:v1:users:POST')(function* () { const [sdk, user, userUtils, passwordUtils, notifier] = yield* Effect.all([ SDKCore, verifyAuthTokenFromHeader(ctx), User, Password, Notifications, ]); if (user instanceof Response) { return user; } const { rank } = user; if (rank !== 'owner' && rank !== 'admin') { return apiResponseLogger(401, 'Unauthorized'); } let { username, password, email, displayname, rank: newUserRank, } = yield* parseAPIContextJson(ctx, JSONData); if (!username) { return apiResponseLogger(400, 'Missing field: Username is required'); } // Only owners can assign the 'owner' rank if (newUserRank === 'owner' && rank !== 'owner') { return apiResponseLogger(401, 'Unauthorized'); } 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 (!newUserRank) { return apiResponseLogger(400, 'Missing field: Rank is required'); } // Prevent privilege escalation: admins cannot assign owner rank if (rank === 'admin' && newUserRank === 'owner') { return apiResponseLogger(403, 'Forbidden: insufficient permission to assign owner rank'); } // 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([ userUtils.verifyUsernameInput(username), passwordUtils.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(password); 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 const newUser = yield* userUtils.createLocalUser( displayname, username, checkEmail.data, password ); const updateRank = yield* sdk.UPDATE.permissions({ user: newUser.id, rank: newUserRank, }); yield* notifier.sendAdminNotification('new_user', newUser.username); return apiResponseLogger( 200, JSON.stringify({ username, email: checkEmail.data, displayname, rank: updateRank.rank, password, }) ); }).pipe(Notifications.Provide), OPTIONS: () => Effect.try(() => OptionsResponse({ allowedMethods: ['GET', 'POST'] })), ALL: () => Effect.try(() => AllResponse()), }, { cors: { methods: ['GET', 'POST', 'OPTIONS'] }, onError: (error) => { console.error('API Error:', error); return createJsonResponse({ error: 'Something went wrong' }, { status: 500 }); }, } );