♻️ refactor: migrate manacore-web from Supabase to mana-core-auth

- 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 <noreply@anthropic.com>
This commit is contained in:
Wuesteon 2025-12-08 17:04:35 +01:00
parent 48c5cb48f7
commit ee091c4b10
23 changed files with 357 additions and 639 deletions

View file

@ -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
// =========================================================================

View file

@ -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: `<a href="${url}">Reset your password</a>`
// });
},
},
// Session configuration

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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)
*