Fix admin rename payload and harden RustDesk ID sync

This commit is contained in:
Esdras Renan 2025-11-26 21:00:08 -03:00
parent bd1bd4bef1
commit f7ad7f6a17
4 changed files with 146 additions and 6 deletions

View file

@ -207,26 +207,64 @@ pub fn ensure_rustdesk(
} }
}; };
let mut final_id = reported_id.clone();
if let Some(expected) = custom_id.as_ref() { if let Some(expected) = custom_id.as_ref() {
if expected != &reported_id { if expected != &reported_id {
log_event(&format!( 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 version = query_version(&exe_path).ok().or(installed_version);
let result = RustdeskProvisioningResult { let result = RustdeskProvisioningResult {
id: reported_id.clone(), id: final_id.clone(),
password: password.clone(), password: password.clone(),
installed_version: version.clone(), installed_version: version.clone(),
updated: freshly_installed, updated: freshly_installed,
last_provisioned_at: Utc::now().timestamp_millis(), 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) Ok(result)
} }

View file

@ -2166,6 +2166,37 @@ function normalizeRemoteAccessList(raw: unknown): RemoteAccessEntry[] {
return entries 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( async function upsertRemoteAccessSnapshotFromHeartbeat(
ctx: MutationCtx, ctx: MutationCtx,
machine: Doc<"machines">, machine: Doc<"machines">,
@ -2189,6 +2220,8 @@ async function upsertRemoteAccessSnapshotFromHeartbeat(
snapshotSource: "heartbeat", snapshotSource: "heartbeat",
provider, provider,
identifier, identifier,
machineId: machine._id,
hostname: machine.hostname,
lastVerifiedAt: timestamp, lastVerifiedAt: timestamp,
} }
@ -2423,6 +2456,7 @@ export const upsertRemoteAccessViaToken = mutation({
notes: cleanedNotes, notes: cleanedNotes,
lastVerifiedAt: timestamp, lastVerifiedAt: timestamp,
metadata: { metadata: {
source: "machine-token",
provider: trimmedProvider, provider: trimmedProvider,
identifier: trimmedIdentifier, identifier: trimmedIdentifier,
url: normalizedUrl, url: normalizedUrl,
@ -2430,6 +2464,9 @@ export const upsertRemoteAccessViaToken = mutation({
password: cleanedPassword, password: cleanedPassword,
notes: cleanedNotes, notes: cleanedNotes,
lastVerifiedAt: timestamp, 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.map((entry, index) => (index === existingIndex ? updatedEntry : entry))
: [...existingEntries, updatedEntry] : [...existingEntries, updatedEntry]
await removeDuplicateRemoteAccessEntries(ctx, machine.tenantId, machine._id, trimmedProvider, trimmedIdentifier, timestamp)
await ctx.db.patch(machine._id, { await ctx.db.patch(machine._id, {
remoteAccess: nextEntries, remoteAccess: nextEntries,
updatedAt: timestamp, updatedAt: timestamp,

View file

@ -49,10 +49,10 @@ export async function POST(request: Request) {
// Chamada por string reference (evita depender do tipo gerado imediatamente) // Chamada por string reference (evita depender do tipo gerado imediatamente)
const client = convex as unknown as { mutation: (name: string, args: unknown) => Promise<unknown> } const client = convex as unknown as { mutation: (name: string, args: unknown) => Promise<unknown> }
// Nota: a mutation `machines:rename` não aceita tenantId; apenas machineId, actorId e hostname.
await client.mutation("machines:rename", { await client.mutation("machines:rename", {
machineId: parsed.data.machineId, machineId: parsed.data.machineId,
actorId, actorId,
tenantId,
hostname: parsed.data.hostname, hostname: parsed.data.hostname,
}) })
@ -62,4 +62,3 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Falha ao renomear dispositivo" }, { status: 500 }) return NextResponse.json({ error: "Falha ao renomear dispositivo" }, { status: 500 })
} }
} }

View file

@ -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",
})
})
})