From cf8d72cac350f731ee66042db5e81df528923d25 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 28 Feb 2026 19:39:21 +0200 Subject: [PATCH] login now username+password based --- apps/main/src/lib/auth.client.ts | 2 - .../molecules/google-oauth-button.svelte | 17 -- .../lib/domains/account/account.vm.svelte.ts | 67 +++-- .../lib/domains/security/auth.vm.svelte.ts | 204 ++------------- .../domains/security/email-login-form.svelte | 166 ++++-------- .../security/magic-link-verification.svelte | 228 ----------------- .../src/routes/(main)/account/+page.svelte | 110 ++++---- .../(main)/account/verify-email/+page.svelte | 240 ------------------ .../src/routes/api/debug/users/+server.ts | 95 ++++--- apps/main/src/routes/auth/login/+page.svelte | 8 +- .../src/routes/auth/magic-link/+page.svelte | 11 - packages/logic/domains/auth/config.base.ts | 110 +------- packages/logic/domains/auth/controller.ts | 88 ------- packages/logic/domains/auth/errors.ts | 27 -- 14 files changed, 245 insertions(+), 1128 deletions(-) delete mode 100644 apps/main/src/lib/components/molecules/google-oauth-button.svelte delete mode 100644 apps/main/src/lib/domains/security/magic-link-verification.svelte delete mode 100644 apps/main/src/routes/(main)/account/verify-email/+page.svelte delete mode 100644 apps/main/src/routes/auth/magic-link/+page.svelte diff --git a/apps/main/src/lib/auth.client.ts b/apps/main/src/lib/auth.client.ts index ddfc649..543a4bc 100644 --- a/apps/main/src/lib/auth.client.ts +++ b/apps/main/src/lib/auth.client.ts @@ -2,7 +2,6 @@ import { adminClient, customSessionClient, inferAdditionalFields, - magicLinkClient, multiSessionClient, usernameClient, } from "better-auth/client/plugins"; @@ -13,7 +12,6 @@ export const authClient = createAuthClient({ plugins: [ usernameClient(), adminClient(), - magicLinkClient(), multiSessionClient(), customSessionClient(), inferAdditionalFields(), diff --git a/apps/main/src/lib/components/molecules/google-oauth-button.svelte b/apps/main/src/lib/components/molecules/google-oauth-button.svelte deleted file mode 100644 index 18aa308..0000000 --- a/apps/main/src/lib/components/molecules/google-oauth-button.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/apps/main/src/lib/domains/account/account.vm.svelte.ts b/apps/main/src/lib/domains/account/account.vm.svelte.ts index 4c0fa77..39bd03d 100644 --- a/apps/main/src/lib/domains/account/account.vm.svelte.ts +++ b/apps/main/src/lib/domains/account/account.vm.svelte.ts @@ -1,12 +1,14 @@ import type { User } from "@pkg/logic/domains/user/data"; +import { apiClient, user as userStore } from "$lib/global.stores"; import { authClient } from "$lib/auth.client"; import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; import { ResultAsync, errAsync, okAsync } from "neverthrow"; import type { Err } from "@pkg/result"; class AccountViewModel { loading = $state(false); - emailLoading = $state(false); + passwordLoading = $state(false); errorMessage = $state(null); async updateProfilePicture(imagePath: string): Promise { @@ -104,44 +106,71 @@ class AccountViewModel { return user; } - async changeEmail(email: string): Promise { - this.emailLoading = true; + async changePassword(password: string): Promise { + this.passwordLoading = true; this.errorMessage = null; + const currentUser = get(userStore); + if (!currentUser?.id) { + this.passwordLoading = false; + this.errorMessage = "User not found"; + toast.error(this.errorMessage); + return false; + } + const client = get(apiClient); + if (!client) { + this.passwordLoading = false; + this.errorMessage = "API client not initialized"; + toast.error(this.errorMessage); + return false; + } + const result = await ResultAsync.fromPromise( - authClient.changeEmail({ newEmail: email }), + client.users["rotate-password"].$put({ + json: { userId: currentUser.id, password }, + }), (error): Err => ({ code: "NETWORK_ERROR", - message: "Failed to change email", + message: "Failed to change password", description: "Network request failed", detail: error instanceof Error ? error.message : String(error), }), ) .andThen((response) => { - if (response.error) { + if (!response.ok) { return errAsync({ code: "API_ERROR", - message: - response.error.message ?? "Failed to change email", - description: - response.error.statusText ?? - "Please try again later", - detail: response.error.statusText ?? "Unknown error", + message: "Failed to change password", + description: `Response failed with status ${response.status}`, + detail: `HTTP ${response.status}`, }); } - return okAsync(response.data); + return ResultAsync.fromPromise( + response.json(), + (error): Err => ({ + code: "PARSING_ERROR", + message: "Failed to parse response", + description: "Invalid response format", + detail: + error instanceof Error + ? error.message + : String(error), + }), + ).andThen((apiResult: any) => { + if (apiResult.error) { + return errAsync(apiResult.error); + } + return okAsync(apiResult.data); + }); }); const success = result.match( () => { - toast.success("Verification email sent!", { - description: - "Please check your inbox and verify your new email address.", - }); + toast.success("Password updated successfully"); return true; }, (error) => { - this.errorMessage = error.message ?? "Failed to change email"; + this.errorMessage = error.message ?? "Failed to change password"; toast.error(this.errorMessage, { description: error.description, }); @@ -149,7 +178,7 @@ class AccountViewModel { }, ); - this.emailLoading = false; + this.passwordLoading = false; return success; } } diff --git a/apps/main/src/lib/domains/security/auth.vm.svelte.ts b/apps/main/src/lib/domains/security/auth.vm.svelte.ts index 1ef7e88..7c9d622 100644 --- a/apps/main/src/lib/domains/security/auth.vm.svelte.ts +++ b/apps/main/src/lib/domains/security/auth.vm.svelte.ts @@ -1,54 +1,32 @@ -import { apiClient } from "$lib/global.stores"; import { authClient } from "$lib/auth.client"; import { toast } from "svelte-sonner"; -import { get } from "svelte/store"; -import { page } from "$app/state"; import { ResultAsync, errAsync, okAsync } from "neverthrow"; import type { Err } from "@pkg/result"; class AuthViewModel { loggingIn = $state(false); - loginError = $state(null); - async handleGoogleOAuthLogin() { - const result = await ResultAsync.fromPromise( - authClient.signIn.social({ provider: "google" }), - (error): Err => ({ - code: "NETWORK_ERROR", - message: "Failed to initiate Google OAuth login", - description: "Network request failed", - detail: error instanceof Error ? error.message : String(error), - }), - ); + async loginWithCredentials(data: FormData): Promise { + const username = data.get("username")?.toString().trim(); + const password = data.get("password")?.toString(); - result.match( - () => { - // OAuth flow will redirect, no action needed - }, - (error) => { - toast.error("Failed to initiate Google login", { - description: error.description, - }); - }, - ); - } + if (!username || username.length < 3) { + toast.error("Please enter a valid username"); + return false; + } - async loginWithEmail(data: FormData): Promise { - const email = data.get("email")?.toString(); - - if (!email || email.length < 5) { - toast.error("Please enter a valid email"); + if (!password || password.length < 6) { + toast.error("Please enter a valid password"); return false; } this.loggingIn = true; - this.loginError = null; const result = await ResultAsync.fromPromise( - authClient.signIn.magicLink({ email }), + authClient.signIn.username({ username, password }), (error): Err => ({ code: "NETWORK_ERROR", - message: "Failed to send magic link", + message: "Failed to login", description: "Network request failed", detail: error instanceof Error ? error.message : String(error), }), @@ -57,10 +35,10 @@ class AuthViewModel { if (response.error) { return errAsync({ code: "API_ERROR", - message: response.error.message ?? "Something went wrong", + message: response.error.message ?? "Invalid credentials", description: response.error.statusText ?? - "Please try another login method or try again later", + "Please check your username and password", detail: response.error.statusText ?? "Unknown error", }); } @@ -69,26 +47,18 @@ class AuthViewModel { const success = result.match( () => { - this.loginError = null; - toast.success("Login request sent", { - description: "Check your email for a magic link to log in", + toast.success("Login successful", { + description: "Redirecting...", }); + setTimeout(() => { + window.location.href = "/"; + }, 500); return true; }, (error) => { - // Set error message for UI display - const errorMessage = error.message ?? "Something went wrong"; - this.loginError = errorMessage; - - // Don't show toast for known error cases that we handle in the UI - if ( - !errorMessage.includes("not valid to login") && - !errorMessage.includes("not allowed") - ) { - toast.error(errorMessage, { - description: error.description, - }); - } + toast.error(error.message ?? "Invalid credentials", { + description: error.description, + }); return false; }, ); @@ -96,138 +66,6 @@ class AuthViewModel { this.loggingIn = false; return success; } - - clearLoginError() { - this.loginError = null; - } - - async verifyMagicLink() { - const token = page.url.searchParams.get("token"); - if (!token) { - throw new Error("No token provided"); - } - - const result = await ResultAsync.fromPromise( - authClient.magicLink.verify({ query: { token } }), - (error): Err => ({ - code: "NETWORK_ERROR", - message: "Failed to verify magic link", - description: "Network request failed", - detail: error instanceof Error ? error.message : String(error), - }), - ) - .andThen((response) => { - if (response.error) { - return errAsync({ - code: "API_ERROR", - message: "Could not verify magic link", - description: response.error.message ?? "Unknown error", - detail: response.error.statusText ?? "Unknown error", - }); - } - if (!response.data?.user?.id) { - return errAsync({ - code: "API_ERROR", - message: "Invalid verification result", - description: "User ID not found in verification response", - detail: "Missing user.id in response", - }); - } - return okAsync(response.data); - }) - .andThen((verificationResult) => { - return ResultAsync.fromPromise( - get(apiClient).users["ensure-account-exists"].$put({ - json: { userId: verificationResult.user.id }, - }), - (error): Err => ({ - code: "NETWORK_ERROR", - message: "Failed to ensure account exists", - description: "Network request failed", - detail: - error instanceof Error - ? error.message - : String(error), - }), - ) - .andThen((response) => { - if (!response.ok) { - return errAsync({ - code: "API_ERROR", - message: "Failed to ensure account exists", - description: `Response failed with status ${response.status}`, - detail: `HTTP ${response.status}`, - }); - } - return ResultAsync.fromPromise( - response.json(), - (error): Err => ({ - code: "PARSING_ERROR", - message: "Failed to parse response", - description: "Invalid response format", - detail: - error instanceof Error - ? error.message - : String(error), - }), - ); - }) - .andThen((apiResult: any) => { - if (apiResult.error) { - return errAsync(apiResult.error); - } - return okAsync(apiResult.data); - }); - }); - - result.match( - () => { - // Success - account ensured - }, - (error: Err) => { - throw new Error(error.message ?? "Failed to verify magic link"); - }, - ); - } - - async verifyEmailChange() { - const token = page.url.searchParams.get("token"); - if (!token) { - throw new Error("No verification token provided"); - } - - const result = await ResultAsync.fromPromise( - authClient.verifyEmail({ - query: { token }, - }), - (error): Err => ({ - code: "NETWORK_ERROR", - message: "Failed to verify email change", - description: "Network request failed", - detail: error instanceof Error ? error.message : String(error), - }), - ) - .andThen((response) => { - if (response.error) { - return errAsync({ - code: "API_ERROR", - message: "Could not verify email change", - description: response.error.message ?? "Unknown error", - detail: response.error.statusText ?? "Unknown error", - }); - } - return okAsync(response.data); - }); - - result.match( - () => { - // Success - email verified - }, - (error: Err) => { - throw new Error(error.message ?? "Could not verify email change"); - }, - ); - } } export const authVM = new AuthViewModel(); diff --git a/apps/main/src/lib/domains/security/email-login-form.svelte b/apps/main/src/lib/domains/security/email-login-form.svelte index ab67c11..cdc24d4 100644 --- a/apps/main/src/lib/domains/security/email-login-form.svelte +++ b/apps/main/src/lib/domains/security/email-login-form.svelte @@ -3,152 +3,76 @@ import Button from "$lib/components/ui/button/button.svelte"; import Input from "$lib/components/ui/input/input.svelte"; import { authVM } from "$lib/domains/security/auth.vm.svelte"; - import { AlertCircle, AtSign, CheckCircle, Loader2 } from "@lucide/svelte"; + import { AtSign, KeyRound, Loader2 } from "@lucide/svelte"; - let status = $state<"idle" | "sending" | "sent" | "failed">("idle"); - let submittedEmail = $state(""); - let email = $state(""); - let emailFocused = $state(false); - let isEmailValid = $state(false); + let username = $state(""); + let password = $state(""); - // Validate email as user types - $effect(() => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - isEmailValid = emailRegex.test(email) && email.length >= 5; - }); - - // Clear API error when user starts typing - $effect(() => { - if (email && authVM.loginError) { - authVM.clearLoginError(); - } - }); - - function onInput(e: Event & { currentTarget: HTMLInputElement }) { - email = e.currentTarget.value; - } + const canSubmit = $derived( + username.trim().length >= 3 && password.length >= 6 && !authVM.loggingIn, + ); async function onSubmit( e: SubmitEvent & { currentTarget: HTMLFormElement }, ) { e.preventDefault(); - if (!isEmailValid || authVM.loggingIn) return; - - const formData = new FormData(e.currentTarget); - submittedEmail = (formData.get("email")?.toString() || "").trim(); - - status = "sending"; - try { - const ok = await authVM.loginWithEmail(formData); - status = ok ? "sent" : "failed"; - } catch { - status = "failed"; - } - } - - function resetFormState() { - status = "idle"; - submittedEmail = ""; - authVM.clearLoginError(); + if (!canSubmit) return; + await authVM.loginWithCredentials(new FormData(e.currentTarget)); } -
- +
-
- (emailFocused = true)} - onblur={() => (emailFocused = false)} - class="h-12 pr-12" - aria-invalid={!isEmailValid && email.length > 0} - /> - -
- {#if authVM.loggingIn} - - {:else if isEmailValid && email.length > 0} - - {:else if email.length > 0} - - - {/if} -
-
- - {#if emailFocused && email.length > 0 && !isEmailValid} -

- Please enter a valid email address. -

- {/if} + +
+ +
+ +
- - - -
- {#if status === "sent"} -
-
- -

- We’ve sent a magic link to {submittedEmail}. Check your inbox. -

-
-
- {:else if status === "failed" && authVM.loginError} -
-
- -

{authVM.loginError}

-
- -
- {/if} -
diff --git a/apps/main/src/lib/domains/security/magic-link-verification.svelte b/apps/main/src/lib/domains/security/magic-link-verification.svelte deleted file mode 100644 index 5f58a66..0000000 --- a/apps/main/src/lib/domains/security/magic-link-verification.svelte +++ /dev/null @@ -1,228 +0,0 @@ - - - - -
- {#if verificationState === "loading"} - -
-
-
- -
-
- {/if} - - - {#if verificationState === "loading"} -
- - Verifying Your Login - - -
- - Please wait while we verify your magic - link... -
- - -
-
-
- - -
-
-
- -
- Secure encrypted connection -
-
-
- -
- Verifying your identity -
-
-
- -
- Processing authentication -
-
-
- {:else if verificationState === "success"} -
-
-
-
- -
-
- - - Login Successful! - - -

- You've been successfully authenticated. - Redirecting you now... -

- -
- Redirecting - -
-
- {:else if verificationState === "error"} -
-
-
-
- -
-
- - - Verification Failed - - -

- {errorMessage || - "We couldn't verify your magic link. It may have expired or been used already."} -

- -
- - -

- Need help? Contact our support team if this - problem persists. -

-
-
- {/if} - - -
-

- Magic links are secure, one-time use authentication - tokens that expire after 10 minutes. -

-
-
-
-
diff --git a/apps/main/src/routes/(main)/account/+page.svelte b/apps/main/src/routes/(main)/account/+page.svelte index 3031c4d..698c87d 100644 --- a/apps/main/src/routes/(main)/account/+page.svelte +++ b/apps/main/src/routes/(main)/account/+page.svelte @@ -10,7 +10,7 @@ import { accountVM } from "$lib/domains/account/account.vm.svelte"; import { breadcrumbs } from "$lib/global.stores"; import AtSign from "@lucide/svelte/icons/at-sign"; - import Mail from "@lucide/svelte/icons/mail"; + import KeyRound from "@lucide/svelte/icons/key-round"; import Save from "@lucide/svelte/icons/save"; import Upload from "@lucide/svelte/icons/upload"; import User from "@lucide/svelte/icons/user"; @@ -22,14 +22,15 @@ const user = $state(data.user!); - // Separate form state for profile and email + // Separate form state for profile and password let profileData = $state({ name: user.name ?? "", username: user.username ?? "", }); - let emailData = $state({ - email: user.email, + let passwordData = $state({ + password: "", + confirmPassword: "", }); // Handle profile form submission (name, username) @@ -38,11 +39,20 @@ await accountVM.updateProfile(profileData); } - // Handle email form submission - async function handleEmailSubmit(e: SubmitEvent) { + // Handle password form submission + async function handlePasswordSubmit(e: SubmitEvent) { e.preventDefault(); - if (user.email !== emailData.email) { - await accountVM.changeEmail(emailData.email); + if ( + passwordData.password.length >= 6 && + passwordData.password === passwordData.confirmPassword + ) { + const didChange = await accountVM.changePassword( + passwordData.password, + ); + if (didChange) { + passwordData.password = ""; + passwordData.confirmPassword = ""; + } } } @@ -171,67 +181,72 @@ - + - Email Settings + Password Settings - Manage your email address and verification status. + Update your account password. -
- +
-
+ +
+ + -
- - {user.emailVerified - ? "Email verified" - : "Email not verified"} - - {#if !user.emailVerified} - - {/if} -
@@ -239,8 +254,7 @@

- Changing your email will require verification of the new - address. + Choose a strong password with at least 6 characters.

diff --git a/apps/main/src/routes/(main)/account/verify-email/+page.svelte b/apps/main/src/routes/(main)/account/verify-email/+page.svelte deleted file mode 100644 index f128cee..0000000 --- a/apps/main/src/routes/(main)/account/verify-email/+page.svelte +++ /dev/null @@ -1,240 +0,0 @@ - - -
-
- - -
- {#if verificationState === "loading"} - -
-
-
- -
-
- {/if} - - - {#if verificationState === "loading"} -
- - Verifying Your Email - - -
- - Please wait while we verify your email - change... -
- - -
-
-
- - -
-
-
- -
- Secure encrypted connection -
-
-
- -
- Validating email ownership -
-
-
- -
- Updating account settings -
-
-
- {:else if verificationState === "success"} -
-
-
-
- -
-
- - - Email Verified Successfully! - - -

- Your email address has been updated and - verified. Redirecting you to your account... -

- -
- Redirecting - -
-
- {:else if verificationState === "error"} -
-
-
-
- -
-
- - - Email Verification Failed - - -

- {errorMessage || - "We couldn't verify your email change. The verification link may have expired or been used already."} -

- -
- - -

- You can try changing your email again from - your account settings. -

-
-
- {/if} - - -
-

- Email verification links are secure, one-time use - tokens that expire after 10 minutes. -

-
-
-
-
-
-
diff --git a/apps/main/src/routes/api/debug/users/+server.ts b/apps/main/src/routes/api/debug/users/+server.ts index 2ce01b0..daf04c8 100644 --- a/apps/main/src/routes/api/debug/users/+server.ts +++ b/apps/main/src/routes/api/debug/users/+server.ts @@ -1,9 +1,10 @@ import { UserRoleMap } from "@pkg/logic/domains/user/data"; -import { user } from "@pkg/db/schema/better.auth.schema"; +import { auth } from "@pkg/logic/domains/auth/config.base"; +import { account, user } from "@pkg/db/schema/better.auth.schema"; import type { RequestHandler } from "./$types"; import { settings } from "@pkg/settings"; import { logger } from "@pkg/logger"; -import { db, eq } from "@pkg/db"; +import { db, eq, or } from "@pkg/db"; import { nanoid } from "nanoid"; import * as v from "valibot"; @@ -48,45 +49,81 @@ export const POST: RequestHandler = async ({ request }) => { const data = await request.json(); const _schema = v.object({ username: v.string(), - email: v.string(), - usertype: v.enum(UserRoleMap), + password: v.pipe(v.string(), v.minLength(6)), + email: v.optional(v.string()), + usertype: v.optional(v.enum(UserRoleMap)), }); const res = v.safeParse(_schema, data); if (!res.success) { return new Response("Invalid data", { status: 400 }); } + const resData = res.output; + const email = resData.email ?? `${resData.username}@debug.local`; + const usertype = resData.usertype ?? UserRoleMap.admin; - if ( - !!( - await db.query.user - .findFirst({ - where: eq(user.role, UserRoleMap.user), - columns: { id: true }, - }) - .execute() - )?.id - ) { - return new Response("Admin already exists", { status: 400 }); + const existingUser = await db.query.user + .findFirst({ + where: or(eq(user.username, resData.username), eq(user.email, email)), + columns: { id: true }, + }) + .execute(); + + if (existingUser?.id) { + return new Response("User already exists", { status: 409 }); } - const resData = res.output; - logger.info( - `Creating ${resData.username} | ${resData.email} (${resData.usertype})`, + `Creating debug user ${resData.username} | ${email} (${usertype})`, ); - const out = await db.insert(user).values({ - id: nanoid(), - username: resData.username, - email: resData.email, - emailVerified: false, - name: resData.username, - role: resData.usertype, - createdAt: new Date(), - updatedAt: new Date(), + const userId = nanoid(); + const accountId = nanoid(); + const hashedPassword = await auth.$context.then((ctx) => + ctx.password.hash(resData.password), + ); + + await db.transaction(async (tx) => { + await tx + .insert(user) + .values({ + id: userId, + username: resData.username, + email, + emailVerified: true, + name: resData.username, + role: usertype, + createdAt: new Date(), + updatedAt: new Date(), + }) + .execute(); + + await tx + .insert(account) + .values({ + id: accountId, + accountId: userId, + providerId: "credential", + userId, + password: hashedPassword, + createdAt: new Date(), + updatedAt: new Date(), + }) + .execute(); }); - logger.debug(out); + logger.info("Debug user created with credential account", { + userId, + username: resData.username, + email, + }); - return new Response(JSON.stringify({ ...out }), { status: 200 }); + return new Response( + JSON.stringify({ + id: userId, + username: resData.username, + email, + role: usertype, + }), + { status: 200 }, + ); }; diff --git a/apps/main/src/routes/auth/login/+page.svelte b/apps/main/src/routes/auth/login/+page.svelte index e4c5e5c..7cffc76 100644 --- a/apps/main/src/routes/auth/login/+page.svelte +++ b/apps/main/src/routes/auth/login/+page.svelte @@ -46,7 +46,7 @@ - Sign in to your account to continue + Sign in with your username and password @@ -54,12 +54,6 @@ - -
-
-

OR

-
-
diff --git a/apps/main/src/routes/auth/magic-link/+page.svelte b/apps/main/src/routes/auth/magic-link/+page.svelte deleted file mode 100644 index 57a39b0..0000000 --- a/apps/main/src/routes/auth/magic-link/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -
-
- -
-
diff --git a/packages/logic/domains/auth/config.base.ts b/packages/logic/domains/auth/config.base.ts index 6a9ca6d..91ea862 100644 --- a/packages/logic/domains/auth/config.base.ts +++ b/packages/logic/domains/auth/config.base.ts @@ -1,52 +1,19 @@ import { admin, customSession, - magicLink, multiSession, username, } from "better-auth/plugins"; -import { getUserController, UserController } from "../user/controller"; -import { AuthController, getAuthController } from "./controller"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { FlowExecCtx } from "@/core/flow.execution.context"; import { UserRoleMap } from "@domains/user/data"; import { getRedisInstance } from "@pkg/keystore"; -import { APIError } from "better-auth/api"; import { settings } from "@core/settings"; import { betterAuth } from "better-auth"; import { logger } from "@pkg/logger"; import { db, schema } from "@pkg/db"; -import { nanoid } from "nanoid"; -// Constants -const EMAIL_EXPIRES_IN_MINS = 10; -const EMAIL_EXPIRES_IN_SECONDS = 60 * EMAIL_EXPIRES_IN_MINS; const COOKIE_CACHE_MAX_AGE = 60 * 5; - -// Helper to create flow context for better-auth callbacks -function createAuthFlowContext(contextLabel: string): FlowExecCtx { - return { - flowId: `auth:${contextLabel}:${nanoid(10)}`, - }; -} - -// Singleton controller instances -let authControllerInstance: AuthController | null = null; -let userControllerInstance: UserController | null = null; - -function getAuthControllerInstance(): AuthController { - if (!authControllerInstance) { - authControllerInstance = getAuthController(); - } - return authControllerInstance; -} - -function getUserControllerInstance(): UserController { - if (!userControllerInstance) { - userControllerInstance = getUserController(); - } - return userControllerInstance; -} +const USERNAME_REGEX = /^[a-zA-Z0-9_]+$/; export const auth = betterAuth({ trustedOrigins: ["http://localhost:5173", settings.betterAuthUrl], @@ -66,48 +33,7 @@ export const auth = betterAuth({ minUsernameLength: 5, maxUsernameLength: 20, usernameValidator: async (username) => { - const fctx = createAuthFlowContext("username-check"); - const uc = getUserControllerInstance(); - - const result = await uc - .isUsernameAvailable(fctx, username) - .match( - (isAvailable) => ({ success: true, isAvailable }), - (error) => { - logger.error( - `[${fctx.flowId}] Failed to check username availability`, - error, - ); - return { success: false, isAvailable: false }; - }, - ); - - return result.isAvailable; - }, - }), - magicLink({ - expiresIn: EMAIL_EXPIRES_IN_SECONDS, - rateLimit: { window: 60, max: 4 }, - sendMagicLink: async ({ email, token, url }, request) => { - const fctx = createAuthFlowContext("magic-link"); - const ac = getAuthControllerInstance(); - - const result = await ac - .sendMagicLink(fctx, email, token, url) - .match( - () => ({ success: true, error: undefined }), - (error) => ({ success: false, error }), - ); - - if (!result.success || result?.error) { - logger.error( - `[${fctx.flowId}] Failed to send magic link`, - result.error, - ); - throw new APIError("INTERNAL_SERVER_ERROR", { - message: result.error?.message, - }); - } + return USERNAME_REGEX.test(username); }, }), admin({ @@ -157,38 +83,6 @@ export const auth = betterAuth({ }, }, user: { - changeEmail: { - enabled: true, - sendChangeEmailVerification: async ( - { user, newEmail, url, token }, - request, - ) => { - const fctx = createAuthFlowContext("email-change"); - const ac = getAuthControllerInstance(); - - const result = await ac - .sendEmailChangeVerificationEmail( - fctx, - newEmail, - token, - url, - ) - .match( - () => ({ success: true, error: undefined }), - (error) => ({ success: false, error }), - ); - - if (!result.success || result?.error) { - logger.error( - `[${fctx.flowId}] Failed to send email change verification`, - result.error, - ); - throw new APIError("INTERNAL_SERVER_ERROR", { - message: result.error?.message, - }); - } - }, - }, modelName: "user", additionalFields: { onboardingDone: { diff --git a/packages/logic/domains/auth/controller.ts b/packages/logic/domains/auth/controller.ts index 6467b99..3b5be9d 100644 --- a/packages/logic/domains/auth/controller.ts +++ b/packages/logic/domains/auth/controller.ts @@ -2,61 +2,14 @@ import { AuthContext, MiddlewareContext, MiddlewareOptions } from "better-auth"; import { AccountRepository } from "../user/account.repository"; import { FlowExecCtx } from "@/core/flow.execution.context"; import { ResultAsync } from "neverthrow"; -import type { Err } from "@pkg/result"; import { authErrors } from "./errors"; import { logger } from "@pkg/logger"; import { nanoid } from "nanoid"; import { db } from "@pkg/db"; export class AuthController { - private readonly mins = 10; - constructor(private accountRepo: AccountRepository) {} - sendEmailChangeVerificationEmail( - fctx: FlowExecCtx, - newEmail: string, - token: string, - url: string, - ): ResultAsync { - logger.info("Sending email change verification link", { - ...fctx, - newEmail, - }); - logger.debug("Original URL", { ...fctx, url }); - - const transformedUrl = url - .replace("/api/auth/verify-email", "/account/verify-email") - .replace("/api/", "/"); - - logger.debug("Transformed URL", { ...fctx, transformedUrl }); - - // Simulate email sending with 90/10 success/failure - const success = Math.random() > 0.1; - - if (!success) { - logger.error("Failed to send email change verification link", { - ...fctx, - error: "Simulated email service failure", - }); - return ResultAsync.fromPromise( - Promise.reject( - authErrors.emailChangeVerificationFailed( - fctx, - "Simulated email service failure", - ), - ), - (error) => error as Err, - ); - } - - logger.info("Email change verification sent successfully", { - ...fctx, - newEmail, - }); - return ResultAsync.fromSafePromise(Promise.resolve(undefined)); - } - swapAccountPasswordForTwoFactor( fctx: FlowExecCtx, ctx: MiddlewareContext< @@ -100,47 +53,6 @@ export class AuthController { }; }); } - - sendMagicLink( - fctx: FlowExecCtx, - email: string, - token: string, - url: string, - ): ResultAsync { - logger.info("Sending magic link", { ...fctx, email }); - logger.debug("Original URL", { ...fctx, url }); - - const transformedUrl = url - .replace("/api/auth/magic-link/verify", "/auth/magic-link") - .replace("/api/", "/"); - - logger.debug("Transformed URL", { ...fctx, transformedUrl }); - - // Simulate email sending with 90/10 success/failure - const success = true; - - if (!success) { - logger.error("Failed to send magic link email", { - ...fctx, - error: "Simulated email service failure", - }); - return ResultAsync.fromPromise( - Promise.reject( - authErrors.magicLinkEmailFailed( - fctx, - "Simulated email service failure", - ), - ), - (error) => error as Err, - ); - } - - logger.info("Magic link email sent successfully (NOT REALLY)", { - ...fctx, - email, - }); - return ResultAsync.fromSafePromise(Promise.resolve(undefined)); - } } export function getAuthController(): AuthController { diff --git a/packages/logic/domains/auth/errors.ts b/packages/logic/domains/auth/errors.ts index 33c3bb1..39104fa 100644 --- a/packages/logic/domains/auth/errors.ts +++ b/packages/logic/domains/auth/errors.ts @@ -3,33 +3,6 @@ import { getError } from "@pkg/logger"; import { ERROR_CODES, type Err } from "@pkg/result"; export const authErrors = { - emailSendFailed: (fctx: FlowExecCtx, detail: string): Err => - getError({ - flowId: fctx.flowId, - code: ERROR_CODES.INTERNAL_SERVER_ERROR, - message: "Failed to send email", - description: "An error occurred while sending the email", - detail, - }), - - magicLinkEmailFailed: (fctx: FlowExecCtx, detail: string): Err => - getError({ - flowId: fctx.flowId, - code: ERROR_CODES.INTERNAL_SERVER_ERROR, - message: "Failed to send magic link email", - description: "An error occurred while sending the magic link", - detail, - }), - - emailChangeVerificationFailed: (fctx: FlowExecCtx, detail: string): Err => - getError({ - flowId: fctx.flowId, - code: ERROR_CODES.INTERNAL_SERVER_ERROR, - message: "Failed to send email change verification link", - description: "An error occurred while sending the verification email", - detail, - }), - passwordRotationFailed: (fctx: FlowExecCtx, detail: string): Err => getError({ flowId: fctx.flowId,