From ee091c4b1093360500bbefb572b22e41ddb18ad8 Mon Sep 17 00:00:00 2001 From: Wuesteon Date: Mon, 8 Dec 2025 17:04:35 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20migrate=20mana?= =?UTF-8?q?core-web=20from=20Supabase=20to=20mana-core-auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add password reset functionality to mana-core-auth using Better Auth - Add forgot-password and reset-password endpoints with DTOs - Update shared-auth package with resetPassword method and endpoint - Update manacore-web auth store with resetPassword method - Refactor reset-password pages to use mana-core-auth instead of Supabase - Remove Supabase dependencies from manacore-web package.json - Remove Supabase server code (hooks.server.ts, supabase.ts, API routes) - Update Dockerfile to remove shared-supabase dependency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/manacore/apps/web/Dockerfile | 1 - apps/manacore/apps/web/package.json | 3 - apps/manacore/apps/web/src/app.d.ts | 23 +- apps/manacore/apps/web/src/hooks.server.ts | 24 +- .../apps/web/src/lib/server/supabase.ts | 43 ---- .../apps/web/src/lib/stores/auth.svelte.ts | 23 ++ .../(auth)/forgot-password/+page.server.ts | 39 ---- .../(auth)/reset-password/+page.server.ts | 44 ---- .../routes/(auth)/reset-password/+page.svelte | 178 ++++++--------- .../apps/web/src/routes/+layout.server.ts | 9 +- apps/manacore/apps/web/src/routes/+layout.ts | 37 +-- .../routes/api/auth/set-session/+server.ts | 36 --- .../routes/api/auth/verify-token/+server.ts | 33 --- .../auth/reset-password/+page.server.ts | 44 ---- .../routes/auth/reset-password/+page.svelte | 211 +----------------- packages/shared-auth/src/core/authService.ts | 36 +++ packages/shared-auth/src/types/index.ts | 1 + pnpm-lock.yaml | 20 -- .../src/auth/auth.controller.ts | 35 +++ .../src/auth/better-auth.config.ts | 26 ++- .../src/auth/dto/forgot-password.dto.ts | 22 ++ .../src/auth/dto/reset-password.dto.ts | 22 ++ .../src/auth/services/better-auth.service.ts | 86 +++++++ 23 files changed, 357 insertions(+), 639 deletions(-) delete mode 100644 apps/manacore/apps/web/src/lib/server/supabase.ts delete mode 100644 apps/manacore/apps/web/src/routes/(auth)/forgot-password/+page.server.ts delete mode 100644 apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.server.ts delete mode 100644 apps/manacore/apps/web/src/routes/api/auth/set-session/+server.ts delete mode 100644 apps/manacore/apps/web/src/routes/api/auth/verify-token/+server.ts delete mode 100644 apps/manacore/apps/web/src/routes/auth/reset-password/+page.server.ts create mode 100644 services/mana-core-auth/src/auth/dto/forgot-password.dto.ts create mode 100644 services/mana-core-auth/src/auth/dto/reset-password.dto.ts diff --git a/apps/manacore/apps/web/Dockerfile b/apps/manacore/apps/web/Dockerfile index 91418444c..187511bc5 100644 --- a/apps/manacore/apps/web/Dockerfile +++ b/apps/manacore/apps/web/Dockerfile @@ -35,7 +35,6 @@ COPY packages/shared-theme-ui ./packages/shared-theme-ui COPY packages/shared-subscription-types ./packages/shared-subscription-types COPY packages/shared-subscription-ui ./packages/shared-subscription-ui COPY packages/shared-profile-ui ./packages/shared-profile-ui -COPY packages/shared-supabase ./packages/shared-supabase COPY packages/shared-types ./packages/shared-types COPY packages/shared-ui ./packages/shared-ui COPY packages/shared-utils ./packages/shared-utils diff --git a/apps/manacore/apps/web/package.json b/apps/manacore/apps/web/package.json index 8c019c6dc..81222619d 100644 --- a/apps/manacore/apps/web/package.json +++ b/apps/manacore/apps/web/package.json @@ -49,15 +49,12 @@ "@manacore/shared-profile-ui": "workspace:*", "@manacore/shared-subscription-types": "workspace:*", "@manacore/shared-subscription-ui": "workspace:*", - "@manacore/shared-supabase": "workspace:*", "@manacore/shared-tailwind": "workspace:*", "@manacore/shared-theme": "workspace:*", "@manacore/shared-theme-ui": "workspace:*", "@manacore/shared-types": "workspace:*", "@manacore/shared-ui": "workspace:*", "@manacore/shared-utils": "workspace:*", - "@supabase/ssr": "^0.5.2", - "@supabase/supabase-js": "^2.81.1", "svelte-dnd-action": "^0.9.68", "svelte-i18n": "^4.0.0" }, diff --git a/apps/manacore/apps/web/src/app.d.ts b/apps/manacore/apps/web/src/app.d.ts index 9272c6861..1476def65 100644 --- a/apps/manacore/apps/web/src/app.d.ts +++ b/apps/manacore/apps/web/src/app.d.ts @@ -1,18 +1,15 @@ -import type { Session, SupabaseClient, User } from '@supabase/supabase-js'; - +/** + * App type declarations for ManaCore web app + * + * Authentication is handled entirely by Mana Core Auth (@manacore/shared-auth). + * No Supabase is needed - all data comes from mana-core-auth APIs. + */ declare global { namespace App { - interface Locals { - supabase: SupabaseClient; - safeGetSession: () => Promise<{ session: Session | null; user: User | null }>; - session: Session | null; - user: User | null; - } - interface PageData { - // Auth is handled by Mana Core Auth (@manacore/shared-auth), not Supabase - // Supabase is used for database operations only - supabase?: SupabaseClient; - } + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface Locals {} + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface PageData {} // interface Error {} // interface Platform {} } diff --git a/apps/manacore/apps/web/src/hooks.server.ts b/apps/manacore/apps/web/src/hooks.server.ts index 1ae4f5d6a..446ce160c 100644 --- a/apps/manacore/apps/web/src/hooks.server.ts +++ b/apps/manacore/apps/web/src/hooks.server.ts @@ -1,29 +1,11 @@ -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; -import { createServerClient } from '@supabase/ssr'; import type { Handle } from '@sveltejs/kit'; /** * Server hooks for ManaCore web app * - * Note: Authentication is handled client-side via Mana Core Auth. - * Supabase is only used for database operations (not auth). + * Authentication is handled entirely by Mana Core Auth (@manacore/shared-auth). + * No Supabase is needed - all data comes from mana-core-auth APIs. */ export const handle: Handle = async ({ event, resolve }) => { - // Create Supabase client for database operations only - event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { - cookies: { - getAll: () => event.cookies.getAll(), - setAll: (cookiesToSet) => { - cookiesToSet.forEach(({ name, value, options }) => { - event.cookies.set(name, value, { ...options, path: '/' }); - }); - }, - }, - }) as any; - - return resolve(event, { - filterSerializedResponseHeaders(name) { - return name === 'content-range' || name === 'x-supabase-api-version'; - }, - }); + return resolve(event); }; diff --git a/apps/manacore/apps/web/src/lib/server/supabase.ts b/apps/manacore/apps/web/src/lib/server/supabase.ts deleted file mode 100644 index 31f4b14a4..000000000 --- a/apps/manacore/apps/web/src/lib/server/supabase.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { RequestEvent } from '@sveltejs/kit'; - -export async function getUser(event: RequestEvent) { - const { - data: { user }, - error, - } = await event.locals.supabase.auth.getUser(); - - if (error) { - console.error('Error fetching user:', error); - return null; - } - - return user; -} - -export async function getSession(event: RequestEvent) { - const { - data: { session }, - error, - } = await event.locals.supabase.auth.getSession(); - - if (error) { - console.error('Error fetching session:', error); - return null; - } - - return session; -} - -export async function requireAuth(event: RequestEvent) { - const session = await getSession(event); - - if (!session) { - throw new Error('Unauthorized'); - } - - return session; -} - -export function getSupabaseServerClient(event: RequestEvent) { - return event.locals.supabase; -} diff --git a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts index 12cb7b306..5b6edc77a 100644 --- a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts @@ -183,6 +183,29 @@ export const authStore = { } }, + /** + * Reset password with token + */ + async resetPassword(token: string, newPassword: string) { + const authService = getAuthService(); + if (!authService) { + return { success: false, error: 'Auth not available on server' }; + } + + try { + const result = await authService.resetPassword(token, newPassword); + + if (!result.success) { + return { success: false, error: result.error || 'Password reset failed' }; + } + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return { success: false, error: errorMessage }; + } + }, + /** * Get access token for API calls */ diff --git a/apps/manacore/apps/web/src/routes/(auth)/forgot-password/+page.server.ts b/apps/manacore/apps/web/src/routes/(auth)/forgot-password/+page.server.ts deleted file mode 100644 index ecad6ad9a..000000000 --- a/apps/manacore/apps/web/src/routes/(auth)/forgot-password/+page.server.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { fail } from '@sveltejs/kit'; -import type { Actions } from './$types'; - -export const actions: Actions = { - default: async ({ request, locals: { supabase }, url }) => { - const formData = await request.formData(); - const email = formData.get('email') as string; - - if (!email) { - return fail(400, { - error: 'Email is required', - email, - }); - } - - // Get the origin for the redirect URL - const origin = url.origin; - const redirectTo = `${origin}/auth/reset-password`; - - // Send password reset email - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo, - }); - - if (error) { - console.error('Password reset error:', error); - return fail(400, { - error: error.message, - email, - }); - } - - // Return success (we don't reveal if the email exists for security) - return { - success: true, - email, - }; - }, -}; diff --git a/apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.server.ts b/apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.server.ts deleted file mode 100644 index 70b0f8002..000000000 --- a/apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.server.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { redirect, fail } from '@sveltejs/kit'; -import type { Actions } from './$types'; - -export const actions: Actions = { - default: async ({ request, locals: { supabase } }) => { - const formData = await request.formData(); - const password = formData.get('password') as string; - const confirmPassword = formData.get('confirmPassword') as string; - - // Validate inputs - if (!password || !confirmPassword) { - return fail(400, { - error: 'Both password fields are required', - }); - } - - if (password.length < 6) { - return fail(400, { - error: 'Password must be at least 6 characters long', - }); - } - - if (password !== confirmPassword) { - return fail(400, { - error: 'Passwords do not match', - }); - } - - // Update the user's password - const { error } = await supabase.auth.updateUser({ - password, - }); - - if (error) { - console.error('Password update error:', error); - return fail(400, { - error: error.message, - }); - } - - // Success - redirect to dashboard - throw redirect(303, '/dashboard'); - }, -}; diff --git a/apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.svelte b/apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.svelte index c067b9fc1..c2885f7c7 100644 --- a/apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(auth)/reset-password/+page.svelte @@ -1,141 +1,103 @@

Reset Password

- {#if verifying} - Verifying your reset link... + {#if success} + Password reset successfully {:else if hasToken} Enter your new password {:else} - Token missing or expired + Invalid or missing reset token {/if}

- {#if verifying} + {#if success}
-
-

Verifying your password reset link...

-
-
- {:else if verificationError} - -
-
⚠️
+

- {verificationError} + Your password has been reset successfully. You will be redirected to the login page + shortly.

- Request a new reset link + Go to login
{:else if hasToken} -
{ - loading = true; - return async ({ update }) => { - await update(); - loading = false; - }; - }} - > - {#if form?.error} + + {#if error}
- {form.error} + {error}
{/if} @@ -152,12 +114,13 @@ name="password" id="password" autocomplete="new-password" - placeholder="••••••••" + placeholder="Enter new password" required - minlength={6} + minlength={12} + bind:value={password} />

- Must be at least 6 characters + Must be at least 12 characters

@@ -173,9 +136,10 @@ name="confirmPassword" id="confirmPassword" autocomplete="new-password" - placeholder="••••••••" + placeholder="Confirm new password" required - minlength={6} + minlength={12} + bind:value={confirmPassword} /> @@ -195,10 +159,10 @@ This password reset link is invalid or has expired.

- Back to login + Request a new reset link diff --git a/apps/manacore/apps/web/src/routes/+layout.server.ts b/apps/manacore/apps/web/src/routes/+layout.server.ts index 11770fd80..907b4b2a0 100644 --- a/apps/manacore/apps/web/src/routes/+layout.server.ts +++ b/apps/manacore/apps/web/src/routes/+layout.server.ts @@ -1,7 +1,8 @@ import type { LayoutServerLoad } from './$types'; -export const load: LayoutServerLoad = async ({ cookies }) => { - return { - cookies: cookies.getAll(), - }; +/** + * Server layout load - minimal, auth handled by mana-core-auth client-side + */ +export const load: LayoutServerLoad = async () => { + return {}; }; diff --git a/apps/manacore/apps/web/src/routes/+layout.ts b/apps/manacore/apps/web/src/routes/+layout.ts index b238de6e1..d33856fe5 100644 --- a/apps/manacore/apps/web/src/routes/+layout.ts +++ b/apps/manacore/apps/web/src/routes/+layout.ts @@ -1,35 +1,14 @@ import { waitLocale } from '$lib/i18n'; import '$lib/i18n'; // This triggers the init() call at module scope -import { createBrowserClient } from '@supabase/ssr'; -import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; import type { LayoutLoad } from './$types'; -export const load: LayoutLoad = async ({ data, depends }) => { +/** + * Layout load function + * + * Auth is handled entirely by Mana Core Auth (@manacore/shared-auth). + * No Supabase is needed - all data comes from mana-core-auth APIs. + */ +export const load: LayoutLoad = async () => { await waitLocale(); - - /** - * Declare a dependency so the layout will be invalidated when `invalidate('supabase:auth')` is called. - */ - depends('supabase:auth'); - - // Create Supabase client for database operations only - // Auth is handled by Mana Core Auth (@manacore/shared-auth) - const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { - global: { - fetch, - }, - cookies: { - getAll() { - return data.cookies; - }, - setAll(cookiesToSet) { - // Browser client handles cookies automatically through the browser - // This is a no-op as cookies are managed via document.cookie in the browser - }, - }, - }); - - // Note: Auth session is managed by Mana Core Auth via authStore, - // not Supabase auth. Supabase is used for database operations only. - return { supabase }; + return {}; }; diff --git a/apps/manacore/apps/web/src/routes/api/auth/set-session/+server.ts b/apps/manacore/apps/web/src/routes/api/auth/set-session/+server.ts deleted file mode 100644 index a03687c6f..000000000 --- a/apps/manacore/apps/web/src/routes/api/auth/set-session/+server.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; - -export const POST: RequestHandler = async ({ request, locals: { supabase } }) => { - try { - const { access_token, refresh_token } = await request.json(); - - if (!access_token || !refresh_token) { - return json( - { success: false, error: 'Access token and refresh token are required' }, - { status: 400 } - ); - } - - // Set the session using the tokens from the URL hash - const { data, error } = await supabase.auth.setSession({ - access_token, - refresh_token, - }); - - if (error) { - console.error('Set session error:', error); - return json({ success: false, error: error.message }, { status: 400 }); - } - - if (!data.session) { - return json({ success: false, error: 'Failed to create session' }, { status: 400 }); - } - - // Session is now set via cookies by the Supabase client - return json({ success: true }); - } catch (error) { - console.error('Unexpected error in set session:', error); - return json({ success: false, error: 'An unexpected error occurred' }, { status: 500 }); - } -}; diff --git a/apps/manacore/apps/web/src/routes/api/auth/verify-token/+server.ts b/apps/manacore/apps/web/src/routes/api/auth/verify-token/+server.ts deleted file mode 100644 index a780638fe..000000000 --- a/apps/manacore/apps/web/src/routes/api/auth/verify-token/+server.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; - -export const POST: RequestHandler = async ({ request, locals: { supabase } }) => { - try { - const { token, type } = await request.json(); - - if (!token || type !== 'recovery') { - return json({ success: false, error: 'Invalid token or type' }, { status: 400 }); - } - - // Verify the OTP token and create a session - const { data, error } = await supabase.auth.verifyOtp({ - token_hash: token, - type: 'recovery', - }); - - if (error) { - console.error('Token verification error:', error); - return json({ success: false, error: error.message }, { status: 400 }); - } - - if (!data.session) { - return json({ success: false, error: 'Failed to create session' }, { status: 400 }); - } - - // Session is now set via cookies by the Supabase client - return json({ success: true }); - } catch (error) { - console.error('Unexpected error in token verification:', error); - return json({ success: false, error: 'An unexpected error occurred' }, { status: 500 }); - } -}; diff --git a/apps/manacore/apps/web/src/routes/auth/reset-password/+page.server.ts b/apps/manacore/apps/web/src/routes/auth/reset-password/+page.server.ts deleted file mode 100644 index 70b0f8002..000000000 --- a/apps/manacore/apps/web/src/routes/auth/reset-password/+page.server.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { redirect, fail } from '@sveltejs/kit'; -import type { Actions } from './$types'; - -export const actions: Actions = { - default: async ({ request, locals: { supabase } }) => { - const formData = await request.formData(); - const password = formData.get('password') as string; - const confirmPassword = formData.get('confirmPassword') as string; - - // Validate inputs - if (!password || !confirmPassword) { - return fail(400, { - error: 'Both password fields are required', - }); - } - - if (password.length < 6) { - return fail(400, { - error: 'Password must be at least 6 characters long', - }); - } - - if (password !== confirmPassword) { - return fail(400, { - error: 'Passwords do not match', - }); - } - - // Update the user's password - const { error } = await supabase.auth.updateUser({ - password, - }); - - if (error) { - console.error('Password update error:', error); - return fail(400, { - error: error.message, - }); - } - - // Success - redirect to dashboard - throw redirect(303, '/dashboard'); - }, -}; diff --git a/apps/manacore/apps/web/src/routes/auth/reset-password/+page.svelte b/apps/manacore/apps/web/src/routes/auth/reset-password/+page.svelte index abaff7f14..ff15f6e4c 100644 --- a/apps/manacore/apps/web/src/routes/auth/reset-password/+page.svelte +++ b/apps/manacore/apps/web/src/routes/auth/reset-password/+page.svelte @@ -1,214 +1,23 @@ - - Reset Password - ManaCore - -
-
-
-

Reset Password

-

- {#if verifying} - Verifying your reset link... - {:else if hasToken} - Enter your new password - {:else} - Token missing or expired - {/if} -

-
- - {#if verifying} - -
-
-

Verifying your password reset link...

-
-
- {:else if verificationError} - -
-
⚠️
-

- {verificationError} -

- - Request a new reset link - -
-
- {:else if hasToken} - - { - loading = true; - return async ({ update }) => { - await update(); - loading = false; - }; - }} - > - {#if form?.error} -
- {form.error} -
- {/if} - -
-
- - -

- Must be at least 6 characters -

-
- -
- - -
- -
- -
-
- -
- {:else} - -
-
⚠️
-

- This password reset link is invalid or has expired. -

- - Back to login - -
-
- {/if} +
+
+

Redirecting...

diff --git a/packages/shared-auth/src/core/authService.ts b/packages/shared-auth/src/core/authService.ts index 3caaf4bcd..0d42ecfb0 100644 --- a/packages/shared-auth/src/core/authService.ts +++ b/packages/shared-auth/src/core/authService.ts @@ -39,6 +39,7 @@ const DEFAULT_ENDPOINTS: AuthEndpoints = { refresh: '/api/v1/auth/refresh', validate: '/api/v1/auth/validate', forgotPassword: '/api/v1/auth/forgot-password', + resetPassword: '/api/v1/auth/reset-password', googleSignIn: '/api/v1/auth/google-signin', appleSignIn: '/api/v1/auth/apple-signin', credits: '/api/v1/credits/balance', @@ -192,6 +193,41 @@ export function createAuthService(config: AuthServiceConfig) { } }, + /** + * Reset password with token + */ + async resetPassword(token: string, newPassword: string): Promise { + try { + const response = await fetch(`${baseUrl}${endpoints.resetPassword}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token, newPassword }), + }); + + if (!response.ok) { + const errorData = await response.json(); + + if (errorData.message?.includes('expired')) { + return { success: false, error: 'Reset link has expired. Please request a new one.' }; + } + + if (errorData.message?.includes('invalid')) { + return { success: false, error: 'Invalid reset link. Please request a new one.' }; + } + + return { success: false, error: errorData.message || 'Password reset failed' }; + } + + return { success: true }; + } catch (error) { + console.error('Error resetting password:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error during password reset', + }; + } + }, + /** * Refresh the authentication tokens */ diff --git a/packages/shared-auth/src/types/index.ts b/packages/shared-auth/src/types/index.ts index 2308bdc24..32d233bb0 100644 --- a/packages/shared-auth/src/types/index.ts +++ b/packages/shared-auth/src/types/index.ts @@ -128,6 +128,7 @@ export interface AuthEndpoints { refresh: string; validate: string; forgotPassword: string; + resetPassword: string; googleSignIn: string; appleSignIn: string; credits: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f21357ea4..4a38c9847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1442,9 +1442,6 @@ importers: '@manacore/shared-subscription-ui': specifier: workspace:* version: link:../../../../packages/shared-subscription-ui - '@manacore/shared-supabase': - specifier: workspace:* - version: link:../../../../packages/shared-supabase '@manacore/shared-tailwind': specifier: workspace:* version: link:../../../../packages/shared-tailwind @@ -1463,12 +1460,6 @@ importers: '@manacore/shared-utils': specifier: workspace:* version: link:../../../../packages/shared-utils - '@supabase/ssr': - specifier: ^0.5.2 - version: 0.5.2(@supabase/supabase-js@2.84.0) - '@supabase/supabase-js': - specifier: ^2.81.1 - version: 2.84.0 svelte-dnd-action: specifier: ^0.9.68 version: 0.9.68(svelte@5.44.0) @@ -9042,11 +9033,6 @@ packages: resolution: {integrity: sha512-ThqjxiCwWiZAroHnYPmnNl6tZk6jxGcG2a7Hp/3kcolPcMj89kWjUTA3cHmhdIWYsP84fHp8MAQjYWMLf7HEUg==} engines: {node: '>=20.0.0'} - '@supabase/ssr@0.5.2': - resolution: {integrity: sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A==} - peerDependencies: - '@supabase/supabase-js': ^2.43.4 - '@supabase/ssr@0.7.0': resolution: {integrity: sha512-G65t5EhLSJ5c8hTCcXifSL9Q/ZRXvqgXeNo+d3P56f4U1IxwTqjB64UfmfixvmMcjuxnq2yGqEWVJqUcO+AzAg==} peerDependencies: @@ -26238,12 +26224,6 @@ snapshots: - bufferutil - utf-8-validate - '@supabase/ssr@0.5.2(@supabase/supabase-js@2.84.0)': - dependencies: - '@supabase/supabase-js': 2.84.0 - '@types/cookie': 0.6.0 - cookie: 0.7.2 - '@supabase/ssr@0.7.0(@supabase/supabase-js@2.84.0)': dependencies: '@supabase/supabase-js': 2.84.0 diff --git a/services/mana-core-auth/src/auth/auth.controller.ts b/services/mana-core-auth/src/auth/auth.controller.ts index 980ff9301..5ad646960 100644 --- a/services/mana-core-auth/src/auth/auth.controller.ts +++ b/services/mana-core-auth/src/auth/auth.controller.ts @@ -18,6 +18,8 @@ import { RegisterB2BDto } from './dto/register-b2b.dto'; import { InviteEmployeeDto } from './dto/invite-employee.dto'; import { AcceptInvitationDto } from './dto/accept-invitation.dto'; import { SetActiveOrganizationDto } from './dto/set-active-organization.dto'; +import { ForgotPasswordDto } from './dto/forgot-password.dto'; +import { ResetPasswordDto } from './dto/reset-password.dto'; import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; /** @@ -137,6 +139,39 @@ export class AuthController { return this.betterAuthService.getJwks(); } + // ========================================================================= + // Password Reset Endpoints + // ========================================================================= + + /** + * Request password reset + * + * Initiates the password reset flow by sending an email with a reset link. + * Always returns success to prevent email enumeration attacks. + */ + @Post('forgot-password') + @HttpCode(HttpStatus.OK) + async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) { + return this.betterAuthService.requestPasswordReset( + forgotPasswordDto.email, + forgotPasswordDto.redirectTo + ); + } + + /** + * Reset password with token + * + * Completes the password reset using the token from the email link. + */ + @Post('reset-password') + @HttpCode(HttpStatus.OK) + async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) { + return this.betterAuthService.resetPassword( + resetPasswordDto.token, + resetPasswordDto.newPassword + ); + } + // ========================================================================= // B2B Registration // ========================================================================= diff --git a/services/mana-core-auth/src/auth/better-auth.config.ts b/services/mana-core-auth/src/auth/better-auth.config.ts index 0f0018824..13efe856f 100644 --- a/services/mana-core-auth/src/auth/better-auth.config.ts +++ b/services/mana-core-auth/src/auth/better-auth.config.ts @@ -80,12 +80,36 @@ export function createBetterAuth(databaseUrl: string) { }, }), - // Email/password authentication only + // Email/password authentication with password reset emailAndPassword: { enabled: true, requireEmailVerification: false, // Can enable later minPasswordLength: 12, maxPasswordLength: 128, + + /** + * Password Reset Configuration + * + * Better Auth provides password reset via: + * - auth.api.forgetPassword({ email }) - Sends reset email + * - auth.api.resetPassword({ newPassword, token }) - Resets password + * + * @see https://www.better-auth.com/docs/authentication/email-password#password-reset + */ + sendResetPassword: async ({ user, url, token }) => { + // TODO: Implement email sending service (e.g., Resend, SendGrid) + // For now, log the reset URL for development + console.log('[Password Reset] User:', user.email); + console.log('[Password Reset] Reset URL:', url); + console.log('[Password Reset] Token:', token); + + // In production, send an email like: + // await sendEmail({ + // to: user.email, + // subject: 'Reset your password', + // html: `Reset your password` + // }); + }, }, // Session configuration diff --git a/services/mana-core-auth/src/auth/dto/forgot-password.dto.ts b/services/mana-core-auth/src/auth/dto/forgot-password.dto.ts new file mode 100644 index 000000000..22a1c8bb8 --- /dev/null +++ b/services/mana-core-auth/src/auth/dto/forgot-password.dto.ts @@ -0,0 +1,22 @@ +import { IsEmail, IsOptional, IsString, IsUrl } from 'class-validator'; + +/** + * Forgot Password DTO + * + * Request body for initiating password reset. + */ +export class ForgotPasswordDto { + /** + * User's email address + */ + @IsEmail() + email: string; + + /** + * Optional redirect URL after password reset + * The reset token will be appended as a query parameter + */ + @IsOptional() + @IsString() + redirectTo?: string; +} diff --git a/services/mana-core-auth/src/auth/dto/reset-password.dto.ts b/services/mana-core-auth/src/auth/dto/reset-password.dto.ts new file mode 100644 index 000000000..fa4d2367b --- /dev/null +++ b/services/mana-core-auth/src/auth/dto/reset-password.dto.ts @@ -0,0 +1,22 @@ +import { IsString, MinLength, MaxLength } from 'class-validator'; + +/** + * Reset Password DTO + * + * Request body for resetting password with token. + */ +export class ResetPasswordDto { + /** + * Reset token from email link + */ + @IsString() + token: string; + + /** + * New password (must meet password requirements) + */ + @IsString() + @MinLength(12, { message: 'Password must be at least 12 characters long' }) + @MaxLength(128, { message: 'Password must be at most 128 characters long' }) + newPassword: string; +} diff --git a/services/mana-core-auth/src/auth/services/better-auth.service.ts b/services/mana-core-auth/src/auth/services/better-auth.service.ts index 11cfce791..25a1be0fc 100644 --- a/services/mana-core-auth/src/auth/services/better-auth.service.ts +++ b/services/mana-core-auth/src/auth/services/better-auth.service.ts @@ -845,6 +845,92 @@ export class BetterAuthService { } } + // ========================================================================= + // Password Reset Methods + // ========================================================================= + + /** + * Request password reset + * + * Sends a password reset email to the user. + * Uses Better Auth's forgetPassword API. + * + * @param email - User's email address + * @param redirectTo - Optional URL to redirect after reset (used in email link) + * @returns Success status + */ + async requestPasswordReset( + email: string, + redirectTo?: string + ): Promise<{ success: boolean; message: string }> { + try { + // Better Auth's forgetPassword method + // See: https://www.better-auth.com/docs/authentication/email-password#password-reset + await (this.auth.api as any).forgetPassword({ + body: { + email, + redirectTo, + }, + }); + + // Always return success to prevent email enumeration + return { + success: true, + message: 'If an account with that email exists, a password reset link has been sent', + }; + } catch (error) { + console.error('[requestPasswordReset] Error:', error); + // Always return success to prevent email enumeration attacks + return { + success: true, + message: 'If an account with that email exists, a password reset link has been sent', + }; + } + } + + /** + * Reset password with token + * + * Resets the user's password using the token from the reset email. + * Uses Better Auth's resetPassword API. + * + * @param token - Reset token from email link + * @param newPassword - New password to set + * @returns Success status + * @throws UnauthorizedException if token is invalid or expired + */ + async resetPassword( + token: string, + newPassword: string + ): Promise<{ success: boolean; message: string }> { + try { + // Better Auth's resetPassword method + // See: https://www.better-auth.com/docs/authentication/email-password#password-reset + await (this.auth.api as any).resetPassword({ + body: { + token, + newPassword, + }, + }); + + return { + success: true, + message: 'Password has been reset successfully', + }; + } catch (error: unknown) { + if (error instanceof Error) { + if ( + error.message?.includes('invalid') || + error.message?.includes('expired') || + error.message?.includes('not found') + ) { + throw new UnauthorizedException('Invalid or expired reset token'); + } + } + throw error; + } + } + /** * Get JWKS (JSON Web Key Set) *