Impede acesso ao portal para máquinas desativadas
This commit is contained in:
parent
0e97e4c0d6
commit
e5085962e9
5 changed files with 195 additions and 18 deletions
|
|
@ -7,6 +7,7 @@ import { appLocalDataDir, executableDir, join } from "@tauri-apps/api/path"
|
||||||
import { ExternalLink, Eye, EyeOff, GalleryVerticalEnd, Loader2, RefreshCw } from "lucide-react"
|
import { ExternalLink, Eye, EyeOff, GalleryVerticalEnd, Loader2, RefreshCw } from "lucide-react"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"
|
||||||
import { cn } from "./lib/utils"
|
import { cn } from "./lib/utils"
|
||||||
|
import { DeactivationScreen } from "./components/DeactivationScreen"
|
||||||
|
|
||||||
type MachineOs = {
|
type MachineOs = {
|
||||||
name: string
|
name: string
|
||||||
|
|
@ -137,14 +138,54 @@ function bytes(n?: number) {
|
||||||
|
|
||||||
function pct(p?: number) { return !p && p !== 0 ? "—" : `${p.toFixed(0)}%` }
|
function pct(p?: number) { return !p && p !== 0 ? "—" : `${p.toFixed(0)}%` }
|
||||||
|
|
||||||
|
type MachineStatePayload = {
|
||||||
|
isActive?: boolean | null
|
||||||
|
metadata?: Record<string, unknown> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractActiveFromMetadata(metadata: unknown): boolean {
|
||||||
|
if (!metadata || typeof metadata !== "object") return true
|
||||||
|
const record = metadata as Record<string, unknown>
|
||||||
|
const direct = record["isActive"]
|
||||||
|
if (typeof direct === "boolean") return direct
|
||||||
|
const state = record["state"]
|
||||||
|
if (state && typeof state === "object") {
|
||||||
|
const nested = state as Record<string, unknown>
|
||||||
|
const active = nested["isActive"] ?? nested["active"] ?? nested["enabled"]
|
||||||
|
if (typeof active === "boolean") return active
|
||||||
|
}
|
||||||
|
const flags = record["flags"]
|
||||||
|
if (flags && typeof flags === "object") {
|
||||||
|
const nested = flags as Record<string, unknown>
|
||||||
|
const active = nested["isActive"] ?? nested["active"]
|
||||||
|
if (typeof active === "boolean") return active
|
||||||
|
}
|
||||||
|
const status = record["status"]
|
||||||
|
if (typeof status === "string") {
|
||||||
|
const normalized = status.trim().toLowerCase()
|
||||||
|
if (["deactivated", "desativada", "desativado", "inactive", "inativo", "disabled"].includes(normalized)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMachineActive(machine?: MachineStatePayload | null): boolean {
|
||||||
|
if (!machine) return true
|
||||||
|
if (typeof machine.isActive === "boolean") return machine.isActive
|
||||||
|
return extractActiveFromMetadata(machine.metadata)
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [store, setStore] = useState<Store | null>(null)
|
const [store, setStore] = useState<Store | null>(null)
|
||||||
const [token, setToken] = useState<string | null>(null)
|
const [token, setToken] = useState<string | null>(null)
|
||||||
const [config, setConfig] = useState<AgentConfig | null>(null)
|
const [config, setConfig] = useState<AgentConfig | null>(null)
|
||||||
const [profile, setProfile] = useState<MachineProfile | null>(null)
|
const [profile, setProfile] = useState<MachineProfile | null>(null)
|
||||||
|
const [logoSrc, setLogoSrc] = useState<string>(() => `${appUrl}/logo-raven.png`)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const [status, setStatus] = useState<string | null>(null)
|
const [status, setStatus] = useState<string | null>(null)
|
||||||
|
const [isMachineActive, setIsMachineActive] = useState(true)
|
||||||
const [showSecret, setShowSecret] = useState(false)
|
const [showSecret, setShowSecret] = useState(false)
|
||||||
const [isLaunchingSystem, setIsLaunchingSystem] = useState(false)
|
const [isLaunchingSystem, setIsLaunchingSystem] = useState(false)
|
||||||
const [, setIsValidatingToken] = useState(false)
|
const [, setIsValidatingToken] = useState(false)
|
||||||
|
|
@ -164,6 +205,7 @@ function App() {
|
||||||
})
|
})
|
||||||
const autoLaunchRef = useRef(false)
|
const autoLaunchRef = useRef(false)
|
||||||
const autoUpdateRef = useRef(false)
|
const autoUpdateRef = useRef(false)
|
||||||
|
const logoFallbackRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -214,6 +256,14 @@ function App() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Falha ao iniciar heartbeat em segundo plano", err)
|
console.error("Falha ao iniciar heartbeat em segundo plano", err)
|
||||||
}
|
}
|
||||||
|
const payload = await res.clone().json().catch(() => null)
|
||||||
|
if (payload && typeof payload === "object" && "machine" in payload) {
|
||||||
|
const machineData = (payload as { machine?: MachineStatePayload }).machine
|
||||||
|
if (machineData) {
|
||||||
|
const currentActive = resolveMachineActive(machineData)
|
||||||
|
setIsMachineActive(currentActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
|
|
@ -231,6 +281,7 @@ function App() {
|
||||||
setToken(null)
|
setToken(null)
|
||||||
setConfig(null)
|
setConfig(null)
|
||||||
setStatus(null)
|
setStatus(null)
|
||||||
|
setIsMachineActive(true)
|
||||||
setError("Este dispositivo precisa ser reprovisionado. Informe o código de provisionamento.")
|
setError("Este dispositivo precisa ser reprovisionado. Informe o código de provisionamento.")
|
||||||
try {
|
try {
|
||||||
const p = await invoke<MachineProfile>("collect_machine_profile")
|
const p = await invoke<MachineProfile>("collect_machine_profile")
|
||||||
|
|
@ -493,13 +544,41 @@ function App() {
|
||||||
setIsLaunchingSystem(true)
|
setIsLaunchingSystem(true)
|
||||||
try {
|
try {
|
||||||
// Tenta criar a sessão via API (evita dependência de redirecionamento + cookies em 3xx)
|
// Tenta criar a sessão via API (evita dependência de redirecionamento + cookies em 3xx)
|
||||||
const res = await fetch(`${apiBaseUrl}/api/machines/sessions`, {
|
const res = await fetch(`${apiBaseUrl}/api/machines/sessions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ machineToken: token, rememberMe: true }),
|
body: JSON.stringify({ machineToken: token, rememberMe: true }),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (res.ok) {
|
||||||
|
const payload = await res.clone().json().catch(() => null)
|
||||||
|
if (payload && typeof payload === "object" && "machine" in payload) {
|
||||||
|
const machineData = (payload as { machine?: MachineStatePayload }).machine
|
||||||
|
if (machineData) {
|
||||||
|
const currentActive = resolveMachineActive(machineData)
|
||||||
|
setIsMachineActive(currentActive)
|
||||||
|
if (currentActive) {
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
if (!currentActive) {
|
||||||
|
setError("Esta máquina está desativada. Entre em contato com o suporte da Rever para reativar o acesso.")
|
||||||
|
setIsLaunchingSystem(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (res.status === 423) {
|
||||||
|
const payload = await res.clone().json().catch(() => null)
|
||||||
|
const message =
|
||||||
|
payload && typeof payload === "object" && typeof (payload as { error?: unknown }).error === "string"
|
||||||
|
? ((payload as { error?: string }).error ?? "").trim()
|
||||||
|
: ""
|
||||||
|
setIsMachineActive(false)
|
||||||
|
setIsLaunchingSystem(false)
|
||||||
|
setError(message.length > 0 ? message : "Esta máquina está desativada. Entre em contato com o suporte da Rever.")
|
||||||
|
return
|
||||||
|
}
|
||||||
// Se sessão falhar, tenta identificar token inválido/expirado
|
// Se sessão falhar, tenta identificar token inválido/expirado
|
||||||
try {
|
try {
|
||||||
const hb = await fetch(`${apiBaseUrl}/api/machines/heartbeat`, {
|
const hb = await fetch(`${apiBaseUrl}/api/machines/heartbeat`, {
|
||||||
|
|
@ -522,6 +601,7 @@ function App() {
|
||||||
setToken(null)
|
setToken(null)
|
||||||
setConfig(null)
|
setConfig(null)
|
||||||
setStatus(null)
|
setStatus(null)
|
||||||
|
setIsMachineActive(true)
|
||||||
setError("Sessão expirada. Reprovisione a máquina para continuar.")
|
setError("Sessão expirada. Reprovisione a máquina para continuar.")
|
||||||
setIsLaunchingSystem(false)
|
setIsLaunchingSystem(false)
|
||||||
const p = await invoke<MachineProfile>("collect_machine_profile")
|
const p = await invoke<MachineProfile>("collect_machine_profile")
|
||||||
|
|
@ -667,7 +747,24 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen grid place-items-center p-6">
|
<div className="min-h-screen grid place-items-center p-6">
|
||||||
|
{token && !isMachineActive ? (
|
||||||
|
<DeactivationScreen companyName={companyName} />
|
||||||
|
) : (
|
||||||
<div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
<div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||||
|
<div className="mb-4 flex justify-center">
|
||||||
|
<img
|
||||||
|
src={logoSrc}
|
||||||
|
alt="Logotipo Raven"
|
||||||
|
width={160}
|
||||||
|
height={160}
|
||||||
|
className="h-16 w-auto md:h-20"
|
||||||
|
onError={() => {
|
||||||
|
if (logoFallbackRef.current) return
|
||||||
|
logoFallbackRef.current = true
|
||||||
|
setLogoSrc(`${appUrl}/raven.png`)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="mb-2 flex items-center justify-between gap-3">
|
<div className="mb-2 flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="flex size-8 items-center justify-center rounded-lg bg-neutral-900 text-white"><GalleryVerticalEnd className="size-4" /></span>
|
<span className="flex size-8 items-center justify-center rounded-lg bg-neutral-900 text-white"><GalleryVerticalEnd className="size-4" /></span>
|
||||||
|
|
@ -869,9 +966,7 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex justify-center">
|
)}
|
||||||
<img src={`${appUrl}/raven.png`} alt="Logotipo Raven" width={110} height={110} className="h-[3.45rem] w-auto" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { z } from "zod"
|
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 { applyCorsHeaders, createCorsPreflight, jsonWithCors } from "@/server/cors"
|
||||||
import {
|
import {
|
||||||
MACHINE_CTX_COOKIE,
|
MACHINE_CTX_COOKIE,
|
||||||
|
|
@ -20,8 +20,9 @@ export async function OPTIONS(request: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
const origin = request.headers.get("origin")
|
||||||
if (request.method !== "POST") {
|
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
|
let payload
|
||||||
|
|
@ -124,7 +125,15 @@ export async function POST(request: Request) {
|
||||||
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} 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)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
import { createMachineSession } from "@/server/machines-session"
|
import { createMachineSession, MachineInactiveError } from "@/server/machines-session"
|
||||||
import { env } from "@/lib/env"
|
import { env } from "@/lib/env"
|
||||||
|
|
||||||
const ERROR_TEMPLATE = `
|
const ERROR_TEMPLATE = `
|
||||||
|
|
@ -30,6 +30,36 @@ const ERROR_TEMPLATE = `
|
||||||
</html>
|
</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) {
|
export async function GET(request: NextRequest) {
|
||||||
const token = request.nextUrl.searchParams.get("token")
|
const token = request.nextUrl.searchParams.get("token")
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
@ -124,6 +154,14 @@ export async function GET(request: NextRequest) {
|
||||||
|
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} 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)
|
console.error("[machines.handshake] Falha ao autenticar máquina", error)
|
||||||
return new NextResponse(ERROR_TEMPLATE, {
|
return new NextResponse(ERROR_TEMPLATE, {
|
||||||
status: 500,
|
status: 500,
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,15 @@ export function PortalTicketForm() {
|
||||||
const [categoryId, setCategoryId] = useState<string | null>(null)
|
const [categoryId, setCategoryId] = useState<string | null>(null)
|
||||||
const [subcategoryId, setSubcategoryId] = 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 [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 [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const machineInactive = machineContext?.isActive === false
|
||||||
const isFormValid = useMemo(() => {
|
const isFormValid = useMemo(() => {
|
||||||
return Boolean(subject.trim() && description.trim() && categoryId && subcategoryId)
|
return Boolean(subject.trim() && description.trim() && categoryId && subcategoryId && !machineInactive)
|
||||||
}, [subject, description, categoryId, subcategoryId])
|
}, [subject, description, categoryId, subcategoryId, machineInactive])
|
||||||
const isViewerReady = Boolean(viewerId)
|
const isViewerReady = Boolean(viewerId)
|
||||||
const viewerErrorMessage = useMemo(() => {
|
const viewerErrorMessage = useMemo(() => {
|
||||||
if (!machineContextError) return null
|
if (!machineContextError) return null
|
||||||
|
|
@ -58,10 +63,14 @@ export function PortalTicketForm() {
|
||||||
async function handleSubmit(event: React.FormEvent) {
|
async function handleSubmit(event: React.FormEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (isSubmitting || !isFormValid) return
|
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) {
|
if (!viewerId) {
|
||||||
const detail = viewerErrorMessage ? ` Detalhes: ${viewerErrorMessage}` : ""
|
const detail = viewerErrorMessage ? ` Detalhes: ${viewerErrorMessage}` : ""
|
||||||
toast.error(
|
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" }
|
{ id: "portal-new-ticket" }
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -127,6 +136,11 @@ export function PortalTicketForm() {
|
||||||
<CardTitle className="text-xl font-semibold text-neutral-900">Abrir novo chamado</CardTitle>
|
<CardTitle className="text-xl font-semibold text-neutral-900">Abrir novo chamado</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6 px-5 pb-6">
|
<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 ? (
|
{!isViewerReady ? (
|
||||||
<div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
|
<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.
|
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}
|
value={subject}
|
||||||
onChange={(event) => setSubject(event.target.value)}
|
onChange={(event) => setSubject(event.target.value)}
|
||||||
placeholder="Ex.: Problema de acesso ao sistema"
|
placeholder="Ex.: Problema de acesso ao sistema"
|
||||||
|
disabled={machineInactive || isSubmitting}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -163,6 +178,7 @@ export function PortalTicketForm() {
|
||||||
value={summary}
|
value={summary}
|
||||||
onChange={(event) => setSummary(event.target.value)}
|
onChange={(event) => setSummary(event.target.value)}
|
||||||
placeholder="Descreva rapidamente o que está acontecendo"
|
placeholder="Descreva rapidamente o que está acontecendo"
|
||||||
|
disabled={machineInactive || isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -174,6 +190,7 @@ export function PortalTicketForm() {
|
||||||
onChange={(html) => setDescription(html)}
|
onChange={(html) => setDescription(html)}
|
||||||
placeholder="Compartilhe passos para reproduzir, mensagens de erro ou informações adicionais."
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -195,6 +212,9 @@ export function PortalTicketForm() {
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onUploaded={(files) => setAttachments((prev) => [...prev, ...files])}
|
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"
|
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">
|
<p className="text-xs text-neutral-500">
|
||||||
Formatos comuns de imagens e documentos são aceitos.
|
Formatos comuns de imagens e documentos são aceitos.
|
||||||
|
|
@ -208,12 +228,13 @@ export function PortalTicketForm() {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => router.push("/portal/tickets")}
|
onClick={() => router.push("/portal/tickets")}
|
||||||
className="rounded-full border-slate-300 px-6 text-sm font-semibold text-neutral-700 hover:bg-neutral-100"
|
className="rounded-full border-slate-300 px-6 text-sm font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
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"
|
className="rounded-full bg-neutral-900 px-6 text-sm font-semibold text-white hover:bg-neutral-900/90"
|
||||||
>
|
>
|
||||||
Registrar chamado
|
Registrar chamado
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,19 @@ export type MachineSessionContext = {
|
||||||
assignedUserEmail: string | null
|
assignedUserEmail: string | null
|
||||||
assignedUserName: string | null
|
assignedUserName: string | null
|
||||||
assignedUserRole: string | null
|
assignedUserRole: string | null
|
||||||
|
isActive: boolean
|
||||||
}
|
}
|
||||||
headers: Headers
|
headers: Headers
|
||||||
response: unknown
|
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> {
|
export async function createMachineSession(machineToken: string, rememberMe = true): Promise<MachineSessionContext> {
|
||||||
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||||
if (!convexUrl) {
|
if (!convexUrl) {
|
||||||
|
|
@ -40,6 +48,11 @@ export async function createMachineSession(machineToken: string, rememberMe = tr
|
||||||
const resolved = await client.mutation(api.machines.resolveToken, { machineToken })
|
const resolved = await client.mutation(api.machines.resolveToken, { machineToken })
|
||||||
let machineEmail = resolved.machine.authEmail ?? null
|
let machineEmail = resolved.machine.authEmail ?? null
|
||||||
|
|
||||||
|
const machineActive = resolved.machine.isActive ?? true
|
||||||
|
if (!machineActive) {
|
||||||
|
throw new MachineInactiveError()
|
||||||
|
}
|
||||||
|
|
||||||
if (!machineEmail) {
|
if (!machineEmail) {
|
||||||
const account = await ensureMachineAccount({
|
const account = await ensureMachineAccount({
|
||||||
machineId: resolved.machine._id,
|
machineId: resolved.machine._id,
|
||||||
|
|
@ -84,6 +97,7 @@ export async function createMachineSession(machineToken: string, rememberMe = tr
|
||||||
assignedUserEmail: resolved.machine.assignedUserEmail ?? null,
|
assignedUserEmail: resolved.machine.assignedUserEmail ?? null,
|
||||||
assignedUserName: resolved.machine.assignedUserName ?? null,
|
assignedUserName: resolved.machine.assignedUserName ?? null,
|
||||||
assignedUserRole: resolved.machine.assignedUserRole ?? null,
|
assignedUserRole: resolved.machine.assignedUserRole ?? null,
|
||||||
|
isActive: machineActive,
|
||||||
},
|
},
|
||||||
headers: signIn.headers,
|
headers: signIn.headers,
|
||||||
response: signIn.response,
|
response: signIn.response,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue