diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs index da95ef2..00ea831 100644 --- a/apps/desktop/src-tauri/src/rustdesk.rs +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -207,26 +207,64 @@ pub fn ensure_rustdesk( } }; + let mut final_id = reported_id.clone(); + if let Some(expected) = custom_id.as_ref() { if expected != &reported_id { log_event(&format!( - "ID retornado difere do determinístico ({expected}) -> aplicando {reported_id}" + "ID retornado difere do determinístico ({expected}) -> reaplicando ID determinístico" )); + + let reapplied = match set_custom_id(&exe_path, expected) { + Ok(_) => { + match query_id_with_retries(&exe_path, 3) { + Ok(rechecked) => { + if &rechecked == expected { + log_event(&format!("ID determinístico aplicado com sucesso: {rechecked}")); + final_id = rechecked; + true + } else { + log_event(&format!( + "ID ainda difere após reaplicação (esperado {expected}, reportado {rechecked}); forçando persistência do determinístico nos perfis" + )); + false + } + } + Err(error) => { + log_event(&format!( + "Falha ao consultar ID após reaplicação: {error}; forçando persistência do determinístico nos perfis" + )); + false + } + } + } + Err(error) => { + log_event(&format!( + "Falha ao reaplicar ID determinístico ({expected}): {error}; persistindo determinístico nos perfis mesmo assim" + )); + false + } + }; + + if !reapplied { + final_id = expected.clone(); + } } } - ensure_remote_id_files(&reported_id); + + ensure_remote_id_files(&final_id); let version = query_version(&exe_path).ok().or(installed_version); let result = RustdeskProvisioningResult { - id: reported_id.clone(), + id: final_id.clone(), password: password.clone(), installed_version: version.clone(), updated: freshly_installed, last_provisioned_at: Utc::now().timestamp_millis(), }; - log_event(&format!("Provisionamento concluído. ID final: {reported_id}. Versão: {:?}", version)); + log_event(&format!("Provisionamento concluído. ID final: {final_id}. Versão: {:?}", version)); Ok(result) } diff --git a/convex/machines.ts b/convex/machines.ts index cb68a11..df81bd1 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -2166,6 +2166,37 @@ function normalizeRemoteAccessList(raw: unknown): RemoteAccessEntry[] { return entries } +async function removeDuplicateRemoteAccessEntries( + ctx: MutationCtx, + tenantId: string, + currentMachineId: Id<"machines">, + provider: string, + identifier: string, + now: number +) { + const machines = await ctx.db + .query("machines") + .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) + .collect() + + const providerLc = provider.toLowerCase() + const identifierLc = identifier.toLowerCase() + + for (const device of machines) { + if (device._id === currentMachineId) continue + const entries = normalizeRemoteAccessList(device.remoteAccess) + const filtered = entries.filter( + (entry) => + entry.provider.toLowerCase() !== providerLc || entry.identifier.toLowerCase() !== identifierLc + ) + if (filtered.length === entries.length) continue + await ctx.db.patch(device._id, { + remoteAccess: filtered.length > 0 ? filtered : null, + updatedAt: now, + }) + } +} + async function upsertRemoteAccessSnapshotFromHeartbeat( ctx: MutationCtx, machine: Doc<"machines">, @@ -2189,6 +2220,8 @@ async function upsertRemoteAccessSnapshotFromHeartbeat( snapshotSource: "heartbeat", provider, identifier, + machineId: machine._id, + hostname: machine.hostname, lastVerifiedAt: timestamp, } @@ -2423,6 +2456,7 @@ export const upsertRemoteAccessViaToken = mutation({ notes: cleanedNotes, lastVerifiedAt: timestamp, metadata: { + source: "machine-token", provider: trimmedProvider, identifier: trimmedIdentifier, url: normalizedUrl, @@ -2430,6 +2464,9 @@ export const upsertRemoteAccessViaToken = mutation({ password: cleanedPassword, notes: cleanedNotes, lastVerifiedAt: timestamp, + machineId: machine._id, + hostname: machine.hostname, + tenantId: machine.tenantId, }, } @@ -2438,6 +2475,8 @@ export const upsertRemoteAccessViaToken = mutation({ ? existingEntries.map((entry, index) => (index === existingIndex ? updatedEntry : entry)) : [...existingEntries, updatedEntry] + await removeDuplicateRemoteAccessEntries(ctx, machine.tenantId, machine._id, trimmedProvider, trimmedIdentifier, timestamp) + await ctx.db.patch(machine._id, { remoteAccess: nextEntries, updatedAt: timestamp, diff --git a/src/app/api/admin/devices/rename/route.ts b/src/app/api/admin/devices/rename/route.ts index 9fd7250..349d8a8 100644 --- a/src/app/api/admin/devices/rename/route.ts +++ b/src/app/api/admin/devices/rename/route.ts @@ -49,10 +49,10 @@ export async function POST(request: Request) { // Chamada por string reference (evita depender do tipo gerado imediatamente) const client = convex as unknown as { mutation: (name: string, args: unknown) => Promise } + // Nota: a mutation `machines:rename` não aceita tenantId; apenas machineId, actorId e hostname. await client.mutation("machines:rename", { machineId: parsed.data.machineId, actorId, - tenantId, hostname: parsed.data.hostname, }) @@ -62,4 +62,3 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Falha ao renomear dispositivo" }, { status: 500 }) } } - diff --git a/tests/api-admin-devices-rename.test.ts b/tests/api-admin-devices-rename.test.ts new file mode 100644 index 0000000..db66202 --- /dev/null +++ b/tests/api-admin-devices-rename.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, beforeEach, vi } from "vitest" + +import { POST } from "@/app/api/admin/devices/rename/route" +import { assertAuthenticatedSession } from "@/lib/auth-server" + +vi.mock("@/lib/auth-server", () => ({ + assertAuthenticatedSession: vi.fn(), +})) + +const mutationMock = vi.fn() + +vi.mock("convex/browser", () => ({ + ConvexHttpClient: vi.fn(() => ({ mutation: mutationMock })), +})) + +vi.mock("@/convex/_generated/api", () => ({ + api: { users: { ensureUser: "users:ensureUser" } }, +})) + +describe("POST /api/admin/devices/rename", () => { + beforeEach(() => { + vi.resetAllMocks() + process.env.NEXT_PUBLIC_CONVEX_URL = "https://convex.example.test" + vi.mocked(assertAuthenticatedSession).mockResolvedValue({ + user: { + id: "session-user", + email: "agent@example.com", + name: "Agent", + role: "AGENT", + tenantId: "tenant-atlas", + avatarUrl: null, + }, + session: { id: "sess", expiresAt: Date.now() + 1000 }, + }) + mutationMock.mockImplementation((name: string) => { + if (name === "users:ensureUser") { + return Promise.resolve({ _id: "user-123" }) + } + return Promise.resolve({ ok: true }) + }) + }) + + it("envia somente machineId, actorId e hostname para machines:rename", async () => { + const body = { + machineId: "machine-123", + hostname: "novo-host", + } + const req = new Request("http://localhost/api/admin/devices/rename", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }) + + const res = await POST(req) + expect(res.status).toBe(200) + const renameCall = mutationMock.mock.calls.find(([name]) => name === "machines:rename") + expect(renameCall).toBeDefined() + expect(renameCall?.[1]).toEqual({ + machineId: "machine-123", + actorId: "user-123", + hostname: "novo-host", + }) + }) +})