Add password setup flow with server-side validation
Build And Push Image / docker (push) Successful in 3m2s
Details
Build And Push Image / docker (push) Successful in 3m2s
Details
- Replace external password setup link with internal navigation - Add comprehensive password validation utility with strength requirements - Create dedicated password setup page and API endpoint - Streamline user flow from email verification to password creation
This commit is contained in:
parent
30b7e23319
commit
d14008efd4
|
|
@ -0,0 +1,410 @@
|
|||
<template>
|
||||
<div :class="containerClasses">
|
||||
<v-container class="fill-height" fluid>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4">
|
||||
<v-card class="elevation-12 rounded-lg">
|
||||
<v-card-text class="text-center pa-8">
|
||||
<div class="mb-6">
|
||||
<v-icon
|
||||
color="primary"
|
||||
size="80"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-lock-plus
|
||||
</v-icon>
|
||||
|
||||
<h1 class="text-h4 font-weight-bold text-primary mb-3">
|
||||
Set Your Password
|
||||
</h1>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis mb-2" v-if="email">
|
||||
Complete your registration by setting a secure password for <strong>{{ email }}</strong>
|
||||
</p>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis">
|
||||
Choose a strong password to secure your MonacoUSA Portal account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Password Setup Form -->
|
||||
<v-form ref="formRef" v-model="formValid" @submit.prevent="setupPassword">
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
label="New Password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:rules="passwordRules"
|
||||
:error="!!errorMessage"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
label="Confirm Password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:rules="confirmPasswordRules"
|
||||
:error="!!errorMessage"
|
||||
:append-inner-icon="showConfirmPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append-inner="showConfirmPassword = !showConfirmPassword"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<!-- Password Strength Indicator -->
|
||||
<v-progress-linear
|
||||
:model-value="passwordStrength"
|
||||
:color="passwordStrengthColor"
|
||||
height="6"
|
||||
class="mb-2"
|
||||
/>
|
||||
<p class="text-caption text-medium-emphasis mb-4">
|
||||
Password strength: {{ passwordStrengthLabel }}
|
||||
</p>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<v-alert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4 text-start"
|
||||
icon="mdi-alert"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
|
||||
<!-- Success Alert -->
|
||||
<v-alert
|
||||
v-if="successMessage"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4 text-start"
|
||||
icon="mdi-check-circle"
|
||||
>
|
||||
{{ successMessage }}
|
||||
</v-alert>
|
||||
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
block
|
||||
:loading="loading"
|
||||
:disabled="!formValid || loading"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-check</v-icon>
|
||||
Set Password & Continue
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
block
|
||||
:to="{ path: '/login' }"
|
||||
:disabled="loading"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-login</v-icon>
|
||||
I Already Have a Password
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="outline"
|
||||
size="small"
|
||||
variant="text"
|
||||
block
|
||||
to="/"
|
||||
:disabled="loading"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-home</v-icon>
|
||||
Return to Home
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
|
||||
<!-- Additional help -->
|
||||
<div class="mt-6 pt-4 border-t">
|
||||
<p class="text-caption text-medium-emphasis mb-2">
|
||||
Need help? Contact support at:
|
||||
</p>
|
||||
<v-chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-email"
|
||||
>
|
||||
support@monacousa.org
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getOptimizedClasses,
|
||||
applyMobileSafariFixes
|
||||
} from '~/utils/mobile-safari-utils';
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
middleware: 'guest'
|
||||
});
|
||||
|
||||
// Mobile Safari optimization classes
|
||||
const containerClasses = computed(() => [
|
||||
'password-setup-page',
|
||||
...getOptimizedClasses()
|
||||
].join(' '));
|
||||
|
||||
// Reactive state
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const successMessage = ref('');
|
||||
const formValid = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const showConfirmPassword = ref(false);
|
||||
|
||||
// Form data
|
||||
const password = ref('');
|
||||
const confirmPassword = ref('');
|
||||
|
||||
// Get query parameters
|
||||
const route = useRoute();
|
||||
const email = computed(() => route.query.email as string || '');
|
||||
const token = computed(() => route.query.token as string || '');
|
||||
|
||||
// Form ref
|
||||
const formRef = ref();
|
||||
|
||||
// Password strength calculation
|
||||
const passwordStrength = computed(() => {
|
||||
if (!password.value) return 0;
|
||||
|
||||
let score = 0;
|
||||
// Length
|
||||
if (password.value.length >= 8) score += 20;
|
||||
if (password.value.length >= 12) score += 10;
|
||||
|
||||
// Character types
|
||||
if (/[a-z]/.test(password.value)) score += 15;
|
||||
if (/[A-Z]/.test(password.value)) score += 15;
|
||||
if (/[0-9]/.test(password.value)) score += 15;
|
||||
if (/[^A-Za-z0-9]/.test(password.value)) score += 25;
|
||||
|
||||
return Math.min(score, 100);
|
||||
});
|
||||
|
||||
const passwordStrengthColor = computed(() => {
|
||||
if (passwordStrength.value < 40) return 'error';
|
||||
if (passwordStrength.value < 70) return 'warning';
|
||||
return 'success';
|
||||
});
|
||||
|
||||
const passwordStrengthLabel = computed(() => {
|
||||
if (passwordStrength.value < 40) return 'Weak';
|
||||
if (passwordStrength.value < 70) return 'Good';
|
||||
return 'Strong';
|
||||
});
|
||||
|
||||
// Validation rules
|
||||
const passwordRules = [
|
||||
(v: string) => !!v || 'Password is required',
|
||||
(v: string) => v.length >= 8 || 'Password must be at least 8 characters',
|
||||
(v: string) => /[A-Z]/.test(v) || 'Password must contain at least one uppercase letter',
|
||||
(v: string) => /[a-z]/.test(v) || 'Password must contain at least one lowercase letter',
|
||||
(v: string) => /[0-9]/.test(v) || 'Password must contain at least one number',
|
||||
];
|
||||
|
||||
const confirmPasswordRules = [
|
||||
(v: string) => !!v || 'Please confirm your password',
|
||||
(v: string) => v === password.value || 'Passwords do not match',
|
||||
];
|
||||
|
||||
// Set page title with mobile viewport optimization
|
||||
useHead({
|
||||
title: 'Set Your Password - MonacoUSA Portal',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Set your password to complete your MonacoUSA Portal registration.'
|
||||
},
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }
|
||||
]
|
||||
});
|
||||
|
||||
// Setup password function
|
||||
const setupPassword = async () => {
|
||||
if (!formValid.value) return;
|
||||
|
||||
if (!email.value) {
|
||||
errorMessage.value = 'No email address provided. Please check the link from your email.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
successMessage.value = '';
|
||||
|
||||
// Call our password setup API
|
||||
const response = await $fetch('/api/auth/setup-password', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
token: token.value
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[setup-password] Password setup successful:', response);
|
||||
|
||||
successMessage.value = 'Password set successfully! Redirecting to login...';
|
||||
|
||||
// Wait a moment to show success message, then redirect
|
||||
setTimeout(() => {
|
||||
navigateTo({
|
||||
path: '/login',
|
||||
query: { email: email.value, passwordSet: 'true' }
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('[setup-password] Password setup failed:', err);
|
||||
|
||||
if (err.statusCode === 400) {
|
||||
errorMessage.value = 'Invalid request. Please check your information and try again.';
|
||||
} else if (err.statusCode === 404) {
|
||||
errorMessage.value = 'User not found. The link may be invalid or expired.';
|
||||
} else if (err.statusCode === 409) {
|
||||
errorMessage.value = 'Password has already been set. You can log in with your existing password.';
|
||||
} else if (err.statusCode === 422) {
|
||||
errorMessage.value = 'Password does not meet security requirements. Please choose a stronger password.';
|
||||
} else {
|
||||
errorMessage.value = err.message || 'Failed to set password. Please try again or contact support.';
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Apply mobile Safari fixes
|
||||
onMounted(() => {
|
||||
// Apply mobile Safari fixes
|
||||
if (typeof window !== 'undefined') {
|
||||
applyMobileSafariFixes();
|
||||
}
|
||||
|
||||
console.log('[setup-password] Password setup page loaded for:', email.value);
|
||||
|
||||
// Check if we have required parameters
|
||||
if (!email.value) {
|
||||
errorMessage.value = 'No email address provided. Please use the link from your verification email.';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.password-setup-page {
|
||||
min-height: 100vh;
|
||||
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
overflow-x: hidden; /* Prevent horizontal scroll on mobile */
|
||||
}
|
||||
|
||||
/* Mobile Safari optimizations */
|
||||
.password-setup-page.is-mobile-safari {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.password-setup-page.performance-mode {
|
||||
will-change: auto;
|
||||
transform: translateZ(0); /* Lighter hardware acceleration */
|
||||
}
|
||||
|
||||
.fill-height {
|
||||
min-height: 100vh;
|
||||
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
||||
}
|
||||
|
||||
/* Mobile Safari fill-height optimization */
|
||||
.is-mobile-safari .fill-height {
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for mobile */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(163, 21, 21, 0.5);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.password-setup-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Optimize button spacing on mobile */
|
||||
.gap-3 {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Improve touch targets on mobile */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.v-btn {
|
||||
min-height: 48px; /* Ensure touch-friendly button size */
|
||||
}
|
||||
}
|
||||
|
||||
/* Performance mode optimizations */
|
||||
.performance-mode .v-card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; /* Lighter shadow */
|
||||
}
|
||||
|
||||
.performance-mode .v-btn {
|
||||
transition: none; /* Remove button transitions for better performance */
|
||||
}
|
||||
|
||||
/* Form styling improvements */
|
||||
.v-text-field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.v-progress-linear {
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -49,26 +49,13 @@
|
|||
size="large"
|
||||
variant="elevated"
|
||||
block
|
||||
:href="setupPasswordUrl"
|
||||
target="_blank"
|
||||
@click="goToPasswordSetup"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-lock</v-icon>
|
||||
<v-icon start>mdi-lock-plus</v-icon>
|
||||
Set Your Password
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
block
|
||||
:to="{ path: '/login', query: { verified: 'true' } }"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-login</v-icon>
|
||||
Log In to Portal
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="outline"
|
||||
size="small"
|
||||
|
|
@ -147,6 +134,16 @@ useHead({
|
|||
]
|
||||
});
|
||||
|
||||
// Go to password setup page
|
||||
const goToPasswordSetup = () => {
|
||||
navigateTo({
|
||||
path: '/auth/setup-password',
|
||||
query: {
|
||||
email: email.value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Apply mobile Safari fixes and track verification
|
||||
onMounted(() => {
|
||||
// Apply mobile Safari fixes
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* Password Setup API Endpoint
|
||||
* Handles setting passwords for newly registered users
|
||||
*/
|
||||
|
||||
import { createKeycloakAdminClient } from '~/server/utils/keycloak-admin';
|
||||
import { validatePassword } from '~/server/utils/security';
|
||||
|
||||
interface SetupPasswordRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log('[api/auth/setup-password] =========================');
|
||||
console.log('[api/auth/setup-password] POST /api/auth/setup-password - Password setup');
|
||||
|
||||
try {
|
||||
const body = await readBody(event) as SetupPasswordRequest;
|
||||
console.log('[api/auth/setup-password] Setup password attempt for:', body.email);
|
||||
|
||||
// 1. Validate request data
|
||||
if (!body.email?.trim()) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Email address is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (!body.password?.trim()) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Password is required'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Validate password strength
|
||||
const passwordValidation = validatePassword(body.password);
|
||||
if (!passwordValidation.isValid) {
|
||||
throw createError({
|
||||
statusCode: 422,
|
||||
statusMessage: `Password validation failed: ${passwordValidation.errors.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Find user in Keycloak
|
||||
const keycloakAdmin = createKeycloakAdminClient();
|
||||
const existingUsers = await keycloakAdmin.findUserByEmail(body.email);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'User not found. Please register first or contact support.'
|
||||
});
|
||||
}
|
||||
|
||||
const user = existingUsers[0];
|
||||
|
||||
// 4. Check if user already has a password set by checking if they have any required actions
|
||||
console.log('[api/auth/setup-password] User found:', user.id, 'Required actions:', user.requiredActions);
|
||||
|
||||
if (user.requiredActions && !user.requiredActions.includes('UPDATE_PASSWORD')) {
|
||||
console.log('[api/auth/setup-password] User already has password set, allowing password update');
|
||||
// Allow password updates - this could be a password reset scenario
|
||||
}
|
||||
|
||||
// 5. Set the user's password in Keycloak using direct REST API
|
||||
console.log('[api/auth/setup-password] Setting password for user:', user.id);
|
||||
|
||||
const adminToken = await keycloakAdmin.getAdminToken();
|
||||
const config = useRuntimeConfig();
|
||||
const adminBaseUrl = config.keycloak.issuer.replace('/realms/', '/admin/realms/');
|
||||
|
||||
// Set password using Keycloak Admin REST API
|
||||
const setPasswordResponse = await fetch(`${adminBaseUrl}/users/${user.id}/reset-password`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${adminToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'MonacoUSA-Portal/1.0'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'password',
|
||||
value: body.password,
|
||||
temporary: false
|
||||
})
|
||||
});
|
||||
|
||||
if (!setPasswordResponse.ok) {
|
||||
const errorText = await setPasswordResponse.text().catch(() => 'Unknown error');
|
||||
throw createError({
|
||||
statusCode: setPasswordResponse.status,
|
||||
statusMessage: `Failed to set password: ${errorText}`
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Update user to ensure they're enabled, email is verified, and remove required actions
|
||||
const updateUserResponse = await fetch(`${adminBaseUrl}/users/${user.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${adminToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'MonacoUSA-Portal/1.0'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...user,
|
||||
enabled: true,
|
||||
emailVerified: true,
|
||||
requiredActions: [], // Remove all required actions including UPDATE_PASSWORD
|
||||
attributes: {
|
||||
...user.attributes,
|
||||
needsPasswordSetup: ['false'],
|
||||
passwordSetAt: [new Date().toISOString()]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!updateUserResponse.ok) {
|
||||
const errorText = await updateUserResponse.text().catch(() => 'Unknown error');
|
||||
console.warn('[api/auth/setup-password] Failed to update user profile:', errorText);
|
||||
// Don't fail the entire operation if this update fails
|
||||
}
|
||||
|
||||
console.log(`[api/auth/setup-password] ✅ Password setup successful for user: ${body.email}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Password set successfully! You can now log in to your account.',
|
||||
data: {
|
||||
email: body.email,
|
||||
passwordSet: true,
|
||||
canLogin: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[api/auth/setup-password] ❌ Password setup failed:', error);
|
||||
|
||||
// Handle Keycloak specific errors
|
||||
if (error.response?.status === 404) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'User not found. Please register first or contact support.'
|
||||
});
|
||||
} else if (error.response?.status === 409) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Password has already been set. You can log in with your existing password.'
|
||||
});
|
||||
} else if (error.response?.status === 400) {
|
||||
throw createError({
|
||||
statusCode: 422,
|
||||
statusMessage: 'Password does not meet Keycloak security requirements. Please choose a stronger password.'
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
|
@ -128,6 +128,59 @@ export const cleanupOldEntries = (): void => {
|
|||
console.log('🧹 Cleaned up old security entries');
|
||||
};
|
||||
|
||||
// Password validation function
|
||||
export const validatePassword = (password: string): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!password || typeof password !== 'string') {
|
||||
errors.push('Password is required');
|
||||
return { isValid: false, errors };
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
errors.push('Password must be at least 8 characters long');
|
||||
}
|
||||
|
||||
if (password.length > 128) {
|
||||
errors.push('Password must not exceed 128 characters');
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push('Password must contain at least one uppercase letter');
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push('Password must contain at least one lowercase letter');
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(password)) {
|
||||
errors.push('Password must contain at least one number');
|
||||
}
|
||||
|
||||
// Optional: require special characters
|
||||
// if (!/[^A-Za-z0-9]/.test(password)) {
|
||||
// errors.push('Password must contain at least one special character');
|
||||
// }
|
||||
|
||||
// Check for common weak patterns
|
||||
const commonPatterns = [
|
||||
/(.)\1{2,}/i, // Three or more consecutive identical characters
|
||||
/123456|654321|abcdef|qwerty|password|admin|login/i, // Common weak passwords
|
||||
];
|
||||
|
||||
for (const pattern of commonPatterns) {
|
||||
if (pattern.test(password)) {
|
||||
errors.push('Password contains common patterns that make it weak');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize cleanup interval (runs every 5 minutes)
|
||||
if (typeof setInterval !== 'undefined') {
|
||||
setInterval(cleanupOldEntries, 5 * 60 * 1000);
|
||||
|
|
|
|||
Loading…
Reference in New Issue