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}
-
@@ -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}
-
-
-
- {:else}
-
-
-
⚠️
-
- This password reset link is invalid or has expired.
-
-
- Back to login
-
-
-
- {/if}
+
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)
*