Add USB storage device control feature
- Add USB policy fields to machines schema (policy, status, error) - Create usbPolicyEvents table for audit logging - Implement Convex mutations/queries for USB policy management - Add REST API endpoints for desktop agent communication - Create Rust usb_control module for Windows registry manipulation - Integrate USB policy check in agent heartbeat loop - Add USB policy control component in admin device overview - Add localhost:3001 to auth trustedOrigins for dev 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0e9310d6e4
commit
49aa143a80
11 changed files with 1116 additions and 1 deletions
296
src/components/admin/devices/usb-policy-control.tsx
Normal file
296
src/components/admin/devices/usb-policy-control.tsx
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useMutation, useQuery } from "convex/react"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Usb, Shield, ShieldOff, ShieldAlert, Clock, CheckCircle2, XCircle, Loader2, History } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
|
||||
type UsbPolicyValue = "ALLOW" | "BLOCK_ALL" | "READONLY"
|
||||
|
||||
const POLICY_OPTIONS: Array<{ value: UsbPolicyValue; label: string; description: string; icon: typeof Shield }> = [
|
||||
{
|
||||
value: "ALLOW",
|
||||
label: "Permitido",
|
||||
description: "Acesso total a dispositivos USB de armazenamento",
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
value: "BLOCK_ALL",
|
||||
label: "Bloqueado",
|
||||
description: "Nenhum acesso a dispositivos USB de armazenamento",
|
||||
icon: ShieldOff,
|
||||
},
|
||||
{
|
||||
value: "READONLY",
|
||||
label: "Somente leitura",
|
||||
description: "Permite leitura, bloqueia escrita em dispositivos USB",
|
||||
icon: ShieldAlert,
|
||||
},
|
||||
]
|
||||
|
||||
function getPolicyConfig(policy: string | undefined | null) {
|
||||
return POLICY_OPTIONS.find((opt) => opt.value === policy) ?? POLICY_OPTIONS[0]
|
||||
}
|
||||
|
||||
function getStatusBadge(status: string | undefined | null) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1 border-amber-200 bg-amber-50 text-amber-700">
|
||||
<Clock className="size-3" />
|
||||
Pendente
|
||||
</Badge>
|
||||
)
|
||||
case "APPLIED":
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1 border-emerald-200 bg-emerald-50 text-emerald-700">
|
||||
<CheckCircle2 className="size-3" />
|
||||
Aplicado
|
||||
</Badge>
|
||||
)
|
||||
case "FAILED":
|
||||
return (
|
||||
<Badge variant="outline" className="gap-1 border-red-200 bg-red-50 text-red-700">
|
||||
<XCircle className="size-3" />
|
||||
Falhou
|
||||
</Badge>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface UsbPolicyControlProps {
|
||||
machineId: string
|
||||
machineName?: string
|
||||
actorEmail?: string
|
||||
actorName?: string
|
||||
actorId?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function UsbPolicyControl({
|
||||
machineId,
|
||||
machineName,
|
||||
actorEmail,
|
||||
actorName,
|
||||
actorId,
|
||||
disabled = false,
|
||||
}: UsbPolicyControlProps) {
|
||||
const [selectedPolicy, setSelectedPolicy] = useState<UsbPolicyValue>("ALLOW")
|
||||
const [isApplying, setIsApplying] = useState(false)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
|
||||
const usbPolicy = useQuery(api.usbPolicy.getUsbPolicy, {
|
||||
machineId: machineId as Id<"machines">,
|
||||
})
|
||||
|
||||
const policyEvents = useQuery(
|
||||
api.usbPolicy.listUsbPolicyEvents,
|
||||
showHistory ? { machineId: machineId as Id<"machines">, limit: 10 } : "skip"
|
||||
)
|
||||
|
||||
const setUsbPolicyMutation = useMutation(api.usbPolicy.setUsbPolicy)
|
||||
|
||||
useEffect(() => {
|
||||
if (usbPolicy?.policy) {
|
||||
setSelectedPolicy(usbPolicy.policy as UsbPolicyValue)
|
||||
}
|
||||
}, [usbPolicy?.policy])
|
||||
|
||||
const currentConfig = getPolicyConfig(usbPolicy?.policy)
|
||||
const CurrentIcon = currentConfig.icon
|
||||
|
||||
const handleApplyPolicy = async () => {
|
||||
if (selectedPolicy === usbPolicy?.policy) {
|
||||
toast.info("A politica selecionada ja esta aplicada.")
|
||||
return
|
||||
}
|
||||
|
||||
setIsApplying(true)
|
||||
try {
|
||||
await setUsbPolicyMutation({
|
||||
machineId: machineId as Id<"machines">,
|
||||
policy: selectedPolicy,
|
||||
actorId: actorId ? (actorId as Id<"users">) : undefined,
|
||||
actorEmail,
|
||||
actorName,
|
||||
})
|
||||
toast.success("Politica USB enviada para aplicacao.")
|
||||
} catch (error) {
|
||||
console.error("[usb-policy] Falha ao aplicar politica", error)
|
||||
toast.error("Falha ao aplicar politica USB. Tente novamente.")
|
||||
} finally {
|
||||
setIsApplying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatEventDate = (timestamp: number) => {
|
||||
return formatDistanceToNow(new Date(timestamp), {
|
||||
addSuffix: true,
|
||||
locale: ptBR,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Usb className="size-5 text-neutral-500" />
|
||||
<CardTitle className="text-base">Controle USB</CardTitle>
|
||||
</div>
|
||||
{usbPolicy?.status && getStatusBadge(usbPolicy.status)}
|
||||
</div>
|
||||
<CardDescription>
|
||||
Gerencie o acesso a dispositivos de armazenamento USB neste dispositivo.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-3 rounded-lg border bg-neutral-50 p-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-full bg-white shadow-sm">
|
||||
<CurrentIcon className="size-5 text-neutral-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{currentConfig.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{currentConfig.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{usbPolicy?.error && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<p className="text-sm font-medium text-red-700">Erro na aplicacao</p>
|
||||
<p className="text-xs text-red-600">{usbPolicy.error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">Alterar politica</label>
|
||||
<Select
|
||||
value={selectedPolicy}
|
||||
onValueChange={(value) => setSelectedPolicy(value as UsbPolicyValue)}
|
||||
disabled={disabled || isApplying}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione uma politica" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{POLICY_OPTIONS.map((option) => {
|
||||
const Icon = option.icon
|
||||
return (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="size-4" />
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={handleApplyPolicy}
|
||||
disabled={disabled || isApplying || selectedPolicy === usbPolicy?.policy}
|
||||
size="default"
|
||||
>
|
||||
{isApplying ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
Aplicando...
|
||||
</>
|
||||
) : (
|
||||
"Aplicar"
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{selectedPolicy === usbPolicy?.policy
|
||||
? "A politica ja esta aplicada"
|
||||
: `Aplicar politica "${getPolicyConfig(selectedPolicy).label}"`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2 text-muted-foreground"
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
>
|
||||
<History className="size-4" />
|
||||
{showHistory ? "Ocultar historico" : "Ver historico de alteracoes"}
|
||||
</Button>
|
||||
|
||||
{showHistory && policyEvents && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{policyEvents.length === 0 ? (
|
||||
<p className="text-center text-xs text-muted-foreground py-2">
|
||||
Nenhuma alteracao registrada
|
||||
</p>
|
||||
) : (
|
||||
policyEvents.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex items-start justify-between rounded-md border bg-white p-2 text-xs"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium">
|
||||
{getPolicyConfig(event.oldPolicy).label}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="font-medium">
|
||||
{getPolicyConfig(event.newPolicy).label}
|
||||
</span>
|
||||
{getStatusBadge(event.status)}
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{event.actorName ?? event.actorEmail ?? "Sistema"} · {formatEventDate(event.createdAt)}
|
||||
</p>
|
||||
{event.error && (
|
||||
<p className="text-red-600">{event.error}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{usbPolicy?.reportedAt && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Ultimo relato do agente: {formatEventDate(usbPolicy.reportedAt)}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue