diff --git a/pages/auth/setup-password.vue b/pages/auth/setup-password.vue new file mode 100644 index 0000000..27c7ef8 --- /dev/null +++ b/pages/auth/setup-password.vue @@ -0,0 +1,410 @@ + + + + + diff --git a/pages/auth/verify-success.vue b/pages/auth/verify-success.vue index 470dfb0..31b2e5b 100644 --- a/pages/auth/verify-success.vue +++ b/pages/auth/verify-success.vue @@ -49,26 +49,13 @@ size="large" variant="elevated" block - :href="setupPasswordUrl" - target="_blank" + @click="goToPasswordSetup" class="text-none" > - mdi-lock + mdi-lock-plus Set Your Password - - mdi-login - Log In to Portal - - { + navigateTo({ + path: '/auth/setup-password', + query: { + email: email.value + } + }); +}; + // Apply mobile Safari fixes and track verification onMounted(() => { // Apply mobile Safari fixes diff --git a/server/api/auth/setup-password.post.ts b/server/api/auth/setup-password.post.ts new file mode 100644 index 0000000..8dcc1c3 --- /dev/null +++ b/server/api/auth/setup-password.post.ts @@ -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; + } +}); diff --git a/server/utils/security.ts b/server/utils/security.ts index 8d47a6f..04c2722 100644 --- a/server/utils/security.ts +++ b/server/utils/security.ts @@ -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);