feat: Oracle IAM OIDC, force password change, security hardening

- OIDC: authorization code flow, JWKS validation, JIT provisioning, group-to-role mapping, dual auth (local/oidc/both)
- Force password change: random admin password on first install, modal blocks UI until changed
- APP_SECRET required in docker-compose (fail if missing), .env.example improved
- Login page: Oracle IAM button (conditional), hide form in oidc-only mode
- OracleIamPage: test connection via backend, masked client_secret handling
- UsersPage: OIDC badge, auth_provider in list
- i18n: login.oidcButton, login.or (pt/en/es)
- README v3.1: Terminal, User Management, Security, OIDC endpoints, versioning
This commit is contained in:
nogueiraguh
2026-04-02 10:26:00 -03:00
parent 60596e790f
commit f22bb54e25
13 changed files with 514 additions and 106 deletions

View File

@@ -1,9 +1,10 @@
import { useEffect, lazy, Suspense } from 'react';
import { useEffect, useState, 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 AppShell from '@/components/layout/AppShell';
import { Loader2 } from 'lucide-react';
import { Loader2, Lock } from 'lucide-react';
// Lazy-loaded pages (code splitting)
const LoginPage = lazy(() => import('@/pages/LoginPage'));
@@ -34,10 +35,72 @@ function PageLoader() {
);
}
function ForcePasswordModal() {
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 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; }
setLoading(true);
try {
await authApi.changePassword(curPw, newPw);
clearMustChangePassword();
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro ao alterar senha');
} finally {
setLoading(false);
}
};
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>
<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>
</div>
</div>
);
}
export default function App() {
const checkAuth = useAuthStore((s) => s.checkAuth);
const loadData = useAppStore((s) => s.loadData);
const user = useAuthStore((s) => s.user);
const mustChangePassword = useAuthStore((s) => s.mustChangePassword);
useEffect(() => {
checkAuth().then(() => {
@@ -51,6 +114,7 @@ export default function App() {
return (
<Suspense fallback={<PageLoader />}>
{user && mustChangePassword && <ForcePasswordModal />}
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<AppShell />}>