From 07d631de40a242f287521cfa3c6e58aa17101235 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Fri, 7 Nov 2025 15:39:36 -0300 Subject: [PATCH] feat: integrar credenciais rustdesk aos acessos remotos --- convex/machines.ts | 29 ++- .../api/admin/devices/remote-access/route.ts | 4 + .../admin/devices/admin-devices-overview.tsx | 169 +++++++++++++++++- src/server/machines/inventory-export.ts | 36 +++- tests/devices/remote-access.test.ts | 10 ++ 5 files changed, 243 insertions(+), 5 deletions(-) diff --git a/convex/machines.ts b/convex/machines.ts index bc919b2..4f0a460 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -1991,6 +1991,8 @@ type RemoteAccessEntry = { provider: string identifier: string url: string | null + username: string | null + password: string | null notes: string | null lastVerifiedAt: number | null metadata: Record | null @@ -2032,6 +2034,8 @@ function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null { provider: "Remoto", identifier: isUrl ? trimmed : trimmed, url: isUrl ? trimmed : null, + username: null, + password: null, notes: null, lastVerifiedAt: null, metadata: null, @@ -2059,6 +2063,19 @@ function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null { null const resolvedIdentifier = identifier ?? url ?? "Acesso remoto" const notes = coerceString(record.notes) ?? coerceString(record.note) ?? coerceString(record.description) ?? coerceString(record.obs) ?? null + const username = + coerceString((record as Record).username) ?? + coerceString((record as Record).user) ?? + coerceString((record as Record).login) ?? + coerceString((record as Record).email) ?? + coerceString((record as Record).account) ?? + null + const password = + coerceString((record as Record).password) ?? + coerceString((record as Record).pass) ?? + coerceString((record as Record).secret) ?? + coerceString((record as Record).pin) ?? + null const timestamp = coerceNumber(record.lastVerifiedAt) ?? coerceNumber(record.verifiedAt) ?? @@ -2075,6 +2092,8 @@ function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null { provider, identifier: resolvedIdentifier, url, + username, + password, notes, lastVerifiedAt: timestamp, metadata, @@ -2105,12 +2124,14 @@ export const updateRemoteAccess = mutation({ provider: v.optional(v.string()), identifier: v.optional(v.string()), url: v.optional(v.string()), + username: v.optional(v.string()), + password: v.optional(v.string()), notes: v.optional(v.string()), action: v.optional(v.string()), entryId: v.optional(v.string()), clear: v.optional(v.boolean()), }, - handler: async (ctx, { machineId, actorId, provider, identifier, url, notes, action, entryId, clear }) => { + handler: async (ctx, { machineId, actorId, provider, identifier, url, username, password, notes, action, entryId, clear }) => { const machine = await ctx.db.get(machineId) if (!machine) { throw new ConvexError("Dispositivo não encontrada") @@ -2192,6 +2213,8 @@ export const updateRemoteAccess = mutation({ } const cleanedNotes = notes?.trim() ? notes.trim() : null + const cleanedUsername = username?.trim() ? username.trim() : null + const cleanedPassword = password?.trim() ? password.trim() : null const lastVerifiedAt = Date.now() const targetEntryId = coerceString(entryId) ?? @@ -2205,12 +2228,16 @@ export const updateRemoteAccess = mutation({ provider: trimmedProvider, identifier: trimmedIdentifier, url: normalizedUrl, + username: cleanedUsername, + password: cleanedPassword, notes: cleanedNotes, lastVerifiedAt, metadata: { provider: trimmedProvider, identifier: trimmedIdentifier, url: normalizedUrl, + username: cleanedUsername, + password: cleanedPassword, notes: cleanedNotes, lastVerifiedAt, }, diff --git a/src/app/api/admin/devices/remote-access/route.ts b/src/app/api/admin/devices/remote-access/route.ts index 62a4b36..cf7f169 100644 --- a/src/app/api/admin/devices/remote-access/route.ts +++ b/src/app/api/admin/devices/remote-access/route.ts @@ -13,6 +13,8 @@ const schema = z.object({ provider: z.string().optional(), identifier: z.string().optional(), url: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), notes: z.string().optional(), entryId: z.string().optional(), action: z.enum(["save", "upsert", "clear", "delete", "remove"]).optional(), @@ -106,6 +108,8 @@ export async function POST(request: Request) { if (provider) mutationArgs.provider = provider if (identifier) mutationArgs.identifier = identifier if (normalizedUrl !== undefined) mutationArgs.url = normalizedUrl + mutationArgs.username = (parsed.username ?? "").trim() + mutationArgs.password = (parsed.password ?? "").trim() if (notes.length) mutationArgs.notes = notes } diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index 553a22f..b2a1388 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -26,6 +26,9 @@ import { RotateCcw, AlertTriangle, Key, + Eye, + EyeOff, + MonitorSmartphone, Globe, Apple, Terminal, @@ -323,6 +326,8 @@ type DeviceRemoteAccessEntry = { clientId: string provider: string | null identifier: string | null + username: string | null + password: string | null url: string | null notes: string | null lastVerifiedAt: number | null @@ -332,6 +337,8 @@ type DeviceRemoteAccessEntry = { export type DeviceRemoteAccess = { provider: string | null identifier: string | null + username: string | null + password: string | null url: string | null notes: string | null lastVerifiedAt: number | null @@ -411,6 +418,8 @@ function normalizeDeviceRemoteAccessEntry(raw: unknown): DeviceRemoteAccessEntry const identifier = readString(record, "identifier", "code", "id", "accessId") ?? readString(record, "value", "label") + const username = readString(record, "username", "user", "login", "email", "account") ?? null + const password = readString(record, "password", "pass", "secret", "pin") ?? null const url = readString(record, "url", "link", "remoteUrl", "console", "viewer") ?? null const notes = readString(record, "notes", "note", "description", "obs") ?? null const timestampCandidate = @@ -423,6 +432,8 @@ function normalizeDeviceRemoteAccessEntry(raw: unknown): DeviceRemoteAccessEntry clientId: id ?? createRemoteAccessClientId(), provider, identifier: identifier ?? url ?? null, + username, + password, url, notes, lastVerifiedAt, @@ -433,8 +444,8 @@ function normalizeDeviceRemoteAccessEntry(raw: unknown): DeviceRemoteAccessEntry export function normalizeDeviceRemoteAccess(raw: unknown): DeviceRemoteAccess | null { const entry = normalizeDeviceRemoteAccessEntry(raw) if (!entry) return null - const { provider, identifier, url, notes, lastVerifiedAt, metadata } = entry - return { provider, identifier, url, notes, lastVerifiedAt, metadata } + const { provider, identifier, username, password, url, notes, lastVerifiedAt, metadata } = entry + return { provider, identifier, username, password, url, notes, lastVerifiedAt, metadata } } export function normalizeDeviceRemoteAccessList(raw: unknown): DeviceRemoteAccessEntry[] { @@ -464,6 +475,15 @@ const REMOTE_ACCESS_METADATA_IGNORED_KEYS = new Set([ "code", "id", "accessId", + "username", + "user", + "login", + "email", + "account", + "password", + "pass", + "secret", + "pin", "url", "link", "remoteUrl", @@ -515,6 +535,25 @@ function readText(record: Record, ...keys: string[]): string | return undefined } +function isRustDeskAccess(entry: DeviceRemoteAccessEntry | null | undefined) { + if (!entry) return false + const provider = (entry.provider ?? entry.metadata?.provider ?? "").toString().toLowerCase() + if (provider.includes("rustdesk")) return true + const url = (entry.url ?? entry.metadata?.url ?? "").toString().toLowerCase() + return url.includes("rustdesk") +} + +function buildRustDeskUri(entry: DeviceRemoteAccessEntry) { + const identifier = (entry.identifier ?? "").replace(/\s+/g, "") + if (!identifier) return null + const params = new URLSearchParams() + if (entry.password) { + params.set("password", entry.password) + } + const query = params.toString() + return `rustdesk://${encodeURIComponent(identifier)}${query ? `?${query}` : ""}` +} + function parseWindowsInstallDate(value: unknown): Date | null { if (!value) return null if (value instanceof Date) return value @@ -2999,9 +3038,12 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { ) const [remoteAccessCustomProvider, setRemoteAccessCustomProvider] = useState("") const [remoteAccessIdentifierInput, setRemoteAccessIdentifierInput] = useState("") + const [remoteAccessUsernameInput, setRemoteAccessUsernameInput] = useState("") + const [remoteAccessPasswordInput, setRemoteAccessPasswordInput] = useState("") const [remoteAccessUrlInput, setRemoteAccessUrlInput] = useState("") const [remoteAccessNotesInput, setRemoteAccessNotesInput] = useState("") const [remoteAccessSaving, setRemoteAccessSaving] = useState(false) + const [visibleRemoteSecrets, setVisibleRemoteSecrets] = useState>({}) const editingRemoteAccess = useMemo( () => remoteAccessEntries.find((entry) => entry.clientId === editingRemoteAccessClientId) ?? null, [editingRemoteAccessClientId, remoteAccessEntries] @@ -3070,6 +3112,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { setRemoteAccessCustomProvider(providerName ?? "") } setRemoteAccessIdentifierInput(editingRemoteAccess?.identifier ?? "") + setRemoteAccessUsernameInput(editingRemoteAccess?.username ?? "") + setRemoteAccessPasswordInput(editingRemoteAccess?.password ?? "") setRemoteAccessUrlInput(editingRemoteAccess?.url ?? "") setRemoteAccessNotesInput(editingRemoteAccess?.notes ?? "") }, [remoteAccessDialog, editingRemoteAccess]) @@ -3080,6 +3124,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { setRemoteAccessProviderOption(REMOTE_ACCESS_PROVIDERS[0].value) setRemoteAccessCustomProvider("") setRemoteAccessIdentifierInput("") + setRemoteAccessUsernameInput("") + setRemoteAccessPasswordInput("") setRemoteAccessUrlInput("") setRemoteAccessNotesInput("") } @@ -3087,6 +3133,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { useEffect(() => { setShowAllWindowsSoftware(false) + setVisibleRemoteSecrets({}) }, [device?.id]) const displayedWindowsSoftware = useMemo( @@ -3149,6 +3196,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { return } + const username = remoteAccessUsernameInput.trim() + const password = remoteAccessPasswordInput.trim() let normalizedUrl: string | undefined const rawUrl = remoteAccessUrlInput.trim() if (rawUrl.length > 0) { @@ -3191,6 +3240,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { deviceId: device.id, provider: providerName, identifier, + username, + password, url: normalizedUrl, notes: notes.length ? notes : undefined, action: "upsert", @@ -3220,6 +3271,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { remoteAccessProviderOption, remoteAccessCustomProvider, remoteAccessIdentifierInput, + remoteAccessUsernameInput, + remoteAccessPasswordInput, remoteAccessUrlInput, remoteAccessNotesInput, editingRemoteAccess, @@ -3337,6 +3390,41 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { } }, []) + const handleCopyRemoteCredential = useCallback(async (value: string | null | undefined, label: string) => { + if (!value) return + try { + await navigator.clipboard.writeText(value) + toast.success(`${label} copiado.`) + } catch (error) { + console.error(error) + toast.error(`Não foi possível copiar ${label.toLowerCase()}.`) + } + }, []) + + const toggleRemoteSecret = useCallback((clientId: string) => { + setVisibleRemoteSecrets((prev) => ({ ...prev, [clientId]: !prev[clientId] })) + }, []) + + const handleRustDeskConnect = useCallback((entry: DeviceRemoteAccessEntry) => { + if (!entry) return + const link = buildRustDeskUri(entry) + if (!link) { + toast.error("Não foi possível montar o link do RustDesk (ID ou senha ausentes).") + return + } + if (typeof window === "undefined") { + toast.error("A conexão direta só funciona no navegador.") + return + } + try { + window.location.href = link + toast.success("Abrindo o RustDesk...") + } catch (error) { + console.error(error) + toast.error("Não foi possível acionar o RustDesk neste dispositivo.") + } + }, []) + // Exportação individual (colunas personalizadas) const [isSingleExportOpen, setIsSingleExportOpen] = useState(false) const [singleExporting, setSingleExporting] = useState(false) @@ -3804,6 +3892,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt) ? new Date(entry.lastVerifiedAt) : null + const isRustDesk = isRustDeskAccess(entry) + const secretVisible = Boolean(visibleRemoteSecrets[entry.clientId]) return (
@@ -3828,6 +3918,51 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { ) : null}
+ {entry.username || entry.password ? ( +
+ {entry.username ? ( +
+ Usuário + + {entry.username} + + +
+ ) : null} + {entry.password ? ( +
+ Senha + + {secretVisible ? entry.password : "••••••••"} + + + +
+ ) : null} +
+ ) : null} {entry.url ? ( ) : null} + {isRustDesk && (entry.identifier || entry.password) ? ( + + ) : null} {entry.notes ? (

{entry.notes}

) : null} @@ -4120,6 +4265,26 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { required />
+
+ + setRemoteAccessUsernameInput(event.target.value)} + placeholder="Ex: suporte@cliente.com" + /> +
+
+ + setRemoteAccessPasswordInput(event.target.value)} + placeholder="Senha permanente do RustDesk ou PIN" + /> +

+ Esse valor ficará disponível para os administradores do painel. Limpe o campo para remover a senha salva. +

+
| null | undefined) type RemoteAccessNormalized = { provider: string | null identifier: string | null + username: string | null + password: string | null url: string | null notes: string | null lastVerifiedAt: number | null @@ -1249,6 +1264,8 @@ function normalizeRemoteAccessEntry( return { provider: providerHint ?? null, identifier: isUrl ? null : trimmed, + username: null, + password: null, url: isUrl ? trimmed : null, notes: null, lastVerifiedAt: null, @@ -1273,6 +1290,19 @@ function normalizeRemoteAccessEntry( ensureString(record["value"]) ?? ensureString(record["label"]) ?? null + const username = + ensureString(record["username"]) ?? + ensureString(record["user"]) ?? + ensureString(record["login"]) ?? + ensureString(record["email"]) ?? + ensureString(record["account"]) ?? + null + const password = + ensureString(record["password"]) ?? + ensureString(record["pass"]) ?? + ensureString(record["secret"]) ?? + ensureString(record["pin"]) ?? + null const url = ensureString(record["url"]) ?? ensureString(record["link"]) ?? @@ -1296,6 +1326,8 @@ function normalizeRemoteAccessEntry( return { provider, identifier, + username, + password, url, notes, lastVerifiedAt, diff --git a/tests/devices/remote-access.test.ts b/tests/devices/remote-access.test.ts index 699edc2..bc694d9 100644 --- a/tests/devices/remote-access.test.ts +++ b/tests/devices/remote-access.test.ts @@ -13,6 +13,8 @@ describe("normalizeDeviceRemoteAccess", () => { expect(result).toEqual({ provider: null, identifier: "PC-001", + username: null, + password: null, url: null, notes: null, lastVerifiedAt: null, @@ -25,6 +27,8 @@ describe("normalizeDeviceRemoteAccess", () => { expect(result).toEqual({ provider: null, identifier: null, + username: null, + password: null, url: "https://remote.example.com/session/123", notes: null, lastVerifiedAt: null, @@ -39,12 +43,16 @@ describe("normalizeDeviceRemoteAccess", () => { code: "123-456-789", remoteUrl: "https://anydesk.com/session/123", note: "Suporte avançado", + user: "admin", + secret: "S3nh@", verifiedAt: timestamp, extraTag: "vip", }) expect(result).toEqual({ provider: "AnyDesk", identifier: "123-456-789", + username: "admin", + password: "S3nh@", url: "https://anydesk.com/session/123", notes: "Suporte avançado", lastVerifiedAt: timestamp, @@ -53,6 +61,8 @@ describe("normalizeDeviceRemoteAccess", () => { code: "123-456-789", remoteUrl: "https://anydesk.com/session/123", note: "Suporte avançado", + user: "admin", + secret: "S3nh@", verifiedAt: timestamp, extraTag: "vip", },