diff --git a/.dvmrc b/.dvmrc index 0bee604..cc6c9a4 100644 --- a/.dvmrc +++ b/.dvmrc @@ -1 +1 @@ -2.3.3 +2.3.5 diff --git a/Dockerfile b/Dockerfile index 0f0f516..1cf5ad3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM denoland/deno:ubuntu-2.3.3 +FROM denoland/deno:ubuntu-2.3.5 EXPOSE 8000 diff --git a/bewcloud.config.sample.ts b/bewcloud.config.sample.ts index e2de4e5..bef75c6 100644 --- a/bewcloud.config.sample.ts +++ b/bewcloud.config.sample.ts @@ -7,7 +7,7 @@ const config: PartialDeep = { allowSignups: false, // If true, anyone can sign up for an account. Note that it's always possible to sign up for the first user, and they will be an admin enableEmailVerification: false, // If true, email verification will be required for signups (using SMTP settings below) enableForeverSignup: true, // If true, all signups become active for 100 years - enableMultiFactor: false, // If true, users can enable multi-factor authentication (TOTP or Passkeys) + enableMultiFactor: false, // If true, users can enable multi-factor authentication (TOTP, Passkeys, or Email if the SMTP settings below are set) // allowedCookieDomains: ['example.com', 'example.net'], // Can be set to allow more than the baseUrl's domain for session cookies // skipCookieDomainSecurity: true, // If true, the cookie domain will not be strictly set and checked against. This skipping slightly reduces security, but is usually necessary for reverse proxies like Cloudflare Tunnel }, diff --git a/components/auth/MultiFactorAuthVerifyForm.tsx b/components/auth/MultiFactorAuthVerifyForm.tsx index 9030bec..821b28e 100644 --- a/components/auth/MultiFactorAuthVerifyForm.tsx +++ b/components/auth/MultiFactorAuthVerifyForm.tsx @@ -13,6 +13,7 @@ export default function MultiFactorAuthVerifyForm( ) { const hasPasskey = availableMethods.includes('passkey'); const hasTotp = availableMethods.includes('totp'); + const hasEmail = availableMethods.includes('email'); return (
@@ -34,6 +35,48 @@ export default function MultiFactorAuthVerifyForm( ) : null} + {hasEmail + ? ( +
+
+ + +
+ +
+ +
+
+ ) + : null} + + {hasEmail && hasTotp + ? ( +
+

or

+
+ ) + : null} + {hasTotp ? (

or

diff --git a/docker-compose.yml b/docker-compose.yml index ffca259..2b7a3b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: website: - image: ghcr.io/bewcloud/bewcloud:v2.0.0 + image: ghcr.io/bewcloud/bewcloud:v2.1.0 restart: always ports: - 127.0.0.1:8000:8000 diff --git a/fresh.gen.ts b/fresh.gen.ts index 7accc1d..f37d247 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -6,6 +6,7 @@ import * as $_404 from './routes/_404.tsx'; import * as $_app from './routes/_app.tsx'; import * as $_middleware from './routes/_middleware.tsx'; import * as $api_auth_multi_factor_disable from './routes/api/auth/multi-factor/disable.ts'; +import * as $api_auth_multi_factor_email_setup from './routes/api/auth/multi-factor/email/setup.ts'; import * as $api_auth_multi_factor_enable from './routes/api/auth/multi-factor/enable.ts'; import * as $api_auth_multi_factor_passkey_begin from './routes/api/auth/multi-factor/passkey/begin.ts'; import * as $api_auth_multi_factor_passkey_setup_begin from './routes/api/auth/multi-factor/passkey/setup-begin.ts'; @@ -78,6 +79,7 @@ const manifest = { './routes/_app.tsx': $_app, './routes/_middleware.tsx': $_middleware, './routes/api/auth/multi-factor/disable.ts': $api_auth_multi_factor_disable, + './routes/api/auth/multi-factor/email/setup.ts': $api_auth_multi_factor_email_setup, './routes/api/auth/multi-factor/enable.ts': $api_auth_multi_factor_enable, './routes/api/auth/multi-factor/passkey/begin.ts': $api_auth_multi_factor_passkey_begin, './routes/api/auth/multi-factor/passkey/setup-begin.ts': $api_auth_multi_factor_passkey_setup_begin, diff --git a/islands/auth/MultiFactorAuthSettings.tsx b/islands/auth/MultiFactorAuthSettings.tsx index 03aa0cb..6f383e9 100644 --- a/islands/auth/MultiFactorAuthSettings.tsx +++ b/islands/auth/MultiFactorAuthSettings.tsx @@ -14,6 +14,10 @@ import { RequestBody as TOTPSetupRequestBody, ResponseBody as TOTPSetupResponseBody, } from '/routes/api/auth/multi-factor/totp/setup.ts'; +import { + RequestBody as EmailSetupRequestBody, + ResponseBody as EmailSetupResponseBody, +} from '/routes/api/auth/multi-factor/email/setup.ts'; import { RequestBody as MultiFactorAuthEnableRequestBody, ResponseBody as MultiFactorAuthEnableResponseBody, @@ -48,20 +52,27 @@ interface PasskeySetupData { type: 'passkey'; } +interface EmailSetupData { + methodId: string; + type: 'email'; +} + const methodTypeLabels: Record = { totp: 'Authenticator App', passkey: 'Passkey', + email: 'Email', }; const methodTypeDescriptions: Record = { - totp: 'Use an authenticator app like Aegis Authenticator or Google Authenticator to generate codes', - passkey: 'Use biometric authentication or security keys', + totp: 'Use an authenticator app like Aegis Authenticator or Google Authenticator to generate codes.', + passkey: 'Use biometric authentication or security keys.', + email: 'Receive codes in your email.', }; -const availableMethodTypes = ['totp', 'passkey'] as MultiFactorAuthMethodType[]; +const availableMethodTypes = ['totp', 'passkey', 'email'] as MultiFactorAuthMethodType[]; export default function MultiFactorAuthSettings({ methods }: MultiFactorAuthSettingsProps) { - const setupData = useSignal(null); + const setupData = useSignal(null); const isLoading = useSignal(false); const error = useSignal(null); const success = useSignal(null); @@ -146,6 +157,26 @@ export default function MultiFactorAuthSettings({ methods }: MultiFactorAuthSett }; }; + const setupEmail = async () => { + const requestBody: EmailSetupRequestBody = {}; + + const response = await fetch('/api/auth/multi-factor/email/setup', { + method: 'POST', + body: JSON.stringify(requestBody), + }); + + const data = await response.json() as EmailSetupResponseBody; + + if (!data.success || !data.data) { + throw new Error(data.error || 'Failed to setup email multi-factor authentication'); + } + + setupData.value = { + type: 'email', + methodId: data.data.methodId!, + }; + }; + const setupMultiFactorAuth = async (type: MultiFactorAuthMethodType) => { isLoading.value = true; error.value = null; @@ -155,6 +186,8 @@ export default function MultiFactorAuthSettings({ methods }: MultiFactorAuthSett await setupTOTP(); } else if (type === 'passkey') { await setupPasskey(); + } else if (type === 'email') { + await setupEmail(); } } catch (setupError) { error.value = (setupError as Error).message; @@ -170,7 +203,7 @@ export default function MultiFactorAuthSettings({ methods }: MultiFactorAuthSett } if (setupData.value.type !== 'passkey' && !verificationToken.value) { - error.value = 'Please enter a verification token'; + error.value = 'Please enter a verification code/token'; return; } @@ -324,7 +357,7 @@ export default function MultiFactorAuthSettings({ methods }: MultiFactorAuthSett ) : null} - {setupData.value && setupData.value.type !== 'passkey' + {setupData.value && setupData.value.type === 'totp' ? (

Setup Authenticator App

@@ -421,6 +454,47 @@ export default function MultiFactorAuthSettings({ methods }: MultiFactorAuthSett ) : null} + {setupData.value && setupData.value.type === 'email' + ? ( +
+

Setup Email

+ +
+ + verificationToken.value = (event.target as HTMLInputElement).value} + placeholder='123456' + class='mt-1 input-field' + maxLength={6} + /> +
+ +
+ + +
+
+ ) + : null} + {hasMultiFactorAuthEnabled && !showDisableForm.value ? (
diff --git a/lib/models/email.ts b/lib/models/email.ts index 3c181d1..83cbb15 100644 --- a/lib/models/email.ts +++ b/lib/models/email.ts @@ -1,4 +1,3 @@ -// deno-fmt-ignore-file import nodemailer from 'nodemailer'; import 'std/dotenv/load.ts'; @@ -25,7 +24,7 @@ export class EmailModel { pass: SMTP_PASSWORD, }, }; - + const transporter = nodemailer.createTransport(transporterConfig); const mailOptions = { @@ -45,28 +44,9 @@ export class EmailModel { } } - static async sendVerificationEmail( - email: string, - verificationCode: string, - ) { - const emailTitle = 'Verify your email in bewCloud'; - - const textBody = ` -${emailTitle} ------------------------- - -You or someone who knows your email is trying to verify it in bewCloud. - -Here's the verification code: - -**${verificationCode}** -=============================== - -This code will expire in 30 minutes. - `; - - /** Based off of https://github.com/ActiveCampaign/postmark-templates/tree/main/templates-inlined/basic/password-reset */ - const htmlBody = ` + /** Based off of https://github.com/ActiveCampaign/postmark-templates/tree/main/templates-inlined/basic/password-reset */ + private static getHtmlBody(title: string, htmlBody: string) { + return ` @@ -75,7 +55,7 @@ This code will expire in 30 minutes. - ${escapeHtml(emailTitle)} + ${escapeHtml(title)}