feat: página Correo con claves de módulos + lint en cero
This commit is contained in:
1 parent
1bf1e12273
commit
f9e0887f10
13 files changed
+471
-12
No files matched your search
@@ -16,6 +16,7 @@
|
||||
| `/upload` | sí | Integrar módulo (categoría, tipo, visibilidad) |
|
||||
| `/actualizar` | sí | Redeploy + webhook + toggle visibilidad |
|
||||
| `/eliminar` | sí | Baja de módulos (con/sin código) |
|
||||
| `/correo` | sí | Claves de correo por módulo (`POST /api/email/modules`, banco `mail.*`) |
|
||||
| `/api/modules`, `/api/stats` | no/sí | Proxy al deployer (token server-side) |
|
||||
| `/api/deployer/*` | sí | Proxy genérico al deployer |
|
||||
| `/api/auth`, `/api/me` | — | Sesión HMAC 12h en cookie httpOnly |
|
||||
@@ -32,6 +33,11 @@ DEPLOYER_TOKEN=<token>
|
||||
NEXT_PUBLIC_API_URL=https://api.devops.norteamericano.cl
|
||||
IT_USERS=cgaray@norteamericano.cl,fmena@norteamericano.cl,jallende@norteamericano.cl
|
||||
IT_SESSION_SECRET=<openssl rand -hex 24>
|
||||
# Cuenta de servicio helpdesk (usuario sige real con rol admin) para
|
||||
# administrar claves de correo desde /correo (solo servidor):
|
||||
HELPDESK_SERVICE_USER=<idUsuario>
|
||||
HELPDESK_SERVICE_PASS=<clave>
|
||||
# SAM_API_URL=https://api.devops.norteamericano.cl # default prod
|
||||
```
|
||||
|
||||
## Desarrollo
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
assertIT,
|
||||
helpdeskFetch,
|
||||
asResponse,
|
||||
errResponse,
|
||||
} from "@/lib/correo-server";
|
||||
|
||||
// DELETE: revoca la clave de un módulo (deja de poder usar
|
||||
// POST /api/email/send hasta registrar una nueva).
|
||||
export async function DELETE(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ modulo: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
await assertIT(req);
|
||||
const modulo = (await params).modulo.trim().toLowerCase();
|
||||
return await asResponse(
|
||||
await helpdeskFetch(
|
||||
`/api/email/modules/${encodeURIComponent(modulo)}`,
|
||||
{ method: "DELETE" }
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
return errResponse(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
assertIT,
|
||||
helpdeskFetch,
|
||||
asResponse,
|
||||
errResponse,
|
||||
} from "@/lib/correo-server";
|
||||
|
||||
const MODULE_RE = /^[a-z0-9-]{2,32}$/;
|
||||
|
||||
// GET: lista módulos autorizados del banco de correo.
|
||||
export async function GET(req: Request): Promise<Response> {
|
||||
try {
|
||||
await assertIT(req);
|
||||
return await asResponse(await helpdeskFetch("/api/email/modules"));
|
||||
} catch (e) {
|
||||
return errResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
// POST {module}: registra (o rota) la clave. La apiKey plana se
|
||||
// devuelve una sola vez: mostrarla y pedir guardarla en el .env
|
||||
// del módulo como MAIL_API_KEY.
|
||||
export async function POST(req: Request): Promise<Response> {
|
||||
try {
|
||||
await assertIT(req);
|
||||
const body = (await req.json().catch(() => ({}))) as {
|
||||
module?: string;
|
||||
};
|
||||
const modName = (body.module ?? "").trim().toLowerCase();
|
||||
if (!MODULE_RE.test(modName)) {
|
||||
return NextResponse.json(
|
||||
{ message: "Módulo inválido ([a-z0-9-], 2-32)" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return await asResponse(
|
||||
await helpdeskFetch("/api/email/modules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ module: modName }),
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
return errResponse(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import SamShell from "@/components/SamShell";
|
||||
import { refreshCorreo, useCorreoModulos } from "@/lib/correo";
|
||||
import { refreshModules, useModules, useMe } from "@/lib/store";
|
||||
import { itMenu } from "@/lib/sam";
|
||||
|
||||
// Correo: claves de servicio por módulo (banco mail.*).
|
||||
// Registrar/rotar muestra la apiKey UNA vez: va al .env del
|
||||
// módulo como MAIL_API_KEY (nunca NEXT_PUBLIC_).
|
||||
export default function Correo() {
|
||||
const { data, error } = useModules();
|
||||
const me = useMe();
|
||||
const modules = data?.modules ?? [];
|
||||
const correo = useCorreoModulos();
|
||||
const rows = correo.data ?? [];
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [msg, setMsg] = useState<{ ok: boolean; text: string; apiKey?: string } | null>(null);
|
||||
|
||||
const register = async () => {
|
||||
const modName = name.trim().toLowerCase();
|
||||
if (!/^[a-z0-9-]{2,32}$/.test(modName)) {
|
||||
setMsg({ ok: false, text: "Módulo inválido ([a-z0-9-], 2-32)" });
|
||||
return;
|
||||
}
|
||||
setBusy(`register:${modName}`);
|
||||
setMsg(null);
|
||||
try {
|
||||
const res = await fetch("/api/correo/modulos", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ module: modName }),
|
||||
cache: "no-store",
|
||||
});
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(j.message || j.error || `HTTP ${res.status}`);
|
||||
setMsg({
|
||||
ok: true,
|
||||
text: `Clave de "${j.module}" generada. Se muestra una sola vez.`,
|
||||
apiKey: j.apiKey,
|
||||
});
|
||||
setName("");
|
||||
refreshCorreo();
|
||||
} catch (e) {
|
||||
setMsg({ ok: false, text: e instanceof Error ? e.message : "Error" });
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (module: string) => {
|
||||
if (!window.confirm(`¿Revocar la clave de "${module}"? Dejará de poder usar POST /api/email/send.`)) return;
|
||||
setBusy(`revoke:${module}`);
|
||||
setMsg(null);
|
||||
try {
|
||||
const res = await fetch(`/api/correo/modulos/${encodeURIComponent(module)}`, {
|
||||
method: "DELETE",
|
||||
cache: "no-store",
|
||||
});
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(j.message || j.error || `HTTP ${res.status}`);
|
||||
setMsg({ ok: true, text: `Clave de "${module}" revocada.` });
|
||||
refreshCorreo();
|
||||
refreshModules();
|
||||
} catch (e) {
|
||||
setMsg({ ok: false, text: e instanceof Error ? e.message : "Error" });
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SamShell
|
||||
brand="IT"
|
||||
subtitle="Correo · Módulos"
|
||||
menu={itMenu(modules)}
|
||||
activeHref="/correo"
|
||||
topUser={me?.email || "SAM"}
|
||||
allowLogout
|
||||
backHref="/"
|
||||
>
|
||||
<div className="alert alert-light section-header mb-3 py-2 px-3">
|
||||
<i className="bx bx-envelope me-1" /> Correo · Módulos autorizados
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-danger">{error}</div>}
|
||||
{correo.error && <div className="alert alert-danger">{correo.error}</div>}
|
||||
{msg && (
|
||||
<div className={`alert ${msg.ok ? "alert-success" : "alert-danger"}`}>
|
||||
{msg.text}
|
||||
{msg.apiKey && (
|
||||
<div className="mt-2">
|
||||
<code className="user-select-all">{msg.apiKey}</code>
|
||||
<div className="small mt-1">
|
||||
Guárdala en el <code>.env</code> del módulo como <code>MAIL_API_KEY</code>.
|
||||
Registrar de nuevo rota la anterior.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="app-card p-3">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Módulo</th>
|
||||
<th>Estado</th>
|
||||
<th className="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((m) => (
|
||||
<tr key={m.module}>
|
||||
<td>
|
||||
<i className="bx bx-cube me-2" />
|
||||
<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"
|
||||
disabled={busy !== null}
|
||||
onClick={() => revoke(m.module)}
|
||||
title="Revocar clave"
|
||||
>
|
||||
{busy === `revoke:${m.module}` ? (
|
||||
<span className="spinner-border spinner-border-sm" />
|
||||
) : (
|
||||
<i className="bx bx-trash" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center text-muted py-4">
|
||||
Sin módulos registrados.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2 mt-3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
style={{ maxWidth: 320 }}
|
||||
placeholder="nombre-modulo (ej: webpay)"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={busy !== null}
|
||||
onClick={register}
|
||||
>
|
||||
{busy?.startsWith("register:") ? (
|
||||
<span className="spinner-border spinner-border-sm" />
|
||||
) : (
|
||||
<>
|
||||
<i className="bx bx-key me-1" />
|
||||
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>
|
||||
</SamShell>
|
||||
);
|
||||
}
|
||||
+11
-6
@@ -1,7 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Fira_Sans } from "next/font/google";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./globals.css";
|
||||
|
||||
const firaSans = Fira_Sans({
|
||||
variable: "--font-fira-sans",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SAM | Sistema de Administración de Matrículas",
|
||||
description: "SAM - Índice de módulos integrados y tableros por módulo",
|
||||
@@ -16,18 +23,16 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="es">
|
||||
<html lang="es" className={firaSans.variable}>
|
||||
<head>
|
||||
<link
|
||||
href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Fira+Sans:400,500,600,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
<body style={{ fontFamily: "var(--font-fira-sans), sans-serif" }}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Suspense } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
function LoginForm() {
|
||||
@@ -36,7 +37,7 @@ function LoginForm() {
|
||||
<div className="login-page">
|
||||
<div className="sidenav">
|
||||
<div className="login-main-text text-light text-center">
|
||||
<img src="/favicon.png" alt="SAM" />
|
||||
<Image src="/favicon.png" alt="SAM" width={80} height={80} />
|
||||
<h1>SAM</h1>
|
||||
<h3>Welcome | Bienvenido/a</h3>
|
||||
<h3>Portal IT</h3>
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useModules, refreshModules } from "@/lib/store";
|
||||
import {
|
||||
@@ -121,7 +122,7 @@ export default function Home() {
|
||||
className="mx-auto d-flex align-items-center justify-content-center bg-white rounded-3 overflow-hidden"
|
||||
style={{ width: 80, height: 80 }}
|
||||
>
|
||||
<img src="/logo-sam.png" alt="SAM" style={{ width: 64, height: 64, objectFit: "contain" }} />
|
||||
<Image src="/logo-sam.png" alt="SAM" width={64} height={64} style={{ objectFit: "contain" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 text-center mt-3">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import type { MenuItem } from "@/lib/sam";
|
||||
|
||||
@@ -31,12 +33,13 @@ export default function SamShell({
|
||||
children,
|
||||
}: SamShellProps) {
|
||||
const [open, setOpen] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await fetch("/api/auth", { method: "DELETE" });
|
||||
} finally {
|
||||
window.location.href = "/login";
|
||||
router.push("/login");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,7 +48,7 @@ export default function SamShell({
|
||||
<div className="sam-sidebar">
|
||||
<div className="logo-details">
|
||||
<span className="logo_name">
|
||||
<img src="/favicon.png" alt="SAM" width={28} height={28} />
|
||||
<Image src="/favicon.png" alt="SAM" width={28} height={28} />
|
||||
<span className="brand-text">SAM</span>
|
||||
</span>
|
||||
<i
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import SamShell from "@/components/SamShell";
|
||||
import { useModules, useStats } from "@/lib/store";
|
||||
import {
|
||||
displayName,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// ============================================================
|
||||
// Acceso server-side al banco de correo (SAM.API /api/email/*).
|
||||
// Auth en dos capas: sesión IT (cookie, solo personal autorizado)
|
||||
// + cuenta de servicio helpdesk (env, debe ser admin). Nada de
|
||||
// esto sale al navegador. Solo importado por route handlers.
|
||||
// Env: SAM_API_URL (default prod), HELPDESK_SERVICE_USER,
|
||||
// HELPDESK_SERVICE_PASS (usuario sige real con rol admin).
|
||||
// ============================================================
|
||||
|
||||
import { COOKIE, readSession } from "./auth";
|
||||
|
||||
const SAM_API_URL =
|
||||
process.env.SAM_API_URL ??
|
||||
process.env.NEXT_PUBLIC_API_URL ??
|
||||
"https://api.devops.norteamericano.cl";
|
||||
|
||||
let cached: { token: string; exp: number } | null = null;
|
||||
|
||||
async function serviceLogin(): Promise<string> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (cached && cached.exp - 60 > now) return cached.token;
|
||||
const idUsuario = process.env.HELPDESK_SERVICE_USER ?? "";
|
||||
const password = process.env.HELPDESK_SERVICE_PASS ?? "";
|
||||
if (!idUsuario || !password) {
|
||||
throw Object.assign(
|
||||
new Error("HELPDESK_SERVICE_USER/PASS no configurados en el servidor"),
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
const res = await fetch(`${SAM_API_URL}/api/helpdesk/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ idUsuario, password }),
|
||||
cache: "no-store",
|
||||
});
|
||||
const data = (await res.json().catch(() => ({}))) as {
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
token?: string;
|
||||
};
|
||||
if (!res.ok || !data.success || !data.token) {
|
||||
throw Object.assign(
|
||||
new Error(data.message || "No se pudo autenticar la cuenta de servicio"),
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
cached = { token: data.token, exp: now + 11 * 3600 };
|
||||
return cached.token;
|
||||
}
|
||||
|
||||
export async function assertIT(req: Request): Promise<string> {
|
||||
const cookie = req.headers.get("cookie") ?? "";
|
||||
const m = cookie.match(new RegExp(`(?:^|;\\s*)${COOKIE}=([^;]*)`));
|
||||
const sess = await readSession(m ? decodeURIComponent(m[1]) : undefined);
|
||||
if (!sess) {
|
||||
throw Object.assign(new Error("Requiere sesión IT"), { status: 401 });
|
||||
}
|
||||
return sess.email;
|
||||
}
|
||||
|
||||
export async function helpdeskFetch(
|
||||
path: string,
|
||||
init?: RequestInit
|
||||
): Promise<Response> {
|
||||
const doFetch = (token: string) =>
|
||||
fetch(`${SAM_API_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init?.headers || {}),
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
let res = await doFetch(await serviceLogin());
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
cached = null;
|
||||
res = await doFetch(await serviceLogin());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function asResponse(res: Response): Promise<Response> {
|
||||
return res
|
||||
.text()
|
||||
.then((text) => {
|
||||
try {
|
||||
return Response.json(JSON.parse(text), { status: res.status });
|
||||
} catch {
|
||||
return new Response(text, { status: res.status });
|
||||
}
|
||||
})
|
||||
.catch((e: unknown) =>
|
||||
Response.json(
|
||||
{ error: e instanceof Error ? e.message : "Error" },
|
||||
{ status: 502 }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function errResponse(e: unknown): Response {
|
||||
const status =
|
||||
typeof e === "object" && e !== null && "status" in e
|
||||
? Number((e as { status: unknown }).status) || 500
|
||||
: 500;
|
||||
return Response.json(
|
||||
{ error: e instanceof Error ? e.message : "Error" },
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// ============================================================
|
||||
// Claves de correo por módulo (banco mail.*): tipos + hook de
|
||||
// lectura con auto-refresh, sin setState en efectos
|
||||
// (misma regla react-hooks que lib/store.ts).
|
||||
// Las claves se administran vía /api/correo/modulos (server-side).
|
||||
// ============================================================
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
export interface CorreoModule {
|
||||
module: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface CorreoSnap {
|
||||
data: CorreoModule[] | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
async function fetchCorreo(): Promise<CorreoSnap> {
|
||||
try {
|
||||
const res = await fetch("/api/correo/modulos", { cache: "no-store" });
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(j.message || j.error || `HTTP ${res.status}`);
|
||||
return { data: Array.isArray(j) ? j : [], error: null };
|
||||
} catch (e) {
|
||||
return {
|
||||
data: null,
|
||||
error: e instanceof Error ? e.message : "Error al cargar módulos",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let snap: CorreoSnap = { data: null, error: null };
|
||||
let started = false;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function notify() {
|
||||
listeners.forEach((l) => l());
|
||||
}
|
||||
|
||||
export function refreshCorreo() {
|
||||
fetchCorreo()
|
||||
.then((next) => {
|
||||
snap = next;
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => notify());
|
||||
}
|
||||
|
||||
function subscribe(cb: () => void) {
|
||||
if (!started) {
|
||||
started = true;
|
||||
refreshCorreo();
|
||||
}
|
||||
listeners.add(cb);
|
||||
return () => {
|
||||
listeners.delete(cb);
|
||||
if (listeners.size === 0) started = false;
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(): CorreoSnap {
|
||||
return snap;
|
||||
}
|
||||
|
||||
export function useCorreoModulos(): CorreoSnap {
|
||||
return useSyncExternalStore(subscribe, snapshot, () => ({
|
||||
data: null,
|
||||
error: null,
|
||||
}));
|
||||
}
|
||||
@@ -305,5 +305,6 @@ export function itMenu(modules: ModuleState[]): MenuItem[] {
|
||||
{ label: "Agregar", icon: "bx-plus", href: "/upload" },
|
||||
{ label: "Actualizar", icon: "bx-refresh", href: "/actualizar" },
|
||||
{ label: "Eliminar", icon: "bx-trash", href: "/eliminar" },
|
||||
{ label: "Correo", icon: "bx-envelope", href: "/correo" },
|
||||
];
|
||||
}
|
||||
+3
-1
@@ -6,7 +6,7 @@ const IT_BOARD = "https://it.sam.devops.norteamericano.cl/tablero";
|
||||
|
||||
// Rutas IT (requieren sesión): tablero, gestión, tableros y APIs de acción.
|
||||
// El índice (/) y el login quedan públicos.
|
||||
const PROTECTED = ["/tablero", "/upload", "/actualizar", "/eliminar", "/m"];
|
||||
const PROTECTED = ["/tablero", "/upload", "/actualizar", "/eliminar", "/correo", "/m"];
|
||||
|
||||
function isProtected(path: string): boolean {
|
||||
return PROTECTED.some((p) => path === p || path.startsWith(p + "/"));
|
||||
@@ -51,6 +51,8 @@ export const config = {
|
||||
"/actualizar/:path*",
|
||||
"/eliminar",
|
||||
"/eliminar/:path*",
|
||||
"/correo",
|
||||
"/correo/:path*",
|
||||
"/m",
|
||||
"/m/:path*",
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user