From 62f2a3d1aba2d73dbfbc22ebcb51f0b1d6a81fe8 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Wed, 1 Apr 2026 15:20:16 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20My=20Settings=20=E2=80=94=20password=20?= =?UTF-8?q?change=20(local=20only),=20timezone,=20MFA,=20auth=5Fprovider?= =?UTF-8?q?=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MySettingsPage: timezone, password change (local users), MFA setup/disable - Password change hidden for federated users (auth_provider != local) - Federated users see info message: "managed by Oracle IAM" - Backend: auth_provider + oidc_subject columns in users table (migration) - /users/me returns auth_provider field - User interface updated with auth_provider - i18n: 12 new keys (pt/en) for password change --- backend/app.py | 5 +- frontend-react/src/api/endpoints/auth.ts | 1 + frontend-react/src/i18n/en.ts | 11 + frontend-react/src/i18n/pt.ts | 11 + .../src/pages/admin/MySettingsPage.tsx | 249 +++++++++++++----- 5 files changed, 207 insertions(+), 70 deletions(-) diff --git a/backend/app.py b/backend/app.py index 962e541..f8e2af8 100644 --- a/backend/app.py +++ b/backend/app.py @@ -465,7 +465,7 @@ def init_db(): c.execute(f"ALTER TABLE oci_configs ADD COLUMN {col}") except sqlite3.OperationalError: pass - for col in ["timezone TEXT DEFAULT 'America/Sao_Paulo'"]: + for col in ["timezone TEXT DEFAULT 'America/Sao_Paulo'", "auth_provider TEXT DEFAULT 'local'", "oidc_subject TEXT"]: try: c.execute(f"ALTER TABLE users ADD COLUMN {col}") except sqlite3.OperationalError: @@ -835,7 +835,8 @@ async def list_users(u=Depends(require("admin"))): @app.get("/api/users/me") async def me(u=Depends(current_user)): - return {k: u[k] for k in ("id","first_name","last_name","username","email","role","mfa_enabled","timezone")} + fields = ("id","first_name","last_name","username","email","role","mfa_enabled","timezone","auth_provider") + return {k: u.get(k, "local" if k == "auth_provider" else None) for k in fields} @app.put("/api/users/{uid}") async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin"))): diff --git a/frontend-react/src/api/endpoints/auth.ts b/frontend-react/src/api/endpoints/auth.ts index fd6131f..896ed03 100644 --- a/frontend-react/src/api/endpoints/auth.ts +++ b/frontend-react/src/api/endpoints/auth.ts @@ -9,6 +9,7 @@ export interface User { email?: string; mfa_enabled?: boolean; timezone?: string; + auth_provider?: string; } export interface LoginResponse { diff --git a/frontend-react/src/i18n/en.ts b/frontend-react/src/i18n/en.ts index 5f95f32..b80b7df 100644 --- a/frontend-react/src/i18n/en.ts +++ b/frontend-react/src/i18n/en.ts @@ -755,6 +755,17 @@ const en: Record = { 'settings.timezone': 'Timezone', 'settings.tzDesc': 'Set your timezone for date and time display.', 'settings.tzChanged': 'Timezone changed successfully', + 'settings.changePassword': 'Change Password', + 'settings.currentPw': 'Current Password', + 'settings.newPw': 'New Password', + 'settings.confirmPw': 'Confirm New Password', + 'settings.savePw': 'Change Password', + 'settings.saving': 'Saving...', + 'settings.pwChanged': 'Password changed successfully', + 'settings.pwRequired': 'Fill in all fields', + 'settings.pwMismatch': 'Passwords do not match', + 'settings.pwMinLength': 'New password must be at least 8 characters', + 'settings.federatedInfo': 'Password and MFA are managed by Oracle IAM Identity Domain. Configure directly in the OCI portal.', 'settings.mfa': 'Multi-Factor Authentication (MFA)', // ── Oracle IAM ── 'iam.title': 'Oracle IAM Identity Domains', diff --git a/frontend-react/src/i18n/pt.ts b/frontend-react/src/i18n/pt.ts index dea4ba0..f78954a 100644 --- a/frontend-react/src/i18n/pt.ts +++ b/frontend-react/src/i18n/pt.ts @@ -756,6 +756,17 @@ const pt: Record = { 'settings.timezone': 'Timezone', 'settings.tzDesc': 'Define o fuso horário para exibição de datas e horários.', 'settings.tzChanged': 'Timezone alterado com sucesso', + 'settings.changePassword': 'Alterar Senha', + 'settings.currentPw': 'Senha Atual', + 'settings.newPw': 'Nova Senha', + 'settings.confirmPw': 'Confirmar Nova Senha', + 'settings.savePw': 'Alterar Senha', + 'settings.saving': 'Salvando...', + 'settings.pwChanged': 'Senha alterada com sucesso', + 'settings.pwRequired': 'Preencha todos os campos', + 'settings.pwMismatch': 'As senhas não conferem', + 'settings.pwMinLength': 'A nova senha deve ter pelo menos 8 caracteres', + 'settings.federatedInfo': 'Senha e MFA são gerenciados pelo Oracle IAM Identity Domain. Configure diretamente no portal OCI.', 'settings.mfa': 'Autenticação Multi-Fator (MFA)', // ── Oracle IAM ── 'iam.title': 'Oracle IAM Identity Domains', diff --git a/frontend-react/src/pages/admin/MySettingsPage.tsx b/frontend-react/src/pages/admin/MySettingsPage.tsx index 2832e8d..479102b 100644 --- a/frontend-react/src/pages/admin/MySettingsPage.tsx +++ b/frontend-react/src/pages/admin/MySettingsPage.tsx @@ -1,5 +1,8 @@ import { useState, useEffect } from 'react'; -import { Settings, Clock, ShieldCheck, ShieldOff, KeyRound, Lock, Loader2, CheckCircle, AlertTriangle, X } from 'lucide-react'; +import { + Settings, Clock, ShieldCheck, ShieldOff, KeyRound, Lock, Loader2, + CheckCircle, AlertTriangle, Eye, EyeOff, Save, +} from 'lucide-react'; import client from '@/api/client'; import { adminApi } from '@/api/endpoints/admin'; import { useAuthStore } from '@/stores/auth'; @@ -10,11 +13,20 @@ export default function MySettingsPage() { const { user: currentUser, checkAuth } = useAuthStore(); const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null); + const isLocal = !currentUser?.auth_provider || currentUser.auth_provider === 'local'; + // Timezone const [timezone, setTimezone] = useState(''); const [tzOptions, setTzOptions] = useState([]); const [tzSaving, setTzSaving] = useState(false); + // Password + const [currentPw, setCurrentPw] = useState(''); + const [newPw, setNewPw] = useState(''); + const [confirmPw, setConfirmPw] = useState(''); + const [showPw, setShowPw] = useState(false); + const [pwSaving, setPwSaving] = useState(false); + // MFA const [mfaSecret, setMfaSecret] = useState(null); const [mfaTotpUri, setMfaTotpUri] = useState(null); @@ -27,9 +39,10 @@ export default function MySettingsPage() { }, []); useEffect(() => { - if (msg) { const t = setTimeout(() => setMsg(null), 4000); return () => clearTimeout(t); } + if (msg) { const timer = setTimeout(() => setMsg(null), 4000); return () => clearTimeout(timer); } }, [msg]); + /* ── Timezone ── */ const handleTzChange = async (tz: string) => { setTzSaving(true); try { @@ -43,6 +56,24 @@ export default function MySettingsPage() { } }; + /* ── Password ── */ + const handleChangePw = async () => { + if (!currentPw || !newPw) { setMsg({ text: t('settings.pwRequired') || 'Preencha todos os campos', type: 'error' }); return; } + if (newPw !== confirmPw) { setMsg({ text: t('settings.pwMismatch') || 'As senhas não conferem', type: 'error' }); return; } + if (newPw.length < 8) { setMsg({ text: t('settings.pwMinLength') || 'A nova senha deve ter pelo menos 8 caracteres', type: 'error' }); return; } + setPwSaving(true); + try { + await client.post('/auth/change-password', { current_password: currentPw, new_password: newPw }); + setMsg({ text: t('settings.pwChanged') || 'Senha alterada com sucesso', type: 'success' }); + setCurrentPw(''); setNewPw(''); setConfirmPw(''); + } catch (err) { + setMsg({ text: err instanceof Error ? err.message : 'Erro ao alterar senha', type: 'error' }); + } finally { + setPwSaving(false); + } + }; + + /* ── MFA ── */ const handleMfaSetup = async () => { setMfaLoading(true); try { @@ -104,13 +135,21 @@ export default function MySettingsPage() { )} + {/* Header */}

{t('settings.title') || 'Minhas Configurações'}

-
{currentUser?.username} — {currentUser?.role}
+
+ {currentUser?.first_name} {currentUser?.last_name} — {currentUser?.username} + {!isLocal && ( + + ORACLE IAM + + )} +
@@ -138,79 +177,153 @@ export default function MySettingsPage() { - {/* MFA */} -
-
-
- {mfaOn ? : } -
-

{t('settings.mfa') || 'Autenticação Multi-Fator (MFA)'}

-
- - {mfaOn ? t('usr.active') || 'Ativo' : t('usr.inactive') || 'Inativo'} - -
- - {mfaOn ? ( -
-
- + {/* Password Change — only for local users */} + {isLocal && ( +
+
+
+
-

{t('mfa.enabledDesc')}

- +

{t('settings.changePassword') || 'Alterar Senha'}

- ) : mfaSecret && mfaTotpUri ? ( -
-
-
- QR Code + +
+
+ +
+ setCurrentPw(e.target.value)} + placeholder="••••••••" + className="flex-1" + style={{ fontFamily: 'var(--fm)' }} + /> +
-
- SECRET - {mfaSecret} + +
+ + setNewPw(e.target.value)} + placeholder="••••••••" + style={{ fontFamily: 'var(--fm)' }} + />
-

- {t('mfa.scanQr') || 'Escaneie o QR code com seu app autenticador e insira o código de 6 dígitos.'} -

-
- setMfaCode(e.target.value.replace(/\D/g, '').slice(0, 6))} - placeholder="000000" maxLength={6} autoFocus - className="px-3 py-2 rounded-lg text-sm text-center tracking-[.3em] font-semibold outline-none" - style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fm)', width: 160 }} - onKeyDown={e => { if (e.key === 'Enter') handleMfaVerify(); }} /> - + +
+ + setConfirmPw(e.target.value)} + placeholder="••••••••" + style={{ fontFamily: 'var(--fm)' }} + onKeyDown={e => { if (e.key === 'Enter') handleChangePw(); }} + />
-
- ) : ( -
-
- -
-

- {t('mfa.generateHint')} -

-
- )} -
+
+ )} + + {/* MFA — only for local users */} + {isLocal && ( +
+
+
+ {mfaOn ? : } +
+

{t('settings.mfa') || 'Autenticação Multi-Fator (MFA)'}

+
+ + {mfaOn ? t('usr.active') || 'Ativo' : t('usr.inactive') || 'Inativo'} + +
+ + {mfaOn ? ( +
+
+ +
+

{t('mfa.enabledDesc')}

+ +
+ ) : mfaSecret && mfaTotpUri ? ( +
+
+
+ QR Code +
+
+
+ SECRET + {mfaSecret} +
+

+ {t('mfa.scanQr') || 'Escaneie o QR code com seu app autenticador e insira o código de 6 dígitos.'} +

+
+ setMfaCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + placeholder="000000" maxLength={6} autoFocus + className="px-3 py-2 rounded-lg text-sm text-center tracking-[.3em] font-semibold outline-none" + style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fm)', width: 160 }} + onKeyDown={e => { if (e.key === 'Enter') handleMfaVerify(); }} /> + +
+
+ ) : ( +
+
+ +
+

+ {t('mfa.generateHint')} +

+ +
+ )} +
+ )} + + {/* Info for federated users */} + {!isLocal && ( +
+
+ +

+ {t('settings.federatedInfo') || 'Senha e MFA são gerenciados pelo Oracle IAM Identity Domain. Configure diretamente no portal OCI.'} +

+
+
+ )}
); }