Impede acesso ao portal para máquinas desativadas

This commit is contained in:
Esdras Renan 2025-10-18 00:01:35 -03:00
parent 0e97e4c0d6
commit e5085962e9
5 changed files with 195 additions and 18 deletions

View file

@ -1,6 +1,6 @@
import { NextResponse } from "next/server"
import { z } from "zod"
import { createMachineSession } from "@/server/machines-session"
import { createMachineSession, MachineInactiveError } from "@/server/machines-session"
import { applyCorsHeaders, createCorsPreflight, jsonWithCors } from "@/server/cors"
import {
MACHINE_CTX_COOKIE,
@ -20,8 +20,9 @@ export async function OPTIONS(request: Request) {
}
export async function POST(request: Request) {
const origin = request.headers.get("origin")
if (request.method !== "POST") {
return jsonWithCors({ error: "Método não permitido" }, 405, request.headers.get("origin"), CORS_METHODS)
return jsonWithCors({ error: "Método não permitido" }, 405, origin, CORS_METHODS)
}
let payload
@ -124,7 +125,15 @@ export async function POST(request: Request) {
return response
} catch (error) {
if (error instanceof MachineInactiveError) {
return jsonWithCors(
{ error: "Máquina desativada. Entre em contato com o suporte da Rever para reativar o acesso." },
423,
origin,
CORS_METHODS
)
}
console.error("[machines.sessions] Falha ao criar sessão", error)
return jsonWithCors({ error: "Falha ao autenticar máquina" }, 500, request.headers.get("origin"), CORS_METHODS)
return jsonWithCors({ error: "Falha ao autenticar máquina" }, 500, origin, CORS_METHODS)
}
}

View file

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server"
import { createMachineSession } from "@/server/machines-session"
import { createMachineSession, MachineInactiveError } from "@/server/machines-session"
import { env } from "@/lib/env"
const ERROR_TEMPLATE = `
@ -30,6 +30,36 @@ const ERROR_TEMPLATE = `
</html>
`
const INACTIVE_TEMPLATE = `
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Máquina desativada</title>
<style>
:root { color-scheme: dark light; }
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background-color: #0f172a; color: #e2e8f0; margin: 0; display: grid; place-items: center; min-height: 100vh; padding: 24px; }
main { width: min(480px, 100%); padding: 32px; border-radius: 16px; background: linear-gradient(145deg, rgba(15,23,42,0.85), rgba(15,23,42,0.65)); box-shadow: 0 18px 42px rgba(15, 23, 42, 0.45); text-align: center; backdrop-filter: blur(14px); border: 1px solid rgba(226, 232, 240, 0.08); }
h1 { margin: 0 0 12px; font-size: 1.7rem; letter-spacing: -0.01em; }
p { margin: 8px 0; line-height: 1.6; color: rgba(226,232,240,0.88); }
a { display: inline-flex; align-items: center; justify-content: center; gap: 8px; margin-top: 22px; padding: 12px 18px; border-radius: 999px; background-color: #00d6eb; color: #0f172a; text-decoration: none; font-weight: 600; transition: background-color 0.2s ease, transform 0.2s ease; }
a:hover { background-color: #1af0ff; transform: translateY(-1px); }
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 999px; background: rgba(248, 113, 113, 0.12); color: #fecaca; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 12px; }
</style>
</head>
<body>
<main>
<div class="badge">Acesso bloqueado</div>
<h1>Esta máquina está desativada</h1>
<p>O acesso ao portal foi suspenso pelos administradores da Rever. Enquanto isso, você não poderá abrir chamados ou enviar atualizações.</p>
<p>Entre em contato com a equipe da Rever para solicitar a reativação desta máquina.</p>
<a href="mailto:suporte@rever.com.br">Falar com o suporte</a>
</main>
</body>
</html>
`
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get("token")
if (!token) {
@ -124,6 +154,14 @@ export async function GET(request: NextRequest) {
return response
} catch (error) {
if (error instanceof MachineInactiveError) {
return new NextResponse(INACTIVE_TEMPLATE, {
status: 423,
headers: {
"Content-Type": "text/html; charset=utf-8",
},
})
}
console.error("[machines.handshake] Falha ao autenticar máquina", error)
return new NextResponse(ERROR_TEMPLATE, {
status: 500,

View file

@ -44,10 +44,15 @@ export function PortalTicketForm() {
const [categoryId, setCategoryId] = useState<string | null>(null)
const [subcategoryId, setSubcategoryId] = useState<string | null>(null)
const [attachments, setAttachments] = useState<Array<{ storageId: string; name: string; size?: number; type?: string }>>([])
const attachmentsTotalBytes = useMemo(
() => attachments.reduce((acc, item) => acc + (item.size ?? 0), 0),
[attachments]
)
const [isSubmitting, setIsSubmitting] = useState(false)
const machineInactive = machineContext?.isActive === false
const isFormValid = useMemo(() => {
return Boolean(subject.trim() && description.trim() && categoryId && subcategoryId)
}, [subject, description, categoryId, subcategoryId])
return Boolean(subject.trim() && description.trim() && categoryId && subcategoryId && !machineInactive)
}, [subject, description, categoryId, subcategoryId, machineInactive])
const isViewerReady = Boolean(viewerId)
const viewerErrorMessage = useMemo(() => {
if (!machineContextError) return null
@ -58,10 +63,14 @@ export function PortalTicketForm() {
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
if (isSubmitting || !isFormValid) return
if (machineInactive) {
toast.error("Esta máquina está desativada no momento. Reative-a para abrir novos chamados.", { id: "portal-new-ticket" })
return
}
if (!viewerId) {
const detail = viewerErrorMessage ? ` Detalhes: ${viewerErrorMessage}` : ""
toast.error(
`N<EFBFBD>o foi poss<73>vel identificar o colaborador vinculado a esta m<>quina. Tente abrir novamente o portal ou contate o suporte.${detail}`,
`Não foi possível identificar o colaborador vinculado a esta máquina. Tente abrir novamente o portal ou contate o suporte.${detail}`,
{ id: "portal-new-ticket" }
)
return
@ -127,6 +136,11 @@ export function PortalTicketForm() {
<CardTitle className="text-xl font-semibold text-neutral-900">Abrir novo chamado</CardTitle>
</CardHeader>
<CardContent className="space-y-6 px-5 pb-6">
{machineInactive ? (
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
Esta máquina foi desativada pelos administradores e não pode abrir novos chamados até ser reativada.
</div>
) : null}
{!isViewerReady ? (
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
Vincule esta máquina a um colaborador na aplicação desktop para enviar chamados em nome dele.
@ -151,6 +165,7 @@ export function PortalTicketForm() {
value={subject}
onChange={(event) => setSubject(event.target.value)}
placeholder="Ex.: Problema de acesso ao sistema"
disabled={machineInactive || isSubmitting}
required
/>
</div>
@ -163,6 +178,7 @@ export function PortalTicketForm() {
value={summary}
onChange={(event) => setSummary(event.target.value)}
placeholder="Descreva rapidamente o que está acontecendo"
disabled={machineInactive || isSubmitting}
/>
</div>
<div className="space-y-1">
@ -174,6 +190,7 @@ export function PortalTicketForm() {
onChange={(html) => setDescription(html)}
placeholder="Compartilhe passos para reproduzir, mensagens de erro ou informações adicionais."
className="rounded-2xl border border-slate-200 shadow-sm focus-within:border-neutral-900 focus-within:ring-neutral-900/20"
disabled={machineInactive || isSubmitting}
/>
</div>
</div>
@ -195,6 +212,9 @@ export function PortalTicketForm() {
<Dropzone
onUploaded={(files) => setAttachments((prev) => [...prev, ...files])}
className="rounded-xl border border-dashed border-slate-300 bg-slate-50 px-3 py-4 text-sm text-neutral-600 shadow-inner"
currentFileCount={attachments.length}
currentTotalBytes={attachmentsTotalBytes}
disabled={!isViewerReady || machineInactive || isSubmitting}
/>
<p className="text-xs text-neutral-500">
Formatos comuns de imagens e documentos são aceitos.
@ -208,12 +228,13 @@ export function PortalTicketForm() {
variant="outline"
onClick={() => router.push("/portal/tickets")}
className="rounded-full border-slate-300 px-6 text-sm font-semibold text-neutral-700 hover:bg-neutral-100"
disabled={isSubmitting}
>
Cancelar
</Button>
<Button
type="submit"
disabled={!isFormValid || isSubmitting}
disabled={!isFormValid || isSubmitting || machineInactive}
className="rounded-full bg-neutral-900 px-6 text-sm font-semibold text-white hover:bg-neutral-900/90"
>
Registrar chamado

View file

@ -24,11 +24,19 @@ export type MachineSessionContext = {
assignedUserEmail: string | null
assignedUserName: string | null
assignedUserRole: string | null
isActive: boolean
}
headers: Headers
response: unknown
}
export class MachineInactiveError extends Error {
constructor(message = "Máquina desativada") {
super(message)
this.name = "MachineInactiveError"
}
}
export async function createMachineSession(machineToken: string, rememberMe = true): Promise<MachineSessionContext> {
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
@ -40,6 +48,11 @@ export async function createMachineSession(machineToken: string, rememberMe = tr
const resolved = await client.mutation(api.machines.resolveToken, { machineToken })
let machineEmail = resolved.machine.authEmail ?? null
const machineActive = resolved.machine.isActive ?? true
if (!machineActive) {
throw new MachineInactiveError()
}
if (!machineEmail) {
const account = await ensureMachineAccount({
machineId: resolved.machine._id,
@ -84,6 +97,7 @@ export async function createMachineSession(machineToken: string, rememberMe = tr
assignedUserEmail: resolved.machine.assignedUserEmail ?? null,
assignedUserName: resolved.machine.assignedUserName ?? null,
assignedUserRole: resolved.machine.assignedUserRole ?? null,
isActive: machineActive,
},
headers: signIn.headers,
response: signIn.response,