UNPKG

studiocms

Version:

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

220 lines (184 loc) 6.08 kB
import { User } from 'studiocms:auth/lib'; import { apiResponseLogger } from 'studiocms:logger'; import { Mailer } from 'studiocms:mailer'; import getTemplate from 'studiocms:mailer/templates'; import { Notifications } from 'studiocms:notifier'; import { SDKCore } from 'studiocms:sdk'; import type { AvailablePermissionRanks } from '@withstudiocms/auth-kit/types'; import type { APIContext } from 'astro'; import { z } from 'astro/zod'; import { AllResponse, appendSearchParamsToUrl, createEffectAPIRoutes, createJsonResponse, Effect, genLogger, OptionsResponse, pipe, readAPIContextJson, } from '../../../effect.js'; type JSONData = { username: string | undefined; email: string | undefined; displayname: string | undefined; rank: AvailablePermissionRanks | undefined; originalUrl: string; }; type Token = { id: string; userId: string; token: string; }; const generateResetUrl = ( { locals: { StudioCMS: { routeMap: { mainLinks: { dashboardIndex }, }, }, }, }: APIContext, baseUrl: string, { id, userId, token }: Token ) => { const resetURL = new URL(`${dashboardIndex}/password-reset`, baseUrl); return pipe( resetURL, appendSearchParamsToUrl('userid', userId), appendSearchParamsToUrl('token', token), appendSearchParamsToUrl('id', id) ); }; const noMailerError = (message: string, resetLink: URL) => `Failed to send email: ${message}. You can provide the following Reset link to your User: ${resetLink}`; export const { POST, OPTIONS, ALL } = createEffectAPIRoutes( { POST: (ctx) => genLogger('studiocms/routes/api/dashboard/create-user-invite.POST')(function* () { const [userHelper, mailer, notify, sdk] = yield* Effect.all([ User, Mailer, Notifications, SDKCore, ]); const siteConfig = ctx.locals.StudioCMS.siteConfig.data; if (!siteConfig) { return apiResponseLogger(500, 'Failed to get site config'); } // 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'); } const { username, email, displayname, rank, originalUrl } = 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 (!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'); } const userPerms = ctx.locals.StudioCMS.security?.userPermissionLevel; if (rank === 'owner' && !userPerms?.isOwner) { return apiResponseLogger(403, 'Unauthorized'); } // 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, { usernameSearch, emailSearch }] = yield* Effect.all([ userHelper.verifyUsernameInput(username), sdk.AUTH.user.searchUsersForUsernameOrEmail(username, checkEmail.data), ]); if (verifyUsernameResponse !== true) { return apiResponseLogger(400, verifyUsernameResponse); } 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'); } // Creates a new user invite const token = yield* sdk.AUTH.user .create( { username, email: checkEmail.data, name: displayname, createdAt: new Date(), id: crypto.randomUUID(), }, rank ) .pipe(Effect.flatMap((newUser) => sdk.resetTokenBucket.new(newUser.id))); if (!token) { return apiResponseLogger(500, 'Failed to create reset token'); } const resetLink = generateResetUrl(ctx, originalUrl, token); yield* notify.sendAdminNotification('new_user', username); if (siteConfig.enableMailer) { const checkMailConnection = yield* mailer.verifyMailConnection; if (!checkMailConnection) { return apiResponseLogger( 500, noMailerError('Failed to connect to mail server', resetLink) ); } if ('error' in checkMailConnection) { return apiResponseLogger( 500, noMailerError('Failed to connect to mail server', resetLink) ); } const htmlTemplate = getTemplate('userInvite'); const mailResponse = yield* mailer.sendMail({ to: checkEmail.data, subject: `You have been invited to join ${siteConfig.title}!`, html: htmlTemplate({ title: siteConfig.title, link: resetLink }), }); if (!mailResponse) { return apiResponseLogger(500, noMailerError('Failed to send email', resetLink)); } if ('error' in mailResponse) { return apiResponseLogger(500, noMailerError(mailResponse.error, resetLink)); } return apiResponseLogger(200, 'User invite created and email sent'); } return apiResponseLogger(200, resetLink.toString()); }).pipe(Mailer.Provide, 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, } ); }, } );