feat: delete users - not tested
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @ai-summary Fastify JWT authentication plugin using Auth0
|
||||
* @ai-context Validates JWT tokens against Auth0 JWKS endpoint, hydrates userContext with profile
|
||||
* @ai-context Includes email verification guard to block unverified users
|
||||
*/
|
||||
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import fp from 'fastify-plugin';
|
||||
@@ -10,6 +11,15 @@ import { appConfig } from '../config/config-loader';
|
||||
import { logger } from '../logging/logger';
|
||||
import { UserProfileRepository } from '../../features/user-profile/data/user-profile.repository';
|
||||
import { pool } from '../config/database';
|
||||
import { auth0ManagementClient } from '../auth/auth0-management.client';
|
||||
|
||||
// Routes that don't require email verification
|
||||
const VERIFICATION_EXEMPT_ROUTES = [
|
||||
'/api/auth/',
|
||||
'/api/onboarding/',
|
||||
'/api/health',
|
||||
'/health',
|
||||
];
|
||||
|
||||
// Define the Auth0 JWT payload type
|
||||
interface Auth0JwtPayload {
|
||||
@@ -42,6 +52,8 @@ declare module 'fastify' {
|
||||
userId: string;
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
emailVerified: boolean;
|
||||
onboardingCompleted: boolean;
|
||||
isAdmin: boolean;
|
||||
adminRecord?: any;
|
||||
};
|
||||
@@ -97,31 +109,91 @@ const authPlugin: FastifyPluginAsync = async (fastify) => {
|
||||
// Initialize profile repository for user profile hydration
|
||||
const profileRepo = new UserProfileRepository(pool);
|
||||
|
||||
// Helper to check if route is exempt from verification
|
||||
const isVerificationExempt = (url: string): boolean => {
|
||||
return VERIFICATION_EXEMPT_ROUTES.some(route => url.startsWith(route));
|
||||
};
|
||||
|
||||
// Decorate with authenticate function that validates JWT
|
||||
fastify.decorate('authenticate', async function(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
|
||||
const userId = request.user?.sub;
|
||||
if (!userId) {
|
||||
throw new Error('Missing user ID in JWT');
|
||||
}
|
||||
|
||||
// Get or create user profile from database
|
||||
// This ensures we have reliable email/displayName for notifications
|
||||
let email = request.user?.email;
|
||||
let displayName: string | undefined;
|
||||
let emailVerified = false;
|
||||
let onboardingCompleted = false;
|
||||
|
||||
try {
|
||||
// If JWT doesn't have email, fetch from Auth0 Management API
|
||||
if (!email || email.includes('@unknown.local')) {
|
||||
try {
|
||||
const auth0User = await auth0ManagementClient.getUser(userId);
|
||||
if (auth0User.email) {
|
||||
email = auth0User.email;
|
||||
emailVerified = auth0User.emailVerified;
|
||||
logger.info('Fetched email from Auth0 Management API', {
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
hasEmail: true,
|
||||
});
|
||||
}
|
||||
} catch (auth0Error) {
|
||||
logger.warn('Failed to fetch user from Auth0 Management API', {
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
error: auth0Error instanceof Error ? auth0Error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get or create profile with correct email
|
||||
const profile = await profileRepo.getOrCreate(userId, {
|
||||
email: request.user?.email || `${userId}@unknown.local`,
|
||||
email: email || `${userId}@unknown.local`,
|
||||
displayName: request.user?.name || request.user?.nickname,
|
||||
});
|
||||
|
||||
// If profile has placeholder email but we now have real email, update it
|
||||
if (profile.email.includes('@unknown.local') && email && !email.includes('@unknown.local')) {
|
||||
await profileRepo.updateEmail(userId, email);
|
||||
logger.info('Updated profile with correct email from Auth0', {
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
});
|
||||
}
|
||||
|
||||
// Use notificationEmail if set, otherwise fall back to profile email
|
||||
email = profile.notificationEmail || profile.email;
|
||||
displayName = profile.displayName || undefined;
|
||||
emailVerified = profile.emailVerified;
|
||||
onboardingCompleted = profile.onboardingCompletedAt !== null;
|
||||
|
||||
// Sync email verification status from Auth0 if needed
|
||||
if (!emailVerified) {
|
||||
try {
|
||||
const isVerifiedInAuth0 = await auth0ManagementClient.checkEmailVerified(userId);
|
||||
if (isVerifiedInAuth0 && !profile.emailVerified) {
|
||||
await profileRepo.updateEmailVerified(userId, true);
|
||||
emailVerified = true;
|
||||
logger.info('Synced email verification status from Auth0', {
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
});
|
||||
}
|
||||
} catch (syncError) {
|
||||
// Don't fail auth if sync fails, just log
|
||||
logger.warn('Failed to sync email verification status', {
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
error: syncError instanceof Error ? syncError.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (profileError) {
|
||||
// Log but don't fail auth if profile fetch fails
|
||||
logger.warn('Failed to fetch user profile', {
|
||||
userId: userId?.substring(0, 8) + '...',
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
error: profileError instanceof Error ? profileError.message : 'Unknown error',
|
||||
});
|
||||
// Fall back to JWT email if available
|
||||
@@ -133,13 +205,29 @@ const authPlugin: FastifyPluginAsync = async (fastify) => {
|
||||
userId,
|
||||
email,
|
||||
displayName,
|
||||
emailVerified,
|
||||
onboardingCompleted,
|
||||
isAdmin: false, // Default to false; admin status checked by admin guard
|
||||
};
|
||||
|
||||
// Email verification guard - block unverified users from non-exempt routes
|
||||
if (!emailVerified && !isVerificationExempt(request.url)) {
|
||||
logger.warn('Blocked unverified user from accessing protected route', {
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
path: request.url,
|
||||
});
|
||||
return reply.code(403).send({
|
||||
error: 'Email not verified',
|
||||
message: 'Please verify your email address before accessing the application',
|
||||
code: 'EMAIL_NOT_VERIFIED',
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('JWT authentication successful', {
|
||||
userId: userId?.substring(0, 8) + '...',
|
||||
userId: userId.substring(0, 8) + '...',
|
||||
hasEmail: !!email,
|
||||
audience: auth0Config.audience
|
||||
emailVerified,
|
||||
audience: auth0Config.audience,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn('JWT authentication failed', {
|
||||
@@ -148,9 +236,9 @@ const authPlugin: FastifyPluginAsync = async (fastify) => {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
|
||||
reply.code(401).send({
|
||||
return reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: 'Invalid or missing JWT token'
|
||||
message: 'Invalid or missing JWT token',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user