feat(desktop-agent,admin/inventory): secure token storage via keyring; extended inventory collectors per OS; new /api/machines/inventory endpoint; posture rules + tickets; Admin UI inventory with filters, search and export; docs + CI desktop release

This commit is contained in:
Esdras Renan 2025-10-09 22:08:20 -03:00
parent c2050f311a
commit 479c66d52c
18 changed files with 1205 additions and 38 deletions

View file

@ -1,5 +1,6 @@
import { invoke } from "@tauri-apps/api/core"
import { Store } from "@tauri-apps/plugin-store"
import { getPassword, setPassword, deletePassword } from "@tauri-apps/plugin-keyring"
type MachineOs = {
name: string
@ -47,7 +48,6 @@ type MachineRegisterResponse = {
type AgentConfig = {
machineId: string
machineToken: string
tenantId?: string | null
companySlug?: string | null
machineEmail?: string | null
@ -70,7 +70,11 @@ declare global {
}
const STORE_FILENAME = "machine-agent.json"
const DEFAULT_APP_URL = "http://localhost:3000"
// Defaults: em produção, apontamos para o domínio público; em dev, localhost
const DEFAULT_APP_URL =
import.meta.env.MODE === "production"
? "https://tickets.esdrasrenan.com.br"
: "http://localhost:3000"
function normalizeUrl(value?: string | null, fallback = DEFAULT_APP_URL) {
const trimmed = (value ?? fallback).trim()
@ -81,7 +85,10 @@ function normalizeUrl(value?: string | null, fallback = DEFAULT_APP_URL) {
}
const appUrl = normalizeUrl(import.meta.env.VITE_APP_URL, DEFAULT_APP_URL)
const apiBaseUrl = normalizeUrl(import.meta.env.VITE_API_BASE_URL, appUrl)
const apiBaseUrl = normalizeUrl(
import.meta.env.VITE_API_BASE_URL,
appUrl
)
const alertElement = document.getElementById("alert-container") as HTMLDivElement | null
const contentElement = document.getElementById("content") as HTMLDivElement | null
@ -107,6 +114,9 @@ function setStatus(message: string) {
let storeInstance: Store | null = null
const KEYRING_SERVICE = "sistema-de-chamados"
const KEYRING_ACCOUNT = "machine-token"
async function ensureStoreLoaded(): Promise<Store> {
if (!storeInstance) {
try {
@ -141,6 +151,24 @@ async function clearConfig() {
const store = await ensureStoreLoaded()
await store.delete("config")
await store.save()
try {
await deletePassword({ service: KEYRING_SERVICE, account: KEYRING_ACCOUNT })
} catch {
// ignore
}
}
async function getMachineToken(): Promise<string | null> {
try {
const token = await getPassword({ service: KEYRING_SERVICE, account: KEYRING_ACCOUNT })
return token && token.length > 0 ? token : null
} catch {
return null
}
}
async function setMachineToken(token: string) {
await setPassword({ service: KEYRING_SERVICE, account: KEYRING_ACCOUNT, password: token })
}
async function collectMachineProfile(): Promise<MachineProfile> {
@ -148,9 +176,11 @@ async function collectMachineProfile(): Promise<MachineProfile> {
}
async function startHeartbeat(config: AgentConfig) {
const token = await getMachineToken()
if (!token) throw new Error("Token da máquina ausente no cofre seguro")
await invoke("start_machine_agent", {
baseUrl: config.apiBaseUrl,
token: config.machineToken,
token,
status: "online",
intervalSeconds: 300,
})
@ -348,9 +378,10 @@ async function handleRegister(profile: MachineProfile, form: HTMLFormElement) {
}
const data = (await response.json()) as MachineRegisterResponse
// Guarda token com segurança no Keyring
await setMachineToken(data.machineToken)
const config: AgentConfig = {
machineId: data.machineId,
machineToken: data.machineToken,
tenantId: data.tenantId ?? null,
companySlug: data.companySlug ?? null,
machineEmail: data.machineEmail ?? null,
@ -387,8 +418,16 @@ async function handleRegister(profile: MachineProfile, form: HTMLFormElement) {
}
function redirectToApp(config: AgentConfig) {
const url = `${config.appUrl}/machines/handshake?token=${encodeURIComponent(config.machineToken)}`
window.location.replace(url)
const perform = async () => {
const token = await getMachineToken()
if (!token) {
setAlert("Token da máquina não encontrado. Reprovisione a máquina.", "error")
return
}
const url = `${config.appUrl}/machines/handshake?token=${encodeURIComponent(token)}`
window.location.replace(url)
}
void perform()
}
async function ensureHeartbeat(config: AgentConfig): Promise<AgentConfig> {
@ -411,7 +450,8 @@ async function bootstrap() {
try {
const stored = await loadConfig()
if (stored?.machineToken) {
const token = await getMachineToken()
if (stored && token) {
const updated = await ensureHeartbeat(stored)
renderRegistered(updated)
return