feat: Phase 4 - password validation, force password reset flow, account lockout, QR code for MFA
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 6s
CI Pipeline / Clippy Lints (push) Successful in 46s
CI Pipeline / Rust Unit Tests (push) Successful in 1m2s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 10s
CI Pipeline / Build .deb & Release (push) Has been skipped
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 6s
CI Pipeline / Clippy Lints (push) Successful in 46s
CI Pipeline / Rust Unit Tests (push) Successful in 1m2s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 10s
CI Pipeline / Build .deb & Release (push) Has been skipped
This commit is contained in:
@ -4,6 +4,7 @@ import {
|
||||
Box, Button, Card, CardContent, Chip, Container, Dialog,
|
||||
DialogActions, DialogContent, DialogTitle, Snackbar,
|
||||
Alert, TextField, Typography, InputAdornment, IconButton,
|
||||
List, ListItem, ListItemIcon, ListItemText,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Person as PersonIcon,
|
||||
@ -11,11 +12,27 @@ import {
|
||||
Visibility, VisibilityOff,
|
||||
VpnKey as MfaIcon,
|
||||
Save as SaveIcon,
|
||||
Check as CheckIcon, Close as CloseIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { usersApi } from '../api/client'
|
||||
import type { User } from '../types'
|
||||
|
||||
/** Password strength checker */
|
||||
function checkPasswordStrength(password: string) {
|
||||
return {
|
||||
length: password.length >= 8,
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
lowercase: /[a-z]/.test(password),
|
||||
digit: /[0-9]/.test(password),
|
||||
special: /[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password),
|
||||
}
|
||||
}
|
||||
|
||||
function isPasswordValid(checks: ReturnType<typeof checkPasswordStrength>) {
|
||||
return checks.length && checks.uppercase && checks.lowercase && checks.digit && checks.special
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const navigate = useNavigate()
|
||||
const { user, setUser } = useAuthStore()
|
||||
@ -49,6 +66,10 @@ export default function ProfilePage() {
|
||||
const showSnack = (severity: 'success' | 'error', message: string) =>
|
||||
setSnack({ open: true, severity, message })
|
||||
|
||||
const pwChecks = checkPasswordStrength(newPw)
|
||||
const pwValid = isPasswordValid(pwChecks)
|
||||
const pwMismatch = !!(newPw && confirmPw && newPw !== confirmPw)
|
||||
|
||||
// ── Load current user on mount ──────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
@ -87,6 +108,10 @@ export default function ProfilePage() {
|
||||
showSnack('error', 'New passwords do not match')
|
||||
return
|
||||
}
|
||||
if (!pwValid) {
|
||||
showSnack('error', 'Password does not meet strength requirements')
|
||||
return
|
||||
}
|
||||
setChangingPw(true)
|
||||
try {
|
||||
await usersApi.changePassword({ current_password: currentPw, new_password: newPw })
|
||||
@ -127,8 +152,6 @@ export default function ProfilePage() {
|
||||
)
|
||||
}
|
||||
|
||||
const pwMismatch = !!(newPw && confirmPw && newPw !== confirmPw)
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ mt: 3 }}>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ mb: 3 }}>My Profile</Typography>
|
||||
@ -217,6 +240,42 @@ export default function ProfilePage() {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{newPw && (
|
||||
<Box sx={{ mt: -1, mb: 0 }}>
|
||||
<List dense disablePadding>
|
||||
<ListItem disableGutters sx={{ py: 0 }}>
|
||||
<ListItemIcon sx={{ minWidth: 28 }}>
|
||||
{pwChecks.length ? <CheckIcon color="success" fontSize="small" /> : <CloseIcon color="error" fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="At least 8 characters" primaryTypographyProps={{ variant: 'caption' }} />
|
||||
</ListItem>
|
||||
<ListItem disableGutters sx={{ py: 0 }}>
|
||||
<ListItemIcon sx={{ minWidth: 28 }}>
|
||||
{pwChecks.uppercase ? <CheckIcon color="success" fontSize="small" /> : <CloseIcon color="error" fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="At least one uppercase letter" primaryTypographyProps={{ variant: 'caption' }} />
|
||||
</ListItem>
|
||||
<ListItem disableGutters sx={{ py: 0 }}>
|
||||
<ListItemIcon sx={{ minWidth: 28 }}>
|
||||
{pwChecks.lowercase ? <CheckIcon color="success" fontSize="small" /> : <CloseIcon color="error" fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="At least one lowercase letter" primaryTypographyProps={{ variant: 'caption' }} />
|
||||
</ListItem>
|
||||
<ListItem disableGutters sx={{ py: 0 }}>
|
||||
<ListItemIcon sx={{ minWidth: 28 }}>
|
||||
{pwChecks.digit ? <CheckIcon color="success" fontSize="small" /> : <CloseIcon color="error" fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="At least one digit" primaryTypographyProps={{ variant: 'caption' }} />
|
||||
</ListItem>
|
||||
<ListItem disableGutters sx={{ py: 0 }}>
|
||||
<ListItemIcon sx={{ minWidth: 28 }}>
|
||||
{pwChecks.special ? <CheckIcon color="success" fontSize="small" /> : <CloseIcon color="error" fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="At least one special character" primaryTypographyProps={{ variant: 'caption' }} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
<TextField
|
||||
label="Confirm New Password"
|
||||
type={showConfirmPw ? 'text' : 'password'}
|
||||
@ -239,7 +298,7 @@ export default function ProfilePage() {
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleChangePassword}
|
||||
disabled={changingPw || !currentPw || !newPw || !confirmPw || !!pwMismatch}
|
||||
disabled={changingPw || !currentPw || !newPw || !confirmPw || !!pwMismatch || !pwValid}
|
||||
>
|
||||
{changingPw ? 'Changing…' : 'Change Password'}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user