diff --git a/app/(app)/machines/MachinesClient.tsx b/app/(app)/machines/MachinesClient.tsx index 1a27552..b150815 100644 --- a/app/(app)/machines/MachinesClient.tsx +++ b/app/(app)/machines/MachinesClient.tsx @@ -1,7 +1,8 @@ "use client"; import Link from "next/link"; -import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState, type KeyboardEvent } from "react"; import { useI18n } from "@/lib/i18n/useI18n"; type MachineRow = { @@ -49,6 +50,7 @@ function badgeClass(status?: string, offline?: boolean) { export default function MachinesClient({ initialMachines = [] }: { initialMachines?: MachineRow[] }) { const { t, locale } = useI18n(); + const router = useRouter(); const [machines, setMachines] = useState(() => initialMachines); const [loading, setLoading] = useState(false); const [showCreate, setShowCreate] = useState(false); @@ -151,6 +153,13 @@ export default function MachinesClient({ initialMachines = [] }: { initialMachin setTimeout(() => setCopyStatus(null), 2000); } + function handleCardKeyDown(event: KeyboardEvent, machineId: string) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + router.push(`/machines/${machineId}`); + } + } + const showCreateCard = showCreate || (!loading && machines.length === 0); return ( @@ -276,10 +285,13 @@ export default function MachinesClient({ initialMachines = [] }: { initialMachin const lastSeen = secondsAgo(hbTs, locale, t("common.never")); return ( - router.push(`/machines/${m.id}`)} + onKeyDown={(event) => handleCardKeyDown(event, m.id)} + className="cursor-pointer rounded-2xl border border-white/10 bg-white/5 p-5 hover:bg-white/10" >
@@ -316,7 +328,8 @@ export default function MachinesClient({ initialMachines = [] }: { initialMachin )}
- + +
); })} diff --git a/app/(app)/machines/[machineId]/MachineDetailClient.tsx b/app/(app)/machines/[machineId]/MachineDetailClient.tsx index b364cb8..8b4689f 100644 --- a/app/(app)/machines/[machineId]/MachineDetailClient.tsx +++ b/app/(app)/machines/[machineId]/MachineDetailClient.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import Link from "next/link"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import DowntimeParetoCard from "@/components/analytics/DowntimeParetoCard"; import { Bar, @@ -19,6 +19,7 @@ import { YAxis, } from "recharts"; import { useI18n } from "@/lib/i18n/useI18n"; +import { useScreenlessMode } from "@/lib/ui/screenlessMode"; type Heartbeat = { ts: string; @@ -290,6 +291,8 @@ function rowsToWorkOrders(rows: WorkOrderRow[]): WorkOrderUpload[] { export default function MachineDetailClient() { const { t, locale } = useI18n(); + const { screenlessMode } = useScreenlessMode(); + const router = useRouter(); const params = useParams<{ machineId: string }>(); const machineId = params?.machineId; @@ -306,6 +309,10 @@ export default function MachineDetailClient() { const [open, setOpen] = useState(null); const fileInputRef = useRef(null); const [uploadState, setUploadState] = useState({ status: "idle" }); + const [canDelete, setCanDelete] = useState(false); + const [deleteError, setDeleteError] = useState(null); + const [confirmDelete, setConfirmDelete] = useState(false); + const [deleting, setDeleting] = useState(false); useEffect(() => { @@ -359,6 +366,27 @@ export default function MachineDetailClient() { }; }, [machineId, t]); + useEffect(() => { + let alive = true; + + async function loadRole() { + try { + const res = await fetch("/api/me", { cache: "no-store" }); + const data = await res.json().catch(() => ({})); + if (!alive) return; + const role = data?.membership?.role; + setCanDelete(role === "OWNER" || role === "ADMIN"); + } catch { + if (alive) setCanDelete(false); + } + } + + loadRole(); + return () => { + alive = false; + }; + }, []); + useEffect(() => { if (open !== "events" || !machineId) return; @@ -470,6 +498,28 @@ export default function MachineDetailClient() { } } + async function deleteMachine() { + if (!machineId) return; + + setDeleting(true); + setDeleteError(null); + + try { + const res = await fetch(`/api/machines/${machineId}`, { method: "DELETE" }); + const data = await res.json().catch(() => ({})); + if (!res.ok || !data.ok) { + throw new Error(data.error || t("machines.delete.error.failed")); + } + router.push("/machines"); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : null; + setDeleteError(message || t("machines.delete.error.failed")); + } finally { + setDeleting(false); + setConfirmDelete(false); + } + } + const uploadButtonLabel = uploadState.status === "parsing" ? t("machine.detail.workOrders.uploadParsing") @@ -958,7 +1008,50 @@ export default function MachineDetailClient() { > {t("machine.detail.back")} + {canDelete && !confirmDelete && ( + + )} + {canDelete && confirmDelete && ( +
+
+
+ {t("machines.delete.confirm", { + name: machine?.name ?? t("machine.detail.titleFallback"), + })} +
+
+ + +
+
+
+ )} + {deleteError && ( +
+ {deleteError} +
+ )}
{t("machine.detail.workOrders.uploadHint")}
@@ -1014,31 +1107,33 @@ export default function MachineDetailClient() { activeStoppage={activeStoppage} /> -
-
-
-
Downtime (preview)
-
Top reasons + quick pareto
+ {!screenlessMode && ( +
+
+
+
Downtime (preview)
+
Top reasons + quick pareto
+
+ + View full report → +
- - View full report → - -
-
- +
+ +
-
+ )} diff --git a/app/api/machines/[machineId]/route.ts b/app/api/machines/[machineId]/route.ts index bc03092..7c902bd 100644 --- a/app/api/machines/[machineId]/route.ts +++ b/app/api/machines/[machineId]/route.ts @@ -1,316 +1,273 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; -import { createHash } from "crypto"; +import { z } from "zod"; import { prisma } from "@/lib/prisma"; import { requireSession } from "@/lib/auth/requireSession"; import { normalizeEvent } from "@/lib/events/normalizeEvent"; +const machineIdSchema = z.string().uuid(); -export async function GET( - _req: NextRequest, - { params }: { params: Promise<{ machineId: string }> } -) { +const ALLOWED_EVENT_TYPES = new Set([ + "slow-cycle", + "microstop", + "macrostop", + "offline", + "error", + "oee-drop", + "quality-spike", + "performance-degradation", + "predictive-oee-decline", + "alert-delivery-failed", +]); + +function canManageMachines(role?: string | null) { + return role === "OWNER" || role === "ADMIN"; +} + +function isPlainObject(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +function parseNumber(value: string | null, fallback: number) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +export async function GET(req: NextRequest, { params }: { params: Promise<{ machineId: string }> }) { const session = await requireSession(); if (!session) { return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); } - const url = new URL(_req.url); - const eventsMode = url.searchParams.get("events") ?? "all"; - const eventsOnly = url.searchParams.get("eventsOnly") === "1"; - const eventsWindowSec = Number(url.searchParams.get("eventsWindowSec") ?? "21600"); // default 6h - const eventsWindowStart = new Date(Date.now() - Math.max(0, eventsWindowSec) * 1000); - const windowSec = Number(url.searchParams.get("windowSec") ?? "3600"); // default 1h - const { machineId } = await params; - - const machineBase = await prisma.machine.findFirst({ - where: { id: machineId, orgId: session.orgId }, - select: { id: true, updatedAt: true }, - }); - - if (!machineBase) { - return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 }); + if (!machineIdSchema.safeParse(machineId).success) { + return NextResponse.json({ ok: false, error: "Invalid machine id" }, { status: 400 }); } - const [heartbeatAgg, kpiAgg, eventAgg, cycleAgg, orgSettingsAgg] = await Promise.all([ - prisma.machineHeartbeat.aggregate({ - where: { orgId: session.orgId, machineId }, - _max: { tsServer: true }, - }), - prisma.machineKpiSnapshot.aggregate({ - where: { orgId: session.orgId, machineId }, - _max: { tsServer: true }, - }), - prisma.machineEvent.aggregate({ - where: { orgId: session.orgId, machineId, ts: { gte: eventsWindowStart } }, - _max: { tsServer: true }, - }), - prisma.machineCycle.aggregate({ - where: { orgId: session.orgId, machineId }, - _max: { ts: true }, + const url = new URL(req.url); + const windowSec = Math.max(0, parseNumber(url.searchParams.get("windowSec"), 3600)); + const eventsWindowSec = Math.max(0, parseNumber(url.searchParams.get("eventsWindowSec"), 21600)); + const eventsMode = url.searchParams.get("events") ?? "critical"; + const eventsOnly = url.searchParams.get("eventsOnly") === "1"; + + const [machineRow, orgSettings, machineSettings] = await Promise.all([ + prisma.machine.findFirst({ + where: { id: machineId, orgId: session.orgId }, + select: { + id: true, + name: true, + code: true, + location: true, + createdAt: true, + updatedAt: true, + heartbeats: { + orderBy: { tsServer: "desc" }, + take: 1, + select: { ts: true, tsServer: true, status: true, message: true, ip: true, fwVersion: true }, + }, + kpiSnapshots: { + orderBy: { ts: "desc" }, + take: 1, + select: { + ts: true, + oee: true, + availability: true, + performance: true, + quality: true, + workOrderId: true, + sku: true, + good: true, + scrap: true, + target: true, + cycleTime: true, + }, + }, + }, }), prisma.orgSettings.findUnique({ where: { orgId: session.orgId }, - select: { updatedAt: true, stoppageMultiplier: true, macroStoppageMultiplier: true }, + select: { stoppageMultiplier: true, macroStoppageMultiplier: true }, + }), + prisma.machineSettings.findUnique({ + where: { machineId }, + select: { overridesJson: true }, }), ]); - const toMs = (value?: Date | null) => (value ? value.getTime() : 0); - const lastModifiedMs = Math.max( - toMs(machineBase.updatedAt), - toMs(heartbeatAgg._max.tsServer), - toMs(kpiAgg._max.tsServer), - toMs(eventAgg._max.tsServer), - toMs(cycleAgg._max.ts), - toMs(orgSettingsAgg?.updatedAt) - ); - - const versionParts = [ - session.orgId, - machineId, - eventsMode, - eventsOnly ? "1" : "0", - eventsWindowSec, - windowSec, - toMs(machineBase.updatedAt), - toMs(heartbeatAgg._max.tsServer), - toMs(kpiAgg._max.tsServer), - toMs(eventAgg._max.tsServer), - toMs(cycleAgg._max.ts), - toMs(orgSettingsAgg?.updatedAt), - ]; - - const etag = `W/"${createHash("sha1").update(versionParts.join("|")).digest("hex")}"`; - const lastModified = new Date(lastModifiedMs || 0).toUTCString(); - const responseHeaders = new Headers({ - "Cache-Control": "private, no-cache, max-age=0, must-revalidate", - ETag: etag, - "Last-Modified": lastModified, - Vary: "Cookie", - }); - - const ifNoneMatch = _req.headers.get("if-none-match"); - if (ifNoneMatch && ifNoneMatch === etag) { - return new NextResponse(null, { status: 304, headers: responseHeaders }); + if (!machineRow) { + return NextResponse.json({ ok: false, error: "Machine not found" }, { status: 404 }); } - const ifModifiedSince = _req.headers.get("if-modified-since"); - if (!ifNoneMatch && ifModifiedSince) { - const since = Date.parse(ifModifiedSince); - if (!Number.isNaN(since) && lastModifiedMs <= since) { - return new NextResponse(null, { status: 304, headers: responseHeaders }); - } - } + const overrides = isPlainObject(machineSettings?.overridesJson) ? machineSettings?.overridesJson : {}; + const thresholdsOverride = isPlainObject(overrides.thresholds) ? overrides.thresholds : {}; + const stoppageMultiplier = + typeof thresholdsOverride.stoppageMultiplier === "number" + ? thresholdsOverride.stoppageMultiplier + : Number(orgSettings?.stoppageMultiplier ?? 1.5); + const macroStoppageMultiplier = + typeof thresholdsOverride.macroStoppageMultiplier === "number" + ? thresholdsOverride.macroStoppageMultiplier + : Number(orgSettings?.macroStoppageMultiplier ?? 5); - const machine = await prisma.machine.findFirst({ - where: { id: machineId, orgId: session.orgId }, - select: { - id: true, - name: true, - code: true, - location: true, - heartbeats: { - orderBy: { tsServer: "desc" }, - take: 1, - select: { ts: true, tsServer: true, status: true, message: true, ip: true, fwVersion: true }, - }, - kpiSnapshots: { - orderBy: { ts: "desc" }, - take: 1, - select: { - ts: true, - oee: true, - availability: true, - performance: true, - quality: true, - workOrderId: true, - sku: true, - good: true, - scrap: true, - target: true, - cycleTime: true, - }, - }, - }, - }); - - if (!machine) { - return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 }); - } - - const microMultiplier = Number(orgSettingsAgg?.stoppageMultiplier ?? 1.5); - const macroMultiplier = Math.max( - microMultiplier, - Number(orgSettingsAgg?.macroStoppageMultiplier ?? 5) - ); - - const rawEvents = await prisma.machineEvent.findMany({ - where: { - orgId: session.orgId, - machineId, - ts: { gte: eventsWindowStart }, - }, - orderBy: { ts: "desc" }, - take: 100, // pull more, we'll filter after normalization - select: { - id: true, - ts: true, - topic: true, - eventType: true, - severity: true, - title: true, - description: true, - requiresAck: true, - data: true, - workOrderId: true, - }, - }); - - const normalized = rawEvents.map((row) => - normalizeEvent(row, { microMultiplier, macroMultiplier }) - ); - - const ALLOWED_TYPES = new Set([ - "slow-cycle", - "microstop", - "macrostop", - "offline", - "error", - "oee-drop", - "quality-spike", - "performance-degradation", - "predictive-oee-decline", - "alert-delivery-failed", - ]); - - const allEvents = normalized.filter((e) => ALLOWED_TYPES.has(e.eventType)); - - const isCritical = (event: (typeof allEvents)[number]) => { - const severity = String(event.severity ?? "").toLowerCase(); - return ( - event.eventType === "macrostop" || - event.requiresAck === true || - severity === "critical" || - severity === "error" || - severity === "high" - ); + const thresholds = { + stoppageMultiplier, + macroStoppageMultiplier, }; - const eventsFiltered = eventsMode === "critical" ? allEvents.filter(isCritical) : allEvents; - const events = eventsFiltered.slice(0, 30); - const eventsCountAll = allEvents.length; - const eventsCountCritical = allEvents.filter(isCritical).length; + const machine = { + ...machineRow, + effectiveCycleTime: null, + latestHeartbeat: machineRow.heartbeats[0] ?? null, + latestKpi: machineRow.kpiSnapshots[0] ?? null, + heartbeats: undefined, + kpiSnapshots: undefined, + }; - if (eventsOnly) { - return NextResponse.json( - { ok: true, events, eventsCountAll, eventsCountCritical }, - { headers: responseHeaders } - ); - } + const cycles = eventsOnly + ? [] + : await prisma.machineCycle.findMany({ + where: { + orgId: session.orgId, + machineId, + ts: { gte: new Date(Date.now() - windowSec * 1000) }, + }, + orderBy: { ts: "asc" }, + select: { + ts: true, + tsServer: true, + cycleCount: true, + actualCycleTime: true, + theoreticalCycleTime: true, + workOrderId: true, + sku: true, + }, + }); - -// ---- cycles window ---- - -const latestKpi = machine.kpiSnapshots[0] ?? null; - -// If KPI cycleTime missing, fallback to DB cycles (we fetch 1 first) -const latestCycleForIdeal = await prisma.machineCycle.findFirst({ - where: { orgId: session.orgId, machineId }, - orderBy: { ts: "desc" }, - select: { theoreticalCycleTime: true }, -}); - -const effectiveCycleTime = - latestKpi?.cycleTime ?? - latestCycleForIdeal?.theoreticalCycleTime ?? - null; - -// Estimate how many cycles we need to cover the window. -// Add buffer so the chart doesn’t look “tight”. -const estCycleSec = Math.max(1, Number(effectiveCycleTime ?? 14)); -const needed = Math.ceil(windowSec / estCycleSec) + 50; - -// Safety cap to avoid crazy payloads -const takeCycles = Math.min(1000, Math.max(200, needed)); - -const rawCycles = await prisma.machineCycle.findMany({ - where: { orgId: session.orgId, machineId }, - orderBy: { ts: "desc" }, - take: takeCycles, - select: { - ts: true, - cycleCount: true, - actualCycleTime: true, - theoreticalCycleTime: true, - workOrderId: true, - sku: true, - }, -}); -const latestCycle = rawCycles[0] ?? null; - -let activeStoppage: { - state: "microstop" | "macrostop"; - startedAt: string; - durationSec: number; - theoreticalCycleTime: number; -} | null = null; - -if (latestCycle?.ts && effectiveCycleTime && effectiveCycleTime > 0) { - const elapsedSec = (Date.now() - latestCycle.ts.getTime()) / 1000; - const microThresholdSec = effectiveCycleTime * microMultiplier; - const macroThresholdSec = effectiveCycleTime * macroMultiplier; - - if (elapsedSec >= microThresholdSec) { - const isMacro = elapsedSec >= macroThresholdSec; - const state = isMacro ? "macrostop" : "microstop"; - const thresholdSec = isMacro ? macroThresholdSec : microThresholdSec; - const startedAtMs = latestCycle.ts.getTime() + thresholdSec * 1000; - - activeStoppage = { - state, - startedAt: new Date(startedAtMs).toISOString(), - durationSec: Math.max(0, Math.floor(elapsedSec - thresholdSec)), - theoreticalCycleTime: effectiveCycleTime, + const cyclesOut = cycles.map((row) => { + const ts = row.tsServer ?? row.ts; + return { + ts, + t: ts.getTime(), + cycleCount: row.cycleCount ?? null, + actual: row.actualCycleTime, + ideal: row.theoreticalCycleTime ?? null, + workOrderId: row.workOrderId ?? null, + sku: row.sku ?? null, }; - } + }); + + const eventWindowStart = new Date(Date.now() - eventsWindowSec * 1000); + const criticalSeverities = ["critical", "error", "high"]; + const eventWhere = { + orgId: session.orgId, + machineId, + ts: { gte: eventWindowStart }, + eventType: { in: Array.from(ALLOWED_EVENT_TYPES) }, + ...(eventsMode === "critical" + ? { + OR: [ + { eventType: "macrostop" }, + { requiresAck: true }, + { severity: { in: criticalSeverities } }, + ], + } + : {}), + }; + + const [rawEvents, eventsCountAll] = await Promise.all([ + prisma.machineEvent.findMany({ + where: eventWhere, + orderBy: { ts: "desc" }, + take: eventsOnly ? 300 : 120, + select: { + id: true, + ts: true, + topic: true, + eventType: true, + severity: true, + title: true, + description: true, + requiresAck: true, + data: true, + workOrderId: true, + }, + }), + prisma.machineEvent.count({ where: eventWhere }), + ]); + + const normalized = rawEvents.map((row) => + normalizeEvent(row, { microMultiplier: stoppageMultiplier, macroMultiplier: macroStoppageMultiplier }) + ); + + const seen = new Set(); + const deduped = normalized.filter((event) => { + const key = `${event.eventType}-${event.ts ?? ""}-${event.title}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + deduped.sort((a, b) => { + const at = a.ts ? a.ts.getTime() : 0; + const bt = b.ts ? b.ts.getTime() : 0; + return bt - at; + }); + + return NextResponse.json({ + ok: true, + machine, + events: deduped, + eventsCountAll, + cycles: cyclesOut, + thresholds, + activeStoppage: null, + }); } -// chart-friendly: oldest -> newest + numeric timestamps -const cycles = rawCycles - .slice() - .reverse() - .map((c) => ({ - ts: c.ts, - t: c.ts.getTime(), - cycleCount: c.cycleCount ?? null, - actual: c.actualCycleTime, - ideal: c.theoreticalCycleTime ?? null, - workOrderId: c.workOrderId ?? null, - sku: c.sku ?? null, - })); - return NextResponse.json( - { - ok: true, - machine: { - id: machine.id, - name: machine.name, - code: machine.code, - location: machine.location, - latestHeartbeat: machine.heartbeats[0] ?? null, - latestKpi: machine.kpiSnapshots[0] ?? null, - effectiveCycleTime, +export async function DELETE(_req: Request, { params }: { params: Promise<{ machineId: string }> }) { + const session = await requireSession(); + if (!session) { + return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); + } + + const { machineId } = await params; + if (!machineIdSchema.safeParse(machineId).success) { + return NextResponse.json({ ok: false, error: "Invalid machine id" }, { status: 400 }); + } + + const membership = await prisma.orgUser.findUnique({ + where: { + orgId_userId: { + orgId: session.orgId, + userId: session.userId, }, - thresholds: { - stoppageMultiplier: microMultiplier, - macroStoppageMultiplier: macroMultiplier, - }, - activeStoppage, - events, - eventsCountAll, - eventsCountCritical, - cycles, }, - { headers: responseHeaders } - ); + select: { role: true }, + }); + + if (!canManageMachines(membership?.role)) { + return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 }); + } + + const result = await prisma.$transaction(async (tx) => { + await tx.machineCycle.deleteMany({ + where: { + machineId, + orgId: session.orgId, + }, + }); + + return tx.machine.deleteMany({ + where: { + id: machineId, + orgId: session.orgId, + }, + }); + }); + + if (result.count === 0) { + return NextResponse.json({ ok: false, error: "Machine not found" }, { status: 404 }); + } + + return NextResponse.json({ ok: true }); } diff --git a/lib/i18n/en.json b/lib/i18n/en.json index 32a7a78..e639a60 100644 --- a/lib/i18n/en.json +++ b/lib/i18n/en.json @@ -114,11 +114,15 @@ "machines.field.name": "Machine Name", "machines.field.code": "Code (optional)", "machines.field.location": "Location (optional)", - "machines.create.loading": "Creating...", - "machines.create.default": "Create Machine", - "machines.create.error.nameRequired": "Machine name is required", - "machines.create.error.failed": "Failed to create machine", - "machines.pairing.title": "Edge pairing code", + "machines.create.loading": "Creating...", + "machines.create.default": "Create Machine", + "machines.create.error.nameRequired": "Machine name is required", + "machines.create.error.failed": "Failed to create machine", + "machines.delete": "Remove", + "machines.delete.loading": "Removing...", + "machines.delete.confirm": "Remove {name}? This will delete the machine and its data.", + "machines.delete.error.failed": "Failed to remove machine", + "machines.pairing.title": "Edge pairing code", "machines.pairing.machine": "Machine:", "machines.pairing.codeLabel": "Pairing code", "machines.pairing.expires": "Expires", diff --git a/lib/i18n/es-MX.json b/lib/i18n/es-MX.json index 8a0daab..17ac58f 100644 --- a/lib/i18n/es-MX.json +++ b/lib/i18n/es-MX.json @@ -114,11 +114,15 @@ "machines.field.name": "Nombre de la máquina", "machines.field.code": "Código (opcional)", "machines.field.location": "Ubicación (opcional)", - "machines.create.loading": "Creando...", - "machines.create.default": "Crear máquina", - "machines.create.error.nameRequired": "El nombre de la máquina es obligatorio", - "machines.create.error.failed": "No se pudo crear la máquina", - "machines.pairing.title": "Código de emparejamiento", + "machines.create.loading": "Creando...", + "machines.create.default": "Crear máquina", + "machines.create.error.nameRequired": "El nombre de la máquina es obligatorio", + "machines.create.error.failed": "No se pudo crear la máquina", + "machines.delete": "Eliminar", + "machines.delete.loading": "Eliminando...", + "machines.delete.confirm": "¿Eliminar {name}? Esto borrará la máquina y sus datos.", + "machines.delete.error.failed": "No se pudo eliminar la máquina", + "machines.pairing.title": "Código de emparejamiento", "machines.pairing.machine": "Máquina:", "machines.pairing.codeLabel": "Código de emparejamiento", "machines.pairing.expires": "Expira", diff --git a/nousar_middleware.ts b/nousar_middleware.ts new file mode 100644 index 0000000..f8c1c44 --- /dev/null +++ b/nousar_middleware.ts @@ -0,0 +1,36 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(req: NextRequest) { + const { pathname, search } = req.nextUrl; + + // Skip auth for public routes + Next internals + if ( + pathname.startsWith("/_next") || + pathname.startsWith("/favicon") || + pathname.startsWith("/api") || + pathname === "/login" || + pathname === "/signup" || + pathname === "/logout" + ) { + return NextResponse.next(); + } + + // TODO: replace with your real session cookie name + const sessionId = req.cookies.get("sessionId")?.value; + + // Protect everything under the app + if (!sessionId) { + const url = req.nextUrl.clone(); + url.pathname = "/login"; + url.searchParams.set("next", pathname + search); + return NextResponse.redirect(url); + } + + return NextResponse.next(); +} + +// Limit the middleware to relevant paths +export const config = { + matcher: ["/((?!_next/static|_next/image).*)"], +};