-
Notifications
You must be signed in to change notification settings - Fork 65
Feature force 2fa #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature force 2fa #28
Conversation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements a comprehensive first login password change and 2FA setup flow to enhance security. When users log in for the first time, they are required to change their default password and optionally set up two-factor authentication before accessing the system.
- Added first-time login detection with
isFirstLoginflag and mandatory password change - Implemented 2FA setup flow with QR code generation and backup codes
- Enhanced password validation to require uppercase, lowercase, numbers, and special characters
Reviewed Changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/web/src/services/auth.service.ts | Added first login password change endpoint and extended LoginResponse interface |
| apps/web/src/components/pages/Users.tsx | Enhanced password reset dialog with secure copy functionality and improved UX |
| apps/web/src/components/pages/Login.tsx | Updated login flow to handle first-time login and 2FA setup steps |
| apps/web/src/components/pages/ForcePasswordChange.tsx | New component for mandatory password change with strength validation |
| apps/web/src/components/pages/Force2FASetup.tsx | New component for 2FA setup with QR code and backup codes |
| apps/web/src/components/pages/Account.tsx | Updated password validation to match new security requirements |
| apps/web/src/auth.tsx | Extended LoginResponse interface for first login handling |
| apps/api/src/utils/password.ts | Added secure password generation utility |
| apps/api/src/utils/jwt.ts | Added temporary token generation for first login flow |
| apps/api/src/domains/users/users.service.ts | Updated to use secure password generation |
| apps/api/src/domains/auth/ | Added first login password change endpoint, validation, and service logic |
| apps/api/src/domains/account/account.validation.ts | Enhanced password validation rules |
| apps/api/prisma/ | Added isFirstLogin flag to User model with migration |
Comments suppressed due to low confidence (1)
apps/web/src/components/pages/Users.tsx:1
- The
useTranslationimport is removed but the translation hooktis still being used in the component. This will cause undefined variable errors.
import { useState } from "react";
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| AlertDialogTitle, | ||
| } from '@/components/ui/alert-dialog'; | ||
| import { toast } from 'sonner'; | ||
| import { accountService } from '@/services/auth.service'; |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import statement references accountService but the auth.service.ts file exports authService. This will cause a runtime error when trying to call setup2FA and enable2FA methods.
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; | ||
| import { Switch } from "@/components/ui/switch"; | ||
| import { UserPlus, Mail, Key, Trash2, Edit, Shield, Loader2, Users as UsersIcon } from "lucide-react"; | ||
| import { UserPlus, Key, Trash2, Edit, Shield, Loader2, Users as UsersIcon, Copy, CheckCircle2, AlertTriangle } from "lucide-react"; |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Mail icon import was removed but may still be used elsewhere in the component, potentially causing undefined variable errors.
| import { UserPlus, Key, Trash2, Edit, Shield, Loader2, Users as UsersIcon, Copy, CheckCircle2, AlertTriangle } from "lucide-react"; | |
| import { UserPlus, Key, Trash2, Edit, Shield, Loader2, Users as UsersIcon, Copy, CheckCircle2, AlertTriangle, Mail } from "lucide-react"; |
| const currentUserId = resetPasswordDialog.userId; | ||
| const currentUsername = resetPasswordDialog.username; | ||
|
|
||
| // Close dialog first | ||
| setResetPasswordDialog({ isOpen: false, userId: '', username: '' }); | ||
|
|
||
| // Then reopen with password after a tiny delay | ||
| setTimeout(() => { | ||
| setResetPasswordDialog({ | ||
| isOpen: true, | ||
| userId: currentUserId, | ||
| username: currentUsername, | ||
| newPassword: newPassword | ||
| }); | ||
| setPasswordCopied(false); | ||
| }, 100); |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach of closing and reopening the dialog with a setTimeout to force re-render is fragile and could cause race conditions. Consider using a proper state update pattern or separate state for the password result.
| const textArea = document.createElement('textarea'); | ||
| textArea.value = password; | ||
|
|
||
| // Make it visible but off-screen to ensure it works | ||
| textArea.style.position = 'fixed'; | ||
| textArea.style.top = '0'; | ||
| textArea.style.left = '-9999px'; | ||
| textArea.style.opacity = '0'; | ||
| textArea.setAttribute('readonly', ''); | ||
| textArea.contentEditable = 'true'; | ||
|
|
||
| document.body.appendChild(textArea); | ||
|
|
||
| // iOS Safari needs this | ||
| if (navigator.userAgent.match(/ipad|iphone/i)) { | ||
| const range = document.createRange(); | ||
| range.selectNodeContents(textArea); | ||
| const selection = window.getSelection(); | ||
| selection?.removeAllRanges(); | ||
| selection?.addRange(range); | ||
| textArea.setSelectionRange(0, password.length); | ||
| } else { | ||
| textArea.focus(); | ||
| textArea.select(); | ||
| textArea.setSelectionRange(0, password.length); | ||
| } | ||
|
|
||
| // Small delay before executing copy | ||
| await new Promise(resolve => setTimeout(resolve, 50)); | ||
|
|
||
| const successful = document.execCommand('copy'); | ||
|
|
||
| if (successful) { | ||
| copySuccess = true; | ||
| copyMethod = 'execCommand'; | ||
|
|
||
| // Keep element for a bit to ensure clipboard is populated | ||
| await new Promise(resolve => setTimeout(resolve, 100)); | ||
| } | ||
|
|
||
| document.body.removeChild(textArea); | ||
| } catch (execError) { |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using arbitrary delays in copy functionality is unreliable across different browsers and devices. Consider using more robust clipboard detection or event-based approaches.
| const textArea = document.createElement('textarea'); | |
| textArea.value = password; | |
| // Make it visible but off-screen to ensure it works | |
| textArea.style.position = 'fixed'; | |
| textArea.style.top = '0'; | |
| textArea.style.left = '-9999px'; | |
| textArea.style.opacity = '0'; | |
| textArea.setAttribute('readonly', ''); | |
| textArea.contentEditable = 'true'; | |
| document.body.appendChild(textArea); | |
| // iOS Safari needs this | |
| if (navigator.userAgent.match(/ipad|iphone/i)) { | |
| const range = document.createRange(); | |
| range.selectNodeContents(textArea); | |
| const selection = window.getSelection(); | |
| selection?.removeAllRanges(); | |
| selection?.addRange(range); | |
| textArea.setSelectionRange(0, password.length); | |
| } else { | |
| textArea.focus(); | |
| textArea.select(); | |
| textArea.setSelectionRange(0, password.length); | |
| } | |
| // Small delay before executing copy | |
| await new Promise(resolve => setTimeout(resolve, 50)); | |
| const successful = document.execCommand('copy'); | |
| if (successful) { | |
| copySuccess = true; | |
| copyMethod = 'execCommand'; | |
| // Keep element for a bit to ensure clipboard is populated | |
| await new Promise(resolve => setTimeout(resolve, 100)); | |
| } | |
| document.body.removeChild(textArea); | |
| } catch (execError) { | |
| // Prefer modern Clipboard API if available | |
| if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { | |
| await navigator.clipboard.writeText(password); | |
| copySuccess = true; | |
| copyMethod = 'clipboard.writeText'; | |
| } else { | |
| const textArea = document.createElement('textarea'); | |
| textArea.value = password; | |
| // Make it visible but off-screen to ensure it works | |
| textArea.style.position = 'fixed'; | |
| textArea.style.top = '0'; | |
| textArea.style.left = '-9999px'; | |
| textArea.style.opacity = '0'; | |
| textArea.setAttribute('readonly', ''); | |
| textArea.contentEditable = 'true'; | |
| document.body.appendChild(textArea); | |
| // iOS Safari needs this | |
| if (navigator.userAgent.match(/ipad|iphone/i)) { | |
| const range = document.createRange(); | |
| range.selectNodeContents(textArea); | |
| const selection = window.getSelection(); | |
| selection?.removeAllRanges(); | |
| selection?.addRange(range); | |
| textArea.setSelectionRange(0, password.length); | |
| } else { | |
| textArea.focus(); | |
| textArea.select(); | |
| textArea.setSelectionRange(0, password.length); | |
| } | |
| // No arbitrary delay; execute copy immediately | |
| const successful = document.execCommand('copy'); | |
| if (successful) { | |
| copySuccess = true; | |
| copyMethod = 'execCommand'; | |
| } | |
| document.body.removeChild(textArea); | |
| } |
| return jwt.sign({ userId, type: 'temp' }, config.jwt.accessSecret, { | ||
| expiresIn: '15m', // Temporary token valid for 15 minutes | ||
| } as any); |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using as any to bypass TypeScript checking is not recommended. Define proper types for the JWT sign options or use the correct interface from the jwt library.
| const result = await this.completeLogin(user, metadata, false); | ||
|
|
||
| // Add flag to indicate if 2FA setup is needed | ||
| return { | ||
| ...result, | ||
| require2FASetup: !user.twoFactor?.enabled, | ||
| }; |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The require2FASetup flag is calculated based on the user state before password change, but after password change the user relationship might have changed. Consider fetching fresh user data or ensuring this flag is accurate.
| // Return successful login response (LoginResult type) | ||
| if ('accessToken' in result && 'refreshToken' in result) { |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using type guards with string property checks is fragile and doesn't provide type safety. Consider using discriminated unions with explicit type fields or proper type narrowing.



feat: Implement first login password change and 2FA setup flow
isFirstLoginflag to User model