mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
feat(auth): add email verification endpoint for Better Auth
Better Auth generates verification URLs with /api/auth/verify-email path, but NestJS uses /api/v1 prefix. This adds a passthrough controller to handle the native Better Auth routes and properly verify user emails. - Add BetterAuthPassthroughController for /api/auth/* routes - Add verifyEmail method to BetterAuthService - Exclude /api/auth/* from global prefix in main.ts - Register passthrough controller in AuthModule Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
def7249058
commit
ad4ae93f29
4 changed files with 103 additions and 3 deletions
|
|
@ -1,11 +1,12 @@
|
|||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { BetterAuthPassthroughController } from './better-auth-passthrough.controller';
|
||||
import { BetterAuthService } from './services/better-auth.service';
|
||||
import { ReferralsModule } from '../referrals/referrals.module';
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => ReferralsModule)],
|
||||
controllers: [AuthController],
|
||||
controllers: [AuthController, BetterAuthPassthroughController],
|
||||
providers: [BetterAuthService],
|
||||
exports: [BetterAuthService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Better Auth Passthrough Controller
|
||||
*
|
||||
* This controller handles Better Auth's native routes that are generated
|
||||
* with the `/api/auth/*` prefix (without the NestJS `/api/v1` prefix).
|
||||
*
|
||||
* Routes handled:
|
||||
* - GET /api/auth/verify-email - Email verification from verification emails
|
||||
*
|
||||
* This is necessary because Better Auth generates URLs with `/api/auth/*`
|
||||
* but our NestJS API uses `/api/v1/*` as the global prefix.
|
||||
*/
|
||||
|
||||
import { Controller, Get, Query, Res, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { BetterAuthService } from './services/better-auth.service';
|
||||
|
||||
@Controller('api/auth')
|
||||
export class BetterAuthPassthroughController {
|
||||
constructor(private readonly betterAuthService: BetterAuthService) {}
|
||||
|
||||
/**
|
||||
* Handle email verification
|
||||
*
|
||||
* Better Auth sends verification emails with links to:
|
||||
* {baseURL}/api/auth/verify-email?token=...
|
||||
*
|
||||
* This endpoint calls Better Auth's verifyEmail API and redirects
|
||||
* the user to the appropriate page.
|
||||
*/
|
||||
@Get('verify-email')
|
||||
async verifyEmail(@Query('token') token: string, @Res() res: Response) {
|
||||
try {
|
||||
if (!token) {
|
||||
return res.redirect('/verification-failed?error=missing_token');
|
||||
}
|
||||
|
||||
// Call Better Auth's verifyEmail API
|
||||
const result = await this.betterAuthService.verifyEmail(token);
|
||||
|
||||
if (result.success) {
|
||||
// Redirect to success page (frontend should handle this)
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://mana.how';
|
||||
return res.redirect(`${frontendUrl}/email-verified`);
|
||||
} else {
|
||||
// Redirect to error page
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://mana.how';
|
||||
return res.redirect(`${frontendUrl}/verification-failed?error=${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[verify-email] Error:', error);
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://mana.how';
|
||||
return res.redirect(`${frontendUrl}/verification-failed?error=verification_failed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -938,6 +938,48 @@ export class BetterAuthService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify email with token
|
||||
*
|
||||
* Verifies the user's email using the token from the verification email.
|
||||
* Uses Better Auth's verifyEmail API.
|
||||
*
|
||||
* @param token - Verification token from email link
|
||||
* @returns Success status
|
||||
*/
|
||||
async verifyEmail(token: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// Better Auth's verifyEmail method
|
||||
// See: https://www.better-auth.com/docs/authentication/email-verification
|
||||
const api = this.auth.api as any;
|
||||
|
||||
const result = await api.verifyEmail({
|
||||
query: { token },
|
||||
});
|
||||
|
||||
console.log('[verifyEmail] Result:', result);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('[verifyEmail] Error:', errorMessage);
|
||||
|
||||
if (errorMessage.includes('invalid') || errorMessage.includes('expired')) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'invalid_or_expired_token',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'verification_failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWKS (JSON Web Key Set)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -79,9 +79,10 @@ async function bootstrap() {
|
|||
})
|
||||
);
|
||||
|
||||
// Global prefix (exclude metrics endpoint)
|
||||
// Global prefix (exclude metrics, health, and Better Auth native routes)
|
||||
// Better Auth generates verification URLs with /api/auth/* prefix
|
||||
app.setGlobalPrefix('api/v1', {
|
||||
exclude: ['metrics', 'health'],
|
||||
exclude: ['metrics', 'health', 'api/auth/(.*)'],
|
||||
});
|
||||
|
||||
const port = configService.get<number>('port') || 3001;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue