import { Handlers, PageProps } from 'fresh/server.ts'; import { generateHash, validateEmail } from '/lib/utils/misc.ts'; import { createSessionResponse, PASSWORD_SALT } from '/lib/auth.ts'; import { FormField, generateFieldHtml, getFormDataField } from '/lib/form-utils.tsx'; import { UserModel, VerificationCodeModel } from '/lib/models/user.ts'; import { sendVerifyEmailEmail } from '/lib/providers/brevo.ts'; import { FreshContextState } from '/lib/types.ts'; import { AppConfig } from '/lib/config.ts'; import { isMultiFactorAuthEnabledForUser } from '/lib/utils/multi-factor-auth.ts'; import { MultiFactorAuthModel } from '/lib/models/multi-factor-auth.ts'; import PasswordlessPasskeyLogin from '/islands/auth/PasswordlessPasskeyLogin.tsx'; interface Data { error?: string; notice?: string; email?: string; formData?: FormData; isEmailVerificationEnabled: boolean; isMultiFactorAuthEnabled: boolean; helpEmail: string; } export const handler: Handlers = { async GET(request, context) { if (context.state.user) { return new Response('Redirect', { status: 303, headers: { 'Location': `/` } }); } const isEmailVerificationEnabled = await AppConfig.isEmailVerificationEnabled(); const isMultiFactorAuthEnabled = await AppConfig.isMultiFactorAuthEnabled() && await UserModel.isThereAnAdmin(); const helpEmail = (await AppConfig.getConfig()).visuals.helpEmail; const searchParams = new URL(request.url).searchParams; const formData = new FormData(); let notice = ''; let email = ''; if (searchParams.get('success') === 'signup') { email = searchParams.get('email') || ''; formData.set('email', email); if (await AppConfig.isEmailVerificationEnabled()) { notice = `You have received a code in your email. Use it to verify your email and login.`; } else { notice = `Your account was created successfully. Login below.`; } } return await context.render({ notice, email, formData, isEmailVerificationEnabled, isMultiFactorAuthEnabled, helpEmail, }); }, async POST(request, context) { if (context.state.user) { return new Response('Redirect', { status: 303, headers: { 'Location': `/` } }); } const isEmailVerificationEnabled = await AppConfig.isEmailVerificationEnabled(); const isMultiFactorAuthEnabled = await AppConfig.isMultiFactorAuthEnabled() && await UserModel.isThereAnAdmin(); const helpEmail = (await AppConfig.getConfig()).visuals.helpEmail; const searchParams = new URL(request.url).searchParams; const formData = await request.clone().formData(); const email = getFormDataField(formData, 'email'); const redirectUrl = searchParams.get('redirect') || '/'; try { if (!validateEmail(email)) { throw new Error(`Invalid email.`); } const password = getFormDataField(formData, 'password'); if (password.length < 6) { throw new Error(`Password is too short.`); } const hashedPassword = await generateHash(`${password}:${PASSWORD_SALT}`, 'SHA-256'); const user = await UserModel.getByEmail(email); if (!user || user.hashed_password !== hashedPassword) { throw new Error('Email not found or invalid password.'); } const isEmailVerificationEnabled = await AppConfig.isEmailVerificationEnabled(); if (!isEmailVerificationEnabled && !user.extra.is_email_verified) { user.extra.is_email_verified = true; await UserModel.update(user); } if (!user.extra.is_email_verified) { const code = getFormDataField(formData, 'verification-code'); if (!code) { const verificationCode = await VerificationCodeModel.create(user, user.email, 'email'); await sendVerifyEmailEmail(user.email, verificationCode); throw new Error('Email not verified. New code sent to verify your email.'); } else { await VerificationCodeModel.validate(user, user.email, code, 'email'); user.extra.is_email_verified = true; await UserModel.update(user); } } if (user.extra.is_email_verified && isMultiFactorAuthEnabled && isMultiFactorAuthEnabledForUser(user)) { return MultiFactorAuthModel.createSessionResponse(request, user, { urlToRedirectTo: redirectUrl }); } return createSessionResponse(request, user, { urlToRedirectTo: redirectUrl }); } catch (error) { console.error(error); return await context.render({ error: (error as Error).toString(), email, formData, isEmailVerificationEnabled, isMultiFactorAuthEnabled, helpEmail, }); } }, }; function formFields(email?: string, showVerificationCode = false) { const fields: FormField[] = [ { name: 'email', label: 'Email', type: 'email', placeholder: 'jane.doe@example.com', value: email || '', required: true, }, { name: 'password', label: 'Password', type: 'password', placeholder: 'super-SECRET-passphrase', required: true, }, ]; if (showVerificationCode) { fields.push({ name: 'verification-code', label: 'Verification Code', description: `The verification code to validate your email.`, type: 'text', placeholder: '000000', required: true, }); } return fields; } export default function Login({ data }: PageProps) { return (

Login

{data?.error ? (

Failed to login!

{data?.error}

) : null} {data?.notice ? (

{data?.isEmailVerificationEnabled ? 'Verify your email!' : 'Account created!'}

{data?.notice}

) : null}
{formFields( data?.email, data?.notice?.includes('verify your email') && data?.isEmailVerificationEnabled, ).map((field) => generateFieldHtml(field, data?.formData || new FormData()))}
{data?.isMultiFactorAuthEnabled ? (

or

) : null}

Need an account?

If you still don't have an account,{' '} signup .

{data?.helpEmail !== '' ? ( <>

Need help?

If you're having any issues or have any questions,{' '} please reach out .

) : null}
); }