diff --git a/backend/app.py b/backend/app.py index 9bde731..80e1593 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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 ─────────────────────────────────────────────────────────── diff --git a/frontend-react/src/App.tsx b/frontend-react/src/App.tsx index 2629fb2..5f30d7d 100644 --- a/frontend-react/src/App.tsx +++ b/frontend-react/src/App.tsx @@ -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 (
+ {t('pw.welcomeUser') || 'Logged in as'} {user?.username} +
++ {t('pw.welcomeMsg') || 'For security, you must change your password before continuing.'} +
+- Por seguranca, altere sua senha antes de continuar. -
-+ {t('pw.successMsg') || 'Redirecting...'} +
+