refactor: gestión de módulos de correo al portal IT
This commit is contained in:
1 parent
e83bc239c0
commit
7009d8756d
3 files changed
+4
-175
No files matched your search
@@ -83,5 +83,5 @@ Base: `NEXT_PUBLIC_API_URL/api/helpdesk`
|
|||||||
- `GET|POST /users` · `PUT /users/{id}` (admin)
|
- `GET|POST /users` · `PUT /users/{id}` (admin)
|
||||||
- `GET|POST /sla` · `PUT /sla/{id}` (admin)
|
- `GET|POST /sla` · `PUT /sla/{id}` (admin)
|
||||||
- `GET /notifications?module=&limit=&unreadOnly=` · `GET /notifications/unread-count?module=` · `PUT /notifications/{id}/read` · `PUT /notifications/read-all?module=` (propias, estándar multi-módulo `mail.notifications`, campana con polling 60s + color por módulo) · `POST /notifications/send` (módulos con `X-Module` + `X-Api-Key`)
|
- `GET /notifications?module=&limit=&unreadOnly=` · `GET /notifications/unread-count?module=` · `PUT /notifications/{id}/read` · `PUT /notifications/read-all?module=` (propias, estándar multi-módulo `mail.notifications`, campana con polling 60s + color por módulo) · `POST /notifications/send` (módulos con `X-Module` + `X-Api-Key`)
|
||||||
- `GET /email/templates` · `POST /email/templates` · `PUT /email/templates/{key}` · `GET /email/log?module=helpdesk&limit=` · `GET|POST /email/modules` · `DELETE /email/modules/{m}` (admin, banco único `mail.*`)
|
- `GET /email/templates` · `POST /email/templates` · `PUT /email/templates/{key}` · `GET /email/log?module=helpdesk&limit=` (admin, banco único `mail.*`; las claves de módulos se gestionan en el portal IT)
|
||||||
- `POST /api/correos` (route propio server-side → `POST /api/email/send` con `X-Module: helpdesk` + `X-Api-Key`) · `POST /helpdesk/email/inbound/sync` (admin, inbound Gmail sin cambios)
|
- `POST /api/correos` (route propio server-side → `POST /api/email/send` con `X-Module: helpdesk` + `X-Api-Key`) · `POST /helpdesk/email/inbound/sync` (admin, inbound Gmail sin cambios)
|
||||||
@@ -8,12 +8,8 @@ import {
|
|||||||
getEmailLog,
|
getEmailLog,
|
||||||
sendTestEmail,
|
sendTestEmail,
|
||||||
syncInboundEmail,
|
syncInboundEmail,
|
||||||
getEmailModules,
|
|
||||||
registerEmailModule,
|
|
||||||
revokeEmailModule,
|
|
||||||
type EmailTemplate,
|
type EmailTemplate,
|
||||||
type EmailLogEntry,
|
type EmailLogEntry,
|
||||||
type EmailModule,
|
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
@@ -36,7 +32,6 @@ export default function EmailPage() {
|
|||||||
const { user, isLoading: authLoading } = useAuth();
|
const { user, isLoading: authLoading } = useAuth();
|
||||||
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
|
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
|
||||||
const [log, setLog] = useState<EmailLogEntry[]>([]);
|
const [log, setLog] = useState<EmailLogEntry[]>([]);
|
||||||
const [modules, setModules] = useState<EmailModule[]>([]);
|
|
||||||
const [logModule, setLogModule] = useState("helpdesk");
|
const [logModule, setLogModule] = useState("helpdesk");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
@@ -56,15 +51,6 @@ export default function EmailPage() {
|
|||||||
const [newModule, setNewModule] = useState("helpdesk");
|
const [newModule, setNewModule] = useState("helpdesk");
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [createError, setCreateError] = useState("");
|
const [createError, setCreateError] = useState("");
|
||||||
// Módulos (GET/POST/DELETE /api/email/modules)
|
|
||||||
const [newModuleName, setNewModuleName] = useState("");
|
|
||||||
const [registering, setRegistering] = useState(false);
|
|
||||||
const [registerResult, setRegisterResult] = useState<{
|
|
||||||
ok: boolean;
|
|
||||||
text: string;
|
|
||||||
apiKey?: string;
|
|
||||||
} | null>(null);
|
|
||||||
const [revoking, setRevoking] = useState<string | null>(null);
|
|
||||||
const [testTo, setTestTo] = useState("");
|
const [testTo, setTestTo] = useState("");
|
||||||
const [testResult, setTestResult] = useState<{ ok: boolean; text: string } | null>(null);
|
const [testResult, setTestResult] = useState<{ ok: boolean; text: string } | null>(null);
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
@@ -75,19 +61,16 @@ export default function EmailPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const [t, l, m] = await Promise.all([
|
const [t, l] = await Promise.all([
|
||||||
getEmailTemplates(),
|
getEmailTemplates(),
|
||||||
getEmailLog(50, logModule),
|
getEmailLog(50, logModule),
|
||||||
getEmailModules(),
|
|
||||||
]);
|
]);
|
||||||
setTemplates(t);
|
setTemplates(t);
|
||||||
setLog(l);
|
setLog(l);
|
||||||
setModules(m);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Error al cargar");
|
setError(err instanceof Error ? err.message : "Error al cargar");
|
||||||
setTemplates([]);
|
setTemplates([]);
|
||||||
setLog([]);
|
setLog([]);
|
||||||
setModules([]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -95,22 +78,16 @@ export default function EmailPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authLoading || user?.role !== "admin") return;
|
if (authLoading || user?.role !== "admin") return;
|
||||||
Promise.all([
|
Promise.all([getEmailTemplates(), getEmailLog(50, logModule)])
|
||||||
getEmailTemplates(),
|
.then(([t, l]) => {
|
||||||
getEmailLog(50, logModule),
|
|
||||||
getEmailModules(),
|
|
||||||
])
|
|
||||||
.then(([t, l, m]) => {
|
|
||||||
setTemplates(t);
|
setTemplates(t);
|
||||||
setLog(l);
|
setLog(l);
|
||||||
setModules(m);
|
|
||||||
setError("");
|
setError("");
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setError(err instanceof Error ? err.message : "Error al cargar");
|
setError(err instanceof Error ? err.message : "Error al cargar");
|
||||||
setTemplates([]);
|
setTemplates([]);
|
||||||
setLog([]);
|
setLog([]);
|
||||||
setModules([]);
|
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [authLoading, user?.role, logModule]);
|
}, [authLoading, user?.role, logModule]);
|
||||||
@@ -191,43 +168,6 @@ export default function EmailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegister = async () => {
|
|
||||||
const modName = newModuleName.trim().toLowerCase();
|
|
||||||
if (!/^[a-z0-9-]{2,32}$/.test(modName)) {
|
|
||||||
setRegisterResult({ ok: false, text: "Módulo inválido ([a-z0-9-], 2-32)" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setRegistering(true);
|
|
||||||
setRegisterResult(null);
|
|
||||||
try {
|
|
||||||
const r = await registerEmailModule(modName);
|
|
||||||
setRegisterResult({
|
|
||||||
ok: true,
|
|
||||||
text: `Clave de "${r.module}" generada. Se muestra una sola vez.`,
|
|
||||||
apiKey: r.apiKey,
|
|
||||||
});
|
|
||||||
setNewModuleName("");
|
|
||||||
fetchData();
|
|
||||||
} catch (err) {
|
|
||||||
setRegisterResult({ ok: false, text: err instanceof Error ? err.message : "Error al registrar" });
|
|
||||||
} finally {
|
|
||||||
setRegistering(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRevoke = async (modName: string) => {
|
|
||||||
if (!window.confirm(`¿Revocar la clave del módulo "${modName}"? Dejará de poder usar POST /api/email/send.`)) return;
|
|
||||||
setRevoking(modName);
|
|
||||||
try {
|
|
||||||
await revokeEmailModule(modName);
|
|
||||||
fetchData();
|
|
||||||
} catch (err) {
|
|
||||||
setRegisterResult({ ok: false, text: err instanceof Error ? err.message : "Error al revocar" });
|
|
||||||
} finally {
|
|
||||||
setRevoking(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTest = async () => {
|
const handleTest = async () => {
|
||||||
if (!testTo.trim() || !testTo.includes("@")) {
|
if (!testTo.trim() || !testTo.includes("@")) {
|
||||||
setTestResult({ ok: false, text: "Destino inválido" });
|
setTestResult({ ok: false, text: "Destino inválido" });
|
||||||
@@ -361,95 +301,6 @@ export default function EmailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card mb-4">
|
|
||||||
<div className="card-body">
|
|
||||||
<h6 className="fw-semibold mb-3">
|
|
||||||
<i className="bi bi-key me-2"></i>
|
|
||||||
Módulos autorizados
|
|
||||||
</h6>
|
|
||||||
{registerResult && (
|
|
||||||
<div className={`alert ${registerResult.ok ? "alert-success" : "alert-danger"} py-2`} role="alert">
|
|
||||||
{registerResult.text}
|
|
||||||
{registerResult.apiKey && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<code className="user-select-all">{registerResult.apiKey}</code>
|
|
||||||
<div className="small mt-1">
|
|
||||||
Guárdala en el <code>.env</code> del módulo como <code>MAIL_API_KEY</code>. No se vuelve a mostrar.
|
|
||||||
Registrar de nuevo rota la clave anterior.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{modules.length > 0 && (
|
|
||||||
<div className="table-responsive mb-3">
|
|
||||||
<table className="table table-sm table-hover align-middle mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Módulo</th>
|
|
||||||
<th>Estado</th>
|
|
||||||
<th className="text-end">Acciones</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{modules.map((m) => (
|
|
||||||
<tr key={m.module}>
|
|
||||||
<td>
|
|
||||||
<code className="small">{m.module}</code>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{m.isActive ? (
|
|
||||||
<span className="badge bg-success">Activo</span>
|
|
||||||
) : (
|
|
||||||
<span className="badge bg-secondary">Revocado</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="text-end">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline-danger"
|
|
||||||
onClick={() => handleRevoke(m.module)}
|
|
||||||
disabled={revoking === m.module}
|
|
||||||
title="Revocar clave"
|
|
||||||
>
|
|
||||||
{revoking === m.module ? (
|
|
||||||
<span className="spinner-border spinner-border-sm" role="status" />
|
|
||||||
) : (
|
|
||||||
<i className="bi bi-trash"></i>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="d-flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="nombre-modulo (ej: webpay)"
|
|
||||||
value={newModuleName}
|
|
||||||
onChange={(e) => setNewModuleName(e.target.value)}
|
|
||||||
style={{ maxWidth: 320 }}
|
|
||||||
/>
|
|
||||||
<button className="btn btn-outline-primary" onClick={handleRegister} disabled={registering}>
|
|
||||||
{registering ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-border spinner-border-sm me-2" role="status" />
|
|
||||||
Generando...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Registrar / rotar clave"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<small className="text-muted d-block mt-2">
|
|
||||||
La clave plana se muestra una sola vez (<code>POST /api/email/modules</code>).
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h6 className="fw-semibold mb-3">
|
<h6 className="fw-semibold mb-3">
|
||||||
|
|||||||
@@ -361,11 +361,6 @@ export interface EmailLogEntry {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailModule {
|
|
||||||
module: string;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEmailTemplates() {
|
export async function getEmailTemplates() {
|
||||||
return fetchApi<EmailTemplate[]>("/api/email/templates");
|
return fetchApi<EmailTemplate[]>("/api/email/templates");
|
||||||
}
|
}
|
||||||
@@ -399,23 +394,6 @@ export async function getEmailLog(limit = 50, module = "helpdesk") {
|
|||||||
return fetchApi<EmailLogEntry[]>(`/api/email/log?${query.toString()}`);
|
return fetchApi<EmailLogEntry[]>(`/api/email/log?${query.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEmailModules() {
|
|
||||||
return fetchApi<EmailModule[]>("/api/email/modules");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function registerEmailModule(module: string) {
|
|
||||||
return fetchApi<{ module: string; apiKey: string }>("/api/email/modules", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ module }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function revokeEmailModule(module: string) {
|
|
||||||
return fetchApi<{ module: string }>(`/api/email/modules/${encodeURIComponent(module)}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Envío vía route handler propio (/api/correos) → POST /api/email/send con
|
// Envío vía route handler propio (/api/correos) → POST /api/email/send con
|
||||||
// X-Module + X-Api-Key en el servidor (la clave nunca va en el bundle).
|
// X-Module + X-Api-Key en el servidor (la clave nunca va en el bundle).
|
||||||
// Reenvía el Bearer de admin para que el route verifique el rol.
|
// Reenvía el Bearer de admin para que el route verifique el rol.
|
||||||
|
|||||||
Reference in new issue
Block a user