Add circuit breaker pattern to email verification system
All checks were successful
Build And Push Image / docker (push) Successful in 2m53s
All checks were successful
Build And Push Image / docker (push) Successful in 2m53s
Implement rate limiting and attempt tracking to prevent verification abuse and infinite reload loops. Add temporary blocking with clear user feedback, enhanced error states, and retry logic. Includes new verification state utilities and improved UI components for better user experience during blocked states.
This commit is contained in:
@@ -11,8 +11,8 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
console.log('[verify-email] Processing verification token...');
|
||||
|
||||
// Verify the token
|
||||
const { verifyEmailToken } = await import('~/server/utils/email-tokens');
|
||||
// Verify the token WITHOUT consuming it yet
|
||||
const { verifyEmailToken, consumeEmailToken } = await import('~/server/utils/email-tokens');
|
||||
const { userId, email } = await verifyEmailToken(token);
|
||||
|
||||
// Update user verification status in Keycloak
|
||||
@@ -20,6 +20,7 @@ export default defineEventHandler(async (event) => {
|
||||
const keycloak = createKeycloakAdminClient();
|
||||
|
||||
let partialSuccess = false;
|
||||
let keycloakError = null;
|
||||
|
||||
try {
|
||||
await keycloak.updateUserProfile(userId, {
|
||||
@@ -31,11 +32,25 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
console.log('[verify-email] Successfully verified user:', userId, 'email:', email);
|
||||
|
||||
// ONLY consume token after successful Keycloak update
|
||||
await consumeEmailToken(token);
|
||||
|
||||
} catch (keycloakError: any) {
|
||||
console.error('[verify-email] Keycloak update failed:', keycloakError.message);
|
||||
// Even if Keycloak update fails, consider verification successful if token was valid
|
||||
// This prevents user frustration due to backend issues
|
||||
partialSuccess = true;
|
||||
|
||||
// Check if this is a retryable error or a permanent failure
|
||||
if (keycloakError.message?.includes('error-user-attribute-required')) {
|
||||
// This is a configuration issue - don't consume token, allow retries
|
||||
console.log('[verify-email] Keycloak configuration error - token preserved for retry');
|
||||
partialSuccess = true;
|
||||
keycloakError = keycloakError.message;
|
||||
} else {
|
||||
// For other errors, still consume token to prevent infinite retries
|
||||
console.log('[verify-email] Consuming token despite Keycloak error to prevent loops');
|
||||
await consumeEmailToken(token);
|
||||
partialSuccess = true;
|
||||
keycloakError = keycloakError.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Return JSON response for client-side navigation
|
||||
@@ -44,7 +59,8 @@ export default defineEventHandler(async (event) => {
|
||||
data: {
|
||||
userId,
|
||||
email,
|
||||
partialSuccess
|
||||
partialSuccess,
|
||||
keycloakError: keycloakError || undefined
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -109,9 +109,7 @@ export async function verifyEmailToken(token: string): Promise<{ userId: string;
|
||||
throw new Error('Token payload mismatch');
|
||||
}
|
||||
|
||||
// Remove token after successful verification (single use)
|
||||
activeTokens.delete(token);
|
||||
|
||||
// DON'T DELETE TOKEN YET - let the caller decide when to consume it
|
||||
console.log('[email-tokens] Successfully verified token for user:', decoded.userId, 'email:', decoded.email);
|
||||
|
||||
return {
|
||||
@@ -133,6 +131,32 @@ export async function verifyEmailToken(token: string): Promise<{ userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume a token after successful operations
|
||||
*/
|
||||
export async function consumeEmailToken(token: string): Promise<void> {
|
||||
if (!token) {
|
||||
throw new Error('Token is required');
|
||||
}
|
||||
|
||||
// Remove token from active tokens (single use)
|
||||
const wasRemoved = activeTokens.delete(token);
|
||||
|
||||
if (wasRemoved) {
|
||||
console.log('[email-tokens] Token consumed successfully');
|
||||
} else {
|
||||
console.log('[email-tokens] Token was already consumed or not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify token without consuming it (for retries)
|
||||
*/
|
||||
export async function verifyEmailTokenWithoutConsuming(token: string): Promise<{ userId: string; email: string }> {
|
||||
// This is the same as verifyEmailToken but more explicit about not consuming
|
||||
return await verifyEmailToken(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is still valid without consuming it
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user