feat: User onboarding finished

This commit is contained in:
Eric Gullickson
2025-12-23 10:26:10 -06:00
parent 55cf4923b8
commit 96ee43ea94
19 changed files with 698 additions and 67 deletions

View File

@@ -8,7 +8,7 @@ import { AuthService } from '../domain/auth.service';
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { signupSchema } from './auth.validation';
import { signupSchema, resendVerificationPublicSchema } from './auth.validation';
export class AuthController {
private authService: AuthService;
@@ -119,4 +119,67 @@ export class AuthController {
});
}
}
/**
* POST /api/auth/resend-verification-public
* Resend verification email by email address
* Public endpoint - no JWT required (for pre-login verification page)
*/
async resendVerificationPublic(request: FastifyRequest, reply: FastifyReply) {
try {
const validation = resendVerificationPublicSchema.safeParse(request.body);
if (!validation.success) {
return reply.code(400).send({
error: 'Validation error',
message: validation.error.errors[0]?.message || 'Invalid input',
});
}
const { email } = validation.data;
const result = await this.authService.resendVerificationByEmail(email);
logger.info('Public resend verification requested', { email: email.substring(0, 3) + '***' });
return reply.code(200).send(result);
} catch (error: any) {
logger.error('Failed to resend verification email (public)', { error });
// Always return success for security (don't reveal if email exists)
return reply.code(200).send({
message: 'If an account exists with this email, a verification link will be sent.',
});
}
}
/**
* GET /api/auth/user-status
* Get user status for routing decisions
* Protected endpoint - requires JWT
*/
async getUserStatus(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = (request as any).user.sub;
const result = await this.authService.getUserStatus(userId);
logger.info('User status retrieved', {
userId: userId.substring(0, 8) + '...',
emailVerified: result.emailVerified,
onboardingCompleted: result.onboardingCompleted,
});
return reply.code(200).send(result);
} catch (error: any) {
logger.error('Failed to get user status', {
error,
userId: (request as any).user?.sub,
});
return reply.code(500).send({
error: 'Internal server error',
message: 'Failed to get user status',
});
}
}
}

View File

@@ -27,4 +27,13 @@ export const authRoutes: FastifyPluginAsync = async (
preHandler: [fastify.authenticate],
handler: authController.resendVerification.bind(authController),
});
// POST /api/auth/resend-verification-public - Resend verification by email (public, no JWT)
fastify.post('/auth/resend-verification-public', authController.resendVerificationPublic.bind(authController));
// GET /api/auth/user-status - Get user status for routing (requires JWT, verification exempt)
fastify.get('/auth/user-status', {
preHandler: [fastify.authenticate],
handler: authController.getUserStatus.bind(authController),
});
};

View File

@@ -21,3 +21,10 @@ export const signupSchema = z.object({
});
export type SignupInput = z.infer<typeof signupSchema>;
// Schema for public resend verification endpoint (no JWT required)
export const resendVerificationPublicSchema = z.object({
email: z.string().email('Invalid email format'),
});
export type ResendVerificationPublicInput = z.infer<typeof resendVerificationPublicSchema>;