feat(f6.2): add per-act representative signing and user smtp
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Failing after 1m25s
DH V2 CI / API · typecheck, tests, build (push) Successful in 34s
DH V2 CI / WEB · typecheck, build (push) Successful in 20s
Production dependency audit / API · production dependencies (push) Successful in 9s
Production dependency audit / WEB · production dependencies (push) Successful in 9s
DH V2 CI / Docker / scripts contract (push) Successful in 1m16s

This commit is contained in:
2026-09-14 15:53:55 -03:00
parent 103ecf2fae
commit 7fe59bccd2
46 changed files with 907 additions and 154 deletions
+193
View File
@@ -0,0 +1,193 @@
import { useEffect, useState } from 'react';
import type { FormEvent } from 'react';
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
import { useAuth } from '../auth/AuthContext';
import { initials } from '../lib/format';
import {
getSelfProfile,
getSelfSmtpSettings,
saveSelfSmtpSettings,
testSelfSmtpSettings,
updateSelfProfile,
} from '../lib/myProfileApi';
import type {
UserSmtpMode,
UserSmtpSecurityMode,
UserSmtpSettings,
} from '../lib/myProfileApi';
import type { AdministrativeUserProfile } from '../lib/userProfileApi';
export function MyProfilePage() {
const { user } = useAuth();
const [profile, setProfile] = useState<AdministrativeUserProfile | null>(null);
const [smtp, setSmtp] = useState<UserSmtpSettings | null>(null);
const [mode, setMode] = useState<UserSmtpMode>('SYSTEM');
const [host, setHost] = useState('');
const [port, setPort] = useState(587);
const [securityMode, setSecurityMode] = useState<UserSmtpSecurityMode>('STARTTLS');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [fromName, setFromName] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const applySmtp = (settings: UserSmtpSettings) => {
setSmtp(settings);
setMode(settings.mode);
setHost(settings.custom?.host ?? '');
setPort(settings.custom?.port ?? 587);
setSecurityMode(settings.custom?.securityMode ?? 'STARTTLS');
setUsername(settings.custom?.username ?? '');
setFromName(settings.custom?.fromName ?? '');
setPassword('');
};
const load = async () => {
setLoading(true);
try {
const [loadedProfile, loadedSmtp] = await Promise.all([
getSelfProfile(),
getSelfSmtpSettings(),
]);
setProfile(loadedProfile);
applySmtp(loadedSmtp);
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setLoading(false);
}
};
useEffect(() => { void load(); }, []);
const saveProfile = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!profile) return;
setError(''); setSuccess(''); setSaving('profile');
try {
const form = new FormData(event.currentTarget);
const updated = await updateSelfProfile({
email: String(form.get('email') ?? '').trim(),
phone: String(form.get('phone') ?? '').trim() || null,
jobTitle: String(form.get('jobTitle') ?? '').trim() || null,
});
setProfile(updated);
setSuccess('Perfil actualizado correctamente.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setSaving('');
}
};
const saveSmtp = async () => {
setError(''); setSuccess(''); setSaving('smtp');
try {
const updated = await saveSelfSmtpSettings(mode === 'SYSTEM' ? {
mode: 'SYSTEM',
} : {
mode: 'CUSTOM', host: host.trim(), port, securityMode,
username: username.trim() || null,
password: password || undefined,
fromName: fromName.trim() || null,
enabled: true,
});
applySmtp(updated);
setSuccess(mode === 'SYSTEM' ? 'Usarás el SMTP general.' : 'SMTP propio guardado.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setSaving('');
}
};
const testSmtp = async () => {
setError(''); setSuccess(''); setSaving('test');
try {
const result = await testSelfSmtpSettings();
setSuccess(`Correo de prueba enviado a ${result.recipient}.`);
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setSaving('');
}
};
if (loading) return <LoadingBlock label="Cargando tu perfil…" />;
if (!profile || !smtp) return <Alert>{error || 'No se pudo cargar el perfil.'}</Alert>;
return <section>
<div className="page-heading user-heading">
<div className="profile-title">
<span className="profile-avatar">{initials(profile.firstName, profile.lastName)}</span>
<div>
<span className="eyebrow">MI PERFIL</span>
<h1>{profile.firstName} {profile.lastName}</h1>
<p>@{profile.username} · {profile.jobTitle || 'Función sin informar'}</p>
</div>
</div>
</div>
{error && <Alert>{error}</Alert>}
{success && <Alert type="success">{success}</Alert>}
<div className="detail-grid">
<form className="panel form-panel" onSubmit={saveProfile}>
<div className="panel-heading">
<div><span className="eyebrow">CONTACTO</span><h2>Datos del usuario</h2></div>
</div>
<div className="form-grid">
<label className="field"><span>Nombres y apellidos</span><input value={`${profile.firstName} ${profile.lastName}`} disabled /></label>
<label className="field"><span>Email institucional *</span><input name="email" type="email" defaultValue={profile.email ?? ''} required /></label>
<label className="field"><span>Teléfono</span><input name="phone" type="tel" defaultValue={profile.phone ?? ''} /></label>
<label className="field"><span>Cargo / función</span><input name="jobTitle" defaultValue={profile.jobTitle ?? ''} /></label>
</div>
<div className="form-actions">
<button className="button primary" type="submit" disabled={saving === 'profile'}>
{saving === 'profile' ? 'Guardando…' : 'Guardar perfil'}
</button>
</div>
</form>
<div className="panel form-panel">
<div className="panel-heading">
<div><span className="eyebrow">CORREO SALIENTE</span><h2>SMTP personal</h2></div>
</div>
<p className="muted-copy">Por defecto, tus correos salen mediante el SMTP general. Podés cambiar a una cuenta SMTP propia con un clic.</p>
<div className="choice-grid">
<label className={`choice-card ${mode === 'SYSTEM' ? 'selected' : ''}`}>
<input type="radio" name="smtpMode" checked={mode === 'SYSTEM'} onChange={() => setMode('SYSTEM')} />
<span><strong>Usar SMTP general</strong><small>{smtp.generalConfigured ? 'Configurado y disponible' : 'Todavía no configurado por el administrador'}</small></span>
<span className="icon"></span>
</label>
<label className={`choice-card ${mode === 'CUSTOM' ? 'selected' : ''}`}>
<input type="radio" name="smtpMode" checked={mode === 'CUSTOM'} onChange={() => setMode('CUSTOM')} />
<span><strong>Usar SMTP propio</strong><small>Los envíos saldrán con tu cuenta {profile.email}</small></span>
<span className="icon"></span>
</label>
</div>
{mode === 'CUSTOM' && <>
<div className="form-grid" style={{ marginTop: 16 }}>
<label className="field"><span>Servidor SMTP *</span><input value={host} onChange={(event) => setHost(event.target.value)} placeholder="smtp.ejemplo.com" /></label>
<label className="field"><span>Puerto *</span><input type="number" min={1} max={65535} value={port} onChange={(event) => setPort(Number(event.target.value))} /></label>
<label className="field"><span>Seguridad *</span><select value={securityMode} onChange={(event) => setSecurityMode(event.target.value as UserSmtpSecurityMode)}><option value="STARTTLS">STARTTLS</option><option value="TLS">TLS</option><option value="NONE">Sin cifrado</option></select></label>
<label className="field"><span>Usuario SMTP</span><input value={username} onChange={(event) => setUsername(event.target.value)} autoComplete="username" /></label>
<label className="field"><span>Contraseña SMTP</span><input type="password" value={password} onChange={(event) => setPassword(event.target.value)} autoComplete="new-password" placeholder={smtp.custom?.hasPassword ? 'Dejar vacío para conservarla' : 'Contraseña SMTP'} /></label>
<label className="field"><span>Nombre del remitente</span><input value={fromName} onChange={(event) => setFromName(event.target.value)} placeholder={`${profile.firstName} ${profile.lastName}`} /></label>
</div>
<p className="muted-copy">La contraseña se cifra en el servidor y nunca vuelve a mostrarse. El remitente será <strong>{profile.email}</strong>.</p>
</>}
<div className="form-actions wrap-actions">
<button className="button primary" type="button" onClick={saveSmtp} disabled={saving === 'smtp' || (mode === 'CUSTOM' && (!host.trim() || !port))}>{saving === 'smtp' ? 'Guardando…' : 'Guardar configuración'}</button>
<button className="button secondary" type="button" onClick={testSmtp} disabled={Boolean(saving)}>{saving === 'test' ? 'Enviando…' : 'Enviar correo de prueba'}</button>
</div>
</div>
</div>
<div className="panel" style={{ marginTop: 16 }}>
<div className="panel-heading"><div><span className="eyebrow">SEGURIDAD</span><h2>Cuenta</h2></div></div>
<p>Tu email es obligatorio para operar en DH Inspección. La contraseña SMTP nunca se muestra ni se incluye en auditorías.</p>
<p className="muted-copy">Usuario autenticado: @{user?.username ?? profile.username}</p>
</div>
</section>;
}