feat: password change 3-step modal with strength meter, default admin/admin123
This commit is contained in:
@@ -543,13 +543,11 @@ def init_db():
|
||||
log.info(f"Recovered stuck workspace {ws['id']}: {ws['status']} -> {prev}")
|
||||
adm = c.execute("SELECT id FROM users WHERE username='admin'").fetchone()
|
||||
if not adm:
|
||||
_tmp_pw = secrets.token_urlsafe(16)
|
||||
c.execute(
|
||||
"INSERT INTO users (id,first_name,last_name,username,email,password_hash,role,must_change_password) VALUES (?,?,?,?,?,?,?,?)",
|
||||
(str(uuid.uuid4()), "Admin", "Sistema", "admin", "admin@local", _hash_pw(_tmp_pw), "admin", 1)
|
||||
(str(uuid.uuid4()), "Admin", "Sistema", "admin", "admin@local", _hash_pw("admin123"), "admin", 1)
|
||||
)
|
||||
log.info(f"Default admin created — username: admin / password: {_tmp_pw}")
|
||||
log.info("⚠ Change the admin password on first login!")
|
||||
log.info("Default admin created — username: admin / password: admin123")
|
||||
|
||||
# ── Crypto helpers ────────────────────────────────────────────────────────────
|
||||
def _hash_pw(pw): salt=secrets.token_hex(16); h=hashlib.pbkdf2_hmac("sha256",pw.encode(),salt.encode(),100_000); return f"{salt}:{h.hex()}"
|
||||
@@ -899,7 +897,15 @@ async def change_pw(req: ChangePwReq, u=Depends(current_user)):
|
||||
if u.get("auth_provider") == "oidc":
|
||||
raise HTTPException(400, "Usuários OIDC não podem alterar senha localmente")
|
||||
if not _verify_pw(req.current_password, u["password_hash"]): raise HTTPException(400, "Senha atual incorreta")
|
||||
with db() as c: c.execute("UPDATE users SET password_hash=?, must_change_password=0 WHERE id=?", (_hash_pw(req.new_password), u["id"]))
|
||||
pw = req.new_password
|
||||
import re
|
||||
if len(pw) < 8: raise HTTPException(400, "Senha deve ter no mínimo 8 caracteres")
|
||||
if not re.search(r'[A-Z]', pw): raise HTTPException(400, "Senha deve conter ao menos uma letra maiúscula")
|
||||
if not re.search(r'[a-z]', pw): raise HTTPException(400, "Senha deve conter ao menos uma letra minúscula")
|
||||
if not re.search(r'\d', pw): raise HTTPException(400, "Senha deve conter ao menos um número")
|
||||
if not re.search(r'[^A-Za-z0-9]', pw): raise HTTPException(400, "Senha deve conter ao menos um caractere especial")
|
||||
if pw == req.current_password: raise HTTPException(400, "A nova senha deve ser diferente da atual")
|
||||
with db() as c: c.execute("UPDATE users SET password_hash=?, must_change_password=0 WHERE id=?", (_hash_pw(pw), u["id"]))
|
||||
return {"ok": True}
|
||||
|
||||
# ── OIDC endpoints ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState, lazy, Suspense } from 'react';
|
||||
import { useEffect, useState, useMemo, lazy, Suspense } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { authApi } from '@/api/endpoints/auth';
|
||||
import { useI18n } from '@/i18n';
|
||||
import AppShell from '@/components/layout/AppShell';
|
||||
import { Loader2, Lock } from 'lucide-react';
|
||||
import { Loader2, Lock, CheckCircle, XCircle, Shield } from 'lucide-react';
|
||||
|
||||
// Lazy-loaded pages (code splitting)
|
||||
const LoginPage = lazy(() => import('@/pages/LoginPage'));
|
||||
@@ -36,25 +37,43 @@ function PageLoader() {
|
||||
}
|
||||
|
||||
function ForcePasswordModal() {
|
||||
const { t } = useI18n();
|
||||
const [step, setStep] = useState<'welcome' | 'form' | 'done'>('welcome');
|
||||
const [curPw, setCurPw] = useState('');
|
||||
const [newPw, setNewPw] = useState('');
|
||||
const [confirmPw, setConfirmPw] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const clearMustChangePassword = useAuthStore((s) => s.clearMustChangePassword);
|
||||
const checkAuth = useAuthStore((s) => s.checkAuth);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
|
||||
const rules = useMemo(() => [
|
||||
{ label: t('pw.minLength') || 'Minimum 8 characters', ok: newPw.length >= 8 },
|
||||
{ label: t('pw.hasUpper') || 'One uppercase letter', ok: /[A-Z]/.test(newPw) },
|
||||
{ label: t('pw.hasLower') || 'One lowercase letter', ok: /[a-z]/.test(newPw) },
|
||||
{ label: t('pw.hasNumber') || 'One number', ok: /\d/.test(newPw) },
|
||||
{ label: t('pw.hasSpecial') || 'One special character (!@#$...)', ok: /[^A-Za-z0-9]/.test(newPw) },
|
||||
{ label: t('pw.match') || 'Passwords match', ok: newPw.length > 0 && newPw === confirmPw },
|
||||
], [newPw, confirmPw, t]);
|
||||
|
||||
const allValid = rules.every(r => r.ok) && curPw.length > 0 && newPw !== curPw;
|
||||
const strength = rules.filter(r => r.ok).length;
|
||||
const strengthColor = strength <= 2 ? 'var(--rd)' : strength <= 4 ? 'var(--yl, #e6a817)' : 'var(--gn)';
|
||||
const strengthLabel = strength <= 2 ? (t('pw.weak') || 'Weak') : strength <= 4 ? (t('pw.medium') || 'Medium') : (t('pw.strong') || 'Strong');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (newPw.length < 8) { setError('A nova senha deve ter no minimo 8 caracteres'); return; }
|
||||
if (newPw !== confirmPw) { setError('As senhas nao coincidem'); return; }
|
||||
if (newPw === curPw) { setError('A nova senha deve ser diferente da atual'); return; }
|
||||
if (!allValid) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await authApi.changePassword(curPw, newPw);
|
||||
clearMustChangePassword();
|
||||
setStep('done');
|
||||
setTimeout(async () => {
|
||||
await checkAuth();
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Erro ao alterar senha');
|
||||
setError(err instanceof Error ? err.message : t('pw.error') || 'Error changing password');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -62,35 +81,124 @@ function ForcePasswordModal() {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)' }}>
|
||||
<div className="w-[400px] p-8 rounded-2xl" style={{ background: 'var(--bg1)', boxShadow: 'var(--sh3)', border: '1px solid var(--bd)' }}>
|
||||
<div className="w-[420px] p-8 rounded-2xl" style={{ background: 'var(--bg1)', boxShadow: 'var(--sh3)', border: '1px solid var(--bd)' }}>
|
||||
|
||||
{step === 'welcome' && (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-14 h-14 rounded-xl flex items-center justify-center mb-4" style={{ background: 'var(--acl)' }}>
|
||||
<Shield size={28} style={{ color: 'var(--ac)' }} />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold mb-2" style={{ color: 'var(--t1)' }}>
|
||||
{t('pw.welcomeTitle') || 'Welcome!'}
|
||||
</h2>
|
||||
<p className="text-xs text-center mb-1" style={{ color: 'var(--t2)' }}>
|
||||
{t('pw.welcomeUser') || 'Logged in as'} <strong>{user?.username}</strong>
|
||||
</p>
|
||||
<p className="text-xs text-center mb-6" style={{ color: 'var(--t3)' }}>
|
||||
{t('pw.welcomeMsg') || 'For security, you must change your password before continuing.'}
|
||||
</p>
|
||||
<button onClick={() => setStep('form')}
|
||||
className="w-full py-2.5 rounded-lg text-sm font-semibold text-white transition-colors"
|
||||
style={{ background: 'var(--ac)' }}>
|
||||
{t('pw.continue') || 'Continue'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'form' && (
|
||||
<>
|
||||
<div className="flex flex-col items-center mb-5">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center mb-3" style={{ background: 'var(--rdl)' }}>
|
||||
<Lock size={24} style={{ color: 'var(--rd)' }} />
|
||||
</div>
|
||||
<h2 className="text-base font-bold" style={{ color: 'var(--t1)' }}>Alterar Senha</h2>
|
||||
<p className="text-xs mt-1 text-center" style={{ color: 'var(--t3)' }}>
|
||||
Por seguranca, altere sua senha antes de continuar.
|
||||
</p>
|
||||
<h2 className="text-base font-bold" style={{ color: 'var(--t1)' }}>
|
||||
{t('pw.changeTitle') || 'Change Password'}
|
||||
</h2>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
||||
<input type="password" placeholder="Senha atual" value={curPw} onChange={e => setCurPw(e.target.value)}
|
||||
<div>
|
||||
<label className="text-[.65rem] font-medium mb-1 block" style={{ color: 'var(--t3)' }}>
|
||||
{t('pw.currentPassword') || 'Current password'}
|
||||
</label>
|
||||
<input type="password" value={curPw} onChange={e => setCurPw(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-lg text-sm outline-none"
|
||||
style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)' }} autoFocus />
|
||||
<input type="password" placeholder="Nova senha (min. 8 caracteres)" value={newPw} onChange={e => setNewPw(e.target.value)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[.65rem] font-medium mb-1 block" style={{ color: 'var(--t3)' }}>
|
||||
{t('pw.newPassword') || 'New password'}
|
||||
</label>
|
||||
<input type="password" value={newPw} onChange={e => setNewPw(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-lg text-sm outline-none"
|
||||
style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)' }} />
|
||||
<input type="password" placeholder="Confirmar nova senha" value={confirmPw} onChange={e => setConfirmPw(e.target.value)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[.65rem] font-medium mb-1 block" style={{ color: 'var(--t3)' }}>
|
||||
{t('pw.confirmPassword') || 'Confirm new password'}
|
||||
</label>
|
||||
<input type="password" value={confirmPw} onChange={e => setConfirmPw(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-lg text-sm outline-none"
|
||||
style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)' }} />
|
||||
{error && (
|
||||
<div className="text-xs px-3 py-2 rounded-lg" style={{ background: 'var(--rdl)', color: 'var(--rd)' }}>{error}</div>
|
||||
</div>
|
||||
|
||||
{newPw.length > 0 && (
|
||||
<div className="rounded-lg px-3 py-2.5" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)' }}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[.6rem] font-semibold" style={{ color: 'var(--t3)' }}>
|
||||
{t('pw.strength') || 'Password strength'}
|
||||
</span>
|
||||
<span className="text-[.6rem] font-bold" style={{ color: strengthColor }}>{strengthLabel}</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 rounded-full mb-2.5" style={{ background: 'var(--bg3)' }}>
|
||||
<div className="h-full rounded-full transition-all duration-300"
|
||||
style={{ width: `${(strength / 6) * 100}%`, background: strengthColor }} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{rules.map((r, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
{r.ok
|
||||
? <CheckCircle size={11} style={{ color: 'var(--gn)', flexShrink: 0 }} />
|
||||
: <XCircle size={11} style={{ color: 'var(--t4)', flexShrink: 0 }} />}
|
||||
<span className="text-[.6rem]" style={{ color: r.ok ? 'var(--gn)' : 'var(--t4)' }}>{r.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" disabled={loading}
|
||||
className="w-full py-2.5 rounded-lg text-sm font-semibold text-white transition-colors disabled:opacity-60"
|
||||
|
||||
{newPw.length > 0 && newPw === curPw && (
|
||||
<div className="text-[.65rem] px-3 py-2 rounded-lg" style={{ background: 'var(--rdl)', color: 'var(--rd)' }}>
|
||||
{t('pw.sameAsOld') || 'New password must be different from the current one'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-[.65rem] px-3 py-2 rounded-lg" style={{ background: 'var(--rdl)', color: 'var(--rd)' }}>{error}</div>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={loading || !allValid}
|
||||
className="w-full py-2.5 rounded-lg text-sm font-semibold text-white transition-colors disabled:opacity-40"
|
||||
style={{ background: 'var(--ac)' }}>
|
||||
{loading ? 'Salvando...' : 'Alterar Senha'}
|
||||
{loading ? (t('pw.saving') || 'Saving...') : (t('pw.changeButton') || 'Change Password')}
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 'done' && (
|
||||
<div className="flex flex-col items-center py-4">
|
||||
<div className="w-14 h-14 rounded-xl flex items-center justify-center mb-4" style={{ background: 'var(--gnl)' }}>
|
||||
<CheckCircle size={28} style={{ color: 'var(--gn)' }} />
|
||||
</div>
|
||||
<h2 className="text-base font-bold mb-2" style={{ color: 'var(--t1)' }}>
|
||||
{t('pw.successTitle') || 'Password changed!'}
|
||||
</h2>
|
||||
<p className="text-xs text-center" style={{ color: 'var(--t3)' }}>
|
||||
{t('pw.successMsg') || 'Redirecting...'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,32 @@ const en: Record<string, string> = {
|
||||
'login.or': 'or',
|
||||
'login.oidcButton': 'Sign in with Oracle IAM',
|
||||
|
||||
// ── Password Change ──
|
||||
'pw.welcomeTitle': 'Welcome!',
|
||||
'pw.welcomeUser': 'Logged in as',
|
||||
'pw.welcomeMsg': 'For security, you must change your password before continuing.',
|
||||
'pw.continue': 'Continue',
|
||||
'pw.changeTitle': 'Change Password',
|
||||
'pw.currentPassword': 'Current password',
|
||||
'pw.newPassword': 'New password',
|
||||
'pw.confirmPassword': 'Confirm new password',
|
||||
'pw.strength': 'Password strength',
|
||||
'pw.weak': 'Weak',
|
||||
'pw.medium': 'Medium',
|
||||
'pw.strong': 'Strong',
|
||||
'pw.minLength': 'Minimum 8 characters',
|
||||
'pw.hasUpper': 'One uppercase letter',
|
||||
'pw.hasLower': 'One lowercase letter',
|
||||
'pw.hasNumber': 'One number',
|
||||
'pw.hasSpecial': 'One special character (!@#$...)',
|
||||
'pw.match': 'Passwords match',
|
||||
'pw.sameAsOld': 'New password must be different from the current one',
|
||||
'pw.error': 'Error changing password',
|
||||
'pw.saving': 'Saving...',
|
||||
'pw.changeButton': 'Change Password',
|
||||
'pw.successTitle': 'Password changed!',
|
||||
'pw.successMsg': 'Redirecting...',
|
||||
|
||||
// ── Chat ──
|
||||
'chat.timeNow': 'now',
|
||||
'chat.today': 'Today',
|
||||
|
||||
@@ -48,6 +48,32 @@ const es: Record<string, string> = {
|
||||
'login.or': 'o',
|
||||
'login.oidcButton': 'Iniciar sesion con Oracle IAM',
|
||||
|
||||
// ── Password Change ──
|
||||
'pw.welcomeTitle': 'Bienvenido!',
|
||||
'pw.welcomeUser': 'Conectado como',
|
||||
'pw.welcomeMsg': 'Por seguridad, cambie su contrasena antes de continuar.',
|
||||
'pw.continue': 'Continuar',
|
||||
'pw.changeTitle': 'Cambiar Contrasena',
|
||||
'pw.currentPassword': 'Contrasena actual',
|
||||
'pw.newPassword': 'Nueva contrasena',
|
||||
'pw.confirmPassword': 'Confirmar nueva contrasena',
|
||||
'pw.strength': 'Fuerza de la contrasena',
|
||||
'pw.weak': 'Debil',
|
||||
'pw.medium': 'Media',
|
||||
'pw.strong': 'Fuerte',
|
||||
'pw.minLength': 'Minimo 8 caracteres',
|
||||
'pw.hasUpper': 'Una letra mayuscula',
|
||||
'pw.hasLower': 'Una letra minuscula',
|
||||
'pw.hasNumber': 'Un numero',
|
||||
'pw.hasSpecial': 'Un caracter especial (!@#$...)',
|
||||
'pw.match': 'Las contrasenas coinciden',
|
||||
'pw.sameAsOld': 'La nueva contrasena debe ser diferente de la actual',
|
||||
'pw.error': 'Error al cambiar contrasena',
|
||||
'pw.saving': 'Guardando...',
|
||||
'pw.changeButton': 'Cambiar Contrasena',
|
||||
'pw.successTitle': 'Contrasena cambiada!',
|
||||
'pw.successMsg': 'Redirigiendo...',
|
||||
|
||||
// ── Chat ──
|
||||
'chat.timeNow': 'ahora',
|
||||
'chat.today': 'Hoy',
|
||||
|
||||
@@ -48,6 +48,32 @@ const pt: Record<string, string> = {
|
||||
'login.or': 'ou',
|
||||
'login.oidcButton': 'Login com Oracle IAM',
|
||||
|
||||
// ── Password Change ──
|
||||
'pw.welcomeTitle': 'Bem-vindo!',
|
||||
'pw.welcomeUser': 'Conectado como',
|
||||
'pw.welcomeMsg': 'Por seguranca, altere sua senha antes de continuar.',
|
||||
'pw.continue': 'Continuar',
|
||||
'pw.changeTitle': 'Alterar Senha',
|
||||
'pw.currentPassword': 'Senha atual',
|
||||
'pw.newPassword': 'Nova senha',
|
||||
'pw.confirmPassword': 'Confirmar nova senha',
|
||||
'pw.strength': 'Forca da senha',
|
||||
'pw.weak': 'Fraca',
|
||||
'pw.medium': 'Media',
|
||||
'pw.strong': 'Forte',
|
||||
'pw.minLength': 'Minimo 8 caracteres',
|
||||
'pw.hasUpper': 'Uma letra maiuscula',
|
||||
'pw.hasLower': 'Uma letra minuscula',
|
||||
'pw.hasNumber': 'Um numero',
|
||||
'pw.hasSpecial': 'Um caractere especial (!@#$...)',
|
||||
'pw.match': 'Senhas coincidem',
|
||||
'pw.sameAsOld': 'A nova senha deve ser diferente da atual',
|
||||
'pw.error': 'Erro ao alterar senha',
|
||||
'pw.saving': 'Salvando...',
|
||||
'pw.changeButton': 'Alterar Senha',
|
||||
'pw.successTitle': 'Senha alterada!',
|
||||
'pw.successMsg': 'Redirecionando...',
|
||||
|
||||
// ── Chat ──
|
||||
'chat.timeNow': 'agora',
|
||||
'chat.today': 'Hoje',
|
||||
|
||||
Reference in New Issue
Block a user