|
|
|
|
@@ -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="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 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>
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
|
|
|
<input type="password" placeholder="Senha atual" 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)}
|
|
|
|
|
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)}
|
|
|
|
|
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>
|
|
|
|
|
)}
|
|
|
|
|
<button type="submit" disabled={loading}
|
|
|
|
|
className="w-full py-2.5 rounded-lg text-sm font-semibold text-white transition-colors disabled:opacity-60"
|
|
|
|
|
style={{ background: 'var(--ac)' }}>
|
|
|
|
|
{loading ? 'Salvando...' : 'Alterar Senha'}
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{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)' }}>
|
|
|
|
|
{t('pw.changeTitle') || 'Change Password'}
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
|
|
|
<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 />
|
|
|
|
|
</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)' }} />
|
|
|
|
|
</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)' }} />
|
|
|
|
|
</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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{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 ? (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>
|
|
|
|
|
);
|
|
|
|
|
|