feat: integrar credenciais rustdesk aos acessos remotos

This commit is contained in:
Esdras Renan 2025-11-07 15:39:36 -03:00
parent 4079f67fcb
commit 07d631de40
5 changed files with 243 additions and 5 deletions

View file

@ -1991,6 +1991,8 @@ type RemoteAccessEntry = {
provider: string provider: string
identifier: string identifier: string
url: string | null url: string | null
username: string | null
password: string | null
notes: string | null notes: string | null
lastVerifiedAt: number | null lastVerifiedAt: number | null
metadata: Record<string, unknown> | null metadata: Record<string, unknown> | null
@ -2032,6 +2034,8 @@ function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null {
provider: "Remoto", provider: "Remoto",
identifier: isUrl ? trimmed : trimmed, identifier: isUrl ? trimmed : trimmed,
url: isUrl ? trimmed : null, url: isUrl ? trimmed : null,
username: null,
password: null,
notes: null, notes: null,
lastVerifiedAt: null, lastVerifiedAt: null,
metadata: null, metadata: null,
@ -2059,6 +2063,19 @@ function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null {
null null
const resolvedIdentifier = identifier ?? url ?? "Acesso remoto" const resolvedIdentifier = identifier ?? url ?? "Acesso remoto"
const notes = coerceString(record.notes) ?? coerceString(record.note) ?? coerceString(record.description) ?? coerceString(record.obs) ?? null const notes = coerceString(record.notes) ?? coerceString(record.note) ?? coerceString(record.description) ?? coerceString(record.obs) ?? null
const username =
coerceString((record as Record<string, unknown>).username) ??
coerceString((record as Record<string, unknown>).user) ??
coerceString((record as Record<string, unknown>).login) ??
coerceString((record as Record<string, unknown>).email) ??
coerceString((record as Record<string, unknown>).account) ??
null
const password =
coerceString((record as Record<string, unknown>).password) ??
coerceString((record as Record<string, unknown>).pass) ??
coerceString((record as Record<string, unknown>).secret) ??
coerceString((record as Record<string, unknown>).pin) ??
null
const timestamp = const timestamp =
coerceNumber(record.lastVerifiedAt) ?? coerceNumber(record.lastVerifiedAt) ??
coerceNumber(record.verifiedAt) ?? coerceNumber(record.verifiedAt) ??
@ -2075,6 +2092,8 @@ function normalizeRemoteAccessEntry(raw: unknown): RemoteAccessEntry | null {
provider, provider,
identifier: resolvedIdentifier, identifier: resolvedIdentifier,
url, url,
username,
password,
notes, notes,
lastVerifiedAt: timestamp, lastVerifiedAt: timestamp,
metadata, metadata,
@ -2105,12 +2124,14 @@ export const updateRemoteAccess = mutation({
provider: v.optional(v.string()), provider: v.optional(v.string()),
identifier: v.optional(v.string()), identifier: v.optional(v.string()),
url: v.optional(v.string()), url: v.optional(v.string()),
username: v.optional(v.string()),
password: v.optional(v.string()),
notes: v.optional(v.string()), notes: v.optional(v.string()),
action: v.optional(v.string()), action: v.optional(v.string()),
entryId: v.optional(v.string()), entryId: v.optional(v.string()),
clear: v.optional(v.boolean()), 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) const machine = await ctx.db.get(machineId)
if (!machine) { if (!machine) {
throw new ConvexError("Dispositivo não encontrada") throw new ConvexError("Dispositivo não encontrada")
@ -2192,6 +2213,8 @@ export const updateRemoteAccess = mutation({
} }
const cleanedNotes = notes?.trim() ? notes.trim() : null 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 lastVerifiedAt = Date.now()
const targetEntryId = const targetEntryId =
coerceString(entryId) ?? coerceString(entryId) ??
@ -2205,12 +2228,16 @@ export const updateRemoteAccess = mutation({
provider: trimmedProvider, provider: trimmedProvider,
identifier: trimmedIdentifier, identifier: trimmedIdentifier,
url: normalizedUrl, url: normalizedUrl,
username: cleanedUsername,
password: cleanedPassword,
notes: cleanedNotes, notes: cleanedNotes,
lastVerifiedAt, lastVerifiedAt,
metadata: { metadata: {
provider: trimmedProvider, provider: trimmedProvider,
identifier: trimmedIdentifier, identifier: trimmedIdentifier,
url: normalizedUrl, url: normalizedUrl,
username: cleanedUsername,
password: cleanedPassword,
notes: cleanedNotes, notes: cleanedNotes,
lastVerifiedAt, lastVerifiedAt,
}, },

View file

@ -13,6 +13,8 @@ const schema = z.object({
provider: z.string().optional(), provider: z.string().optional(),
identifier: z.string().optional(), identifier: z.string().optional(),
url: z.string().optional(), url: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
notes: z.string().optional(), notes: z.string().optional(),
entryId: z.string().optional(), entryId: z.string().optional(),
action: z.enum(["save", "upsert", "clear", "delete", "remove"]).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 (provider) mutationArgs.provider = provider
if (identifier) mutationArgs.identifier = identifier if (identifier) mutationArgs.identifier = identifier
if (normalizedUrl !== undefined) mutationArgs.url = normalizedUrl if (normalizedUrl !== undefined) mutationArgs.url = normalizedUrl
mutationArgs.username = (parsed.username ?? "").trim()
mutationArgs.password = (parsed.password ?? "").trim()
if (notes.length) mutationArgs.notes = notes if (notes.length) mutationArgs.notes = notes
} }

View file

@ -26,6 +26,9 @@ import {
RotateCcw, RotateCcw,
AlertTriangle, AlertTriangle,
Key, Key,
Eye,
EyeOff,
MonitorSmartphone,
Globe, Globe,
Apple, Apple,
Terminal, Terminal,
@ -323,6 +326,8 @@ type DeviceRemoteAccessEntry = {
clientId: string clientId: string
provider: string | null provider: string | null
identifier: string | null identifier: string | null
username: string | null
password: string | null
url: string | null url: string | null
notes: string | null notes: string | null
lastVerifiedAt: number | null lastVerifiedAt: number | null
@ -332,6 +337,8 @@ type DeviceRemoteAccessEntry = {
export type DeviceRemoteAccess = { export type DeviceRemoteAccess = {
provider: string | null provider: string | null
identifier: string | null identifier: string | null
username: string | null
password: string | null
url: string | null url: string | null
notes: string | null notes: string | null
lastVerifiedAt: number | null lastVerifiedAt: number | null
@ -411,6 +418,8 @@ function normalizeDeviceRemoteAccessEntry(raw: unknown): DeviceRemoteAccessEntry
const identifier = const identifier =
readString(record, "identifier", "code", "id", "accessId") ?? readString(record, "identifier", "code", "id", "accessId") ??
readString(record, "value", "label") 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 url = readString(record, "url", "link", "remoteUrl", "console", "viewer") ?? null
const notes = readString(record, "notes", "note", "description", "obs") ?? null const notes = readString(record, "notes", "note", "description", "obs") ?? null
const timestampCandidate = const timestampCandidate =
@ -423,6 +432,8 @@ function normalizeDeviceRemoteAccessEntry(raw: unknown): DeviceRemoteAccessEntry
clientId: id ?? createRemoteAccessClientId(), clientId: id ?? createRemoteAccessClientId(),
provider, provider,
identifier: identifier ?? url ?? null, identifier: identifier ?? url ?? null,
username,
password,
url, url,
notes, notes,
lastVerifiedAt, lastVerifiedAt,
@ -433,8 +444,8 @@ function normalizeDeviceRemoteAccessEntry(raw: unknown): DeviceRemoteAccessEntry
export function normalizeDeviceRemoteAccess(raw: unknown): DeviceRemoteAccess | null { export function normalizeDeviceRemoteAccess(raw: unknown): DeviceRemoteAccess | null {
const entry = normalizeDeviceRemoteAccessEntry(raw) const entry = normalizeDeviceRemoteAccessEntry(raw)
if (!entry) return null if (!entry) return null
const { provider, identifier, url, notes, lastVerifiedAt, metadata } = entry const { provider, identifier, username, password, url, notes, lastVerifiedAt, metadata } = entry
return { provider, identifier, url, notes, lastVerifiedAt, metadata } return { provider, identifier, username, password, url, notes, lastVerifiedAt, metadata }
} }
export function normalizeDeviceRemoteAccessList(raw: unknown): DeviceRemoteAccessEntry[] { export function normalizeDeviceRemoteAccessList(raw: unknown): DeviceRemoteAccessEntry[] {
@ -464,6 +475,15 @@ const REMOTE_ACCESS_METADATA_IGNORED_KEYS = new Set([
"code", "code",
"id", "id",
"accessId", "accessId",
"username",
"user",
"login",
"email",
"account",
"password",
"pass",
"secret",
"pin",
"url", "url",
"link", "link",
"remoteUrl", "remoteUrl",
@ -515,6 +535,25 @@ function readText(record: Record<string, unknown>, ...keys: string[]): string |
return undefined 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 { function parseWindowsInstallDate(value: unknown): Date | null {
if (!value) return null if (!value) return null
if (value instanceof Date) return value if (value instanceof Date) return value
@ -2999,9 +3038,12 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) )
const [remoteAccessCustomProvider, setRemoteAccessCustomProvider] = useState("") const [remoteAccessCustomProvider, setRemoteAccessCustomProvider] = useState("")
const [remoteAccessIdentifierInput, setRemoteAccessIdentifierInput] = useState("") const [remoteAccessIdentifierInput, setRemoteAccessIdentifierInput] = useState("")
const [remoteAccessUsernameInput, setRemoteAccessUsernameInput] = useState("")
const [remoteAccessPasswordInput, setRemoteAccessPasswordInput] = useState("")
const [remoteAccessUrlInput, setRemoteAccessUrlInput] = useState("") const [remoteAccessUrlInput, setRemoteAccessUrlInput] = useState("")
const [remoteAccessNotesInput, setRemoteAccessNotesInput] = useState("") const [remoteAccessNotesInput, setRemoteAccessNotesInput] = useState("")
const [remoteAccessSaving, setRemoteAccessSaving] = useState(false) const [remoteAccessSaving, setRemoteAccessSaving] = useState(false)
const [visibleRemoteSecrets, setVisibleRemoteSecrets] = useState<Record<string, boolean>>({})
const editingRemoteAccess = useMemo( const editingRemoteAccess = useMemo(
() => remoteAccessEntries.find((entry) => entry.clientId === editingRemoteAccessClientId) ?? null, () => remoteAccessEntries.find((entry) => entry.clientId === editingRemoteAccessClientId) ?? null,
[editingRemoteAccessClientId, remoteAccessEntries] [editingRemoteAccessClientId, remoteAccessEntries]
@ -3070,6 +3112,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
setRemoteAccessCustomProvider(providerName ?? "") setRemoteAccessCustomProvider(providerName ?? "")
} }
setRemoteAccessIdentifierInput(editingRemoteAccess?.identifier ?? "") setRemoteAccessIdentifierInput(editingRemoteAccess?.identifier ?? "")
setRemoteAccessUsernameInput(editingRemoteAccess?.username ?? "")
setRemoteAccessPasswordInput(editingRemoteAccess?.password ?? "")
setRemoteAccessUrlInput(editingRemoteAccess?.url ?? "") setRemoteAccessUrlInput(editingRemoteAccess?.url ?? "")
setRemoteAccessNotesInput(editingRemoteAccess?.notes ?? "") setRemoteAccessNotesInput(editingRemoteAccess?.notes ?? "")
}, [remoteAccessDialog, editingRemoteAccess]) }, [remoteAccessDialog, editingRemoteAccess])
@ -3080,6 +3124,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
setRemoteAccessProviderOption(REMOTE_ACCESS_PROVIDERS[0].value) setRemoteAccessProviderOption(REMOTE_ACCESS_PROVIDERS[0].value)
setRemoteAccessCustomProvider("") setRemoteAccessCustomProvider("")
setRemoteAccessIdentifierInput("") setRemoteAccessIdentifierInput("")
setRemoteAccessUsernameInput("")
setRemoteAccessPasswordInput("")
setRemoteAccessUrlInput("") setRemoteAccessUrlInput("")
setRemoteAccessNotesInput("") setRemoteAccessNotesInput("")
} }
@ -3087,6 +3133,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
useEffect(() => { useEffect(() => {
setShowAllWindowsSoftware(false) setShowAllWindowsSoftware(false)
setVisibleRemoteSecrets({})
}, [device?.id]) }, [device?.id])
const displayedWindowsSoftware = useMemo( const displayedWindowsSoftware = useMemo(
@ -3149,6 +3196,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
return return
} }
const username = remoteAccessUsernameInput.trim()
const password = remoteAccessPasswordInput.trim()
let normalizedUrl: string | undefined let normalizedUrl: string | undefined
const rawUrl = remoteAccessUrlInput.trim() const rawUrl = remoteAccessUrlInput.trim()
if (rawUrl.length > 0) { if (rawUrl.length > 0) {
@ -3191,6 +3240,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
deviceId: device.id, deviceId: device.id,
provider: providerName, provider: providerName,
identifier, identifier,
username,
password,
url: normalizedUrl, url: normalizedUrl,
notes: notes.length ? notes : undefined, notes: notes.length ? notes : undefined,
action: "upsert", action: "upsert",
@ -3220,6 +3271,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
remoteAccessProviderOption, remoteAccessProviderOption,
remoteAccessCustomProvider, remoteAccessCustomProvider,
remoteAccessIdentifierInput, remoteAccessIdentifierInput,
remoteAccessUsernameInput,
remoteAccessPasswordInput,
remoteAccessUrlInput, remoteAccessUrlInput,
remoteAccessNotesInput, remoteAccessNotesInput,
editingRemoteAccess, 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) // Exportação individual (colunas personalizadas)
const [isSingleExportOpen, setIsSingleExportOpen] = useState(false) const [isSingleExportOpen, setIsSingleExportOpen] = useState(false)
const [singleExporting, setSingleExporting] = useState(false) const [singleExporting, setSingleExporting] = useState(false)
@ -3804,6 +3892,8 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt) entry.lastVerifiedAt && Number.isFinite(entry.lastVerifiedAt)
? new Date(entry.lastVerifiedAt) ? new Date(entry.lastVerifiedAt)
: null : null
const isRustDesk = isRustDeskAccess(entry)
const secretVisible = Boolean(visibleRemoteSecrets[entry.clientId])
return ( return (
<div key={entry.clientId} className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-xs sm:text-sm text-slate-700"> <div key={entry.clientId} className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-xs sm:text-sm text-slate-700">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
@ -3828,6 +3918,51 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</Button> </Button>
) : null} ) : null}
</div> </div>
{entry.username || entry.password ? (
<div className="flex flex-col gap-1">
{entry.username ? (
<div className="inline-flex flex-wrap items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Usuário</span>
<code className="rounded-md border border-slate-200 bg-white px-2 py-0.5 font-mono text-xs text-slate-700">
{entry.username}
</code>
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
onClick={() => handleCopyRemoteCredential(entry.username, "Usuário do acesso remoto")}
>
<ClipboardCopy className="size-3.5" /> Copiar
</Button>
</div>
) : null}
{entry.password ? (
<div className="inline-flex flex-wrap items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Senha</span>
<code className="rounded-md border border-slate-200 bg-white px-2 py-0.5 font-mono text-xs text-slate-700">
{secretVisible ? entry.password : "••••••••"}
</code>
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
onClick={() => toggleRemoteSecret(entry.clientId)}
>
{secretVisible ? <EyeOff className="size-3.5" /> : <Eye className="size-3.5" />}
{secretVisible ? "Ocultar" : "Mostrar"}
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 border border-transparent px-2 text-slate-600 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900"
onClick={() => handleCopyRemoteCredential(entry.password, "Senha do acesso remoto")}
>
<ClipboardCopy className="size-3.5" /> Copiar
</Button>
</div>
) : null}
</div>
) : null}
{entry.url ? ( {entry.url ? (
<a <a
href={entry.url} href={entry.url}
@ -3838,6 +3973,16 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
Abrir console remoto Abrir console remoto
</a> </a>
) : null} ) : null}
{isRustDesk && (entry.identifier || entry.password) ? (
<Button
variant="secondary"
size="sm"
className="mt-1 inline-flex items-center gap-2 bg-white/80 text-slate-800 hover:bg-white"
onClick={() => handleRustDeskConnect(entry)}
>
<MonitorSmartphone className="size-4" /> Conectar via RustDesk
</Button>
) : null}
{entry.notes ? ( {entry.notes ? (
<p className="whitespace-pre-wrap text-[11px] text-slate-600">{entry.notes}</p> <p className="whitespace-pre-wrap text-[11px] text-slate-600">{entry.notes}</p>
) : null} ) : null}
@ -4120,6 +4265,26 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
required required
/> />
</div> </div>
<div className="grid gap-2">
<label className="text-sm font-medium">Usuário (opcional)</label>
<Input
value={remoteAccessUsernameInput}
onChange={(event) => setRemoteAccessUsernameInput(event.target.value)}
placeholder="Ex: suporte@cliente.com"
/>
</div>
<div className="grid gap-2">
<label className="text-sm font-medium">Senha / PIN (opcional)</label>
<Input
type="password"
value={remoteAccessPasswordInput}
onChange={(event) => setRemoteAccessPasswordInput(event.target.value)}
placeholder="Senha permanente do RustDesk ou PIN"
/>
<p className="text-xs text-muted-foreground">
Esse valor ficará disponível para os administradores do painel. Limpe o campo para remover a senha salva.
</p>
</div>
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm font-medium">Link (opcional)</label> <label className="text-sm font-medium">Link (opcional)</label>
<Input <Input

View file

@ -359,8 +359,19 @@ const PHYSICAL_DISK_COLUMN_WIDTHS = [22, 32, 18, 16, 16, 22, 16] as const
const NETWORK_HEADERS = ["Hostname", "Interface", "MAC", "IP", "Origem"] as const const NETWORK_HEADERS = ["Hostname", "Interface", "MAC", "IP", "Origem"] as const
const NETWORK_COLUMN_WIDTHS = [22, 28, 22, 24, 18] as const const NETWORK_COLUMN_WIDTHS = [22, 28, 22, 24, 18] as const
const REMOTE_ACCESS_HEADERS = ["Hostname", "Provedor", "Identificador", "URL", "Notas", "Última verificação", "Origem", "Metadados"] as const const REMOTE_ACCESS_HEADERS = [
const REMOTE_ACCESS_COLUMN_WIDTHS = [22, 22, 24, 36, 28, 22, 16, 40] as const "Hostname",
"Provedor",
"Identificador",
"Usuário",
"Senha",
"URL",
"Notas",
"Última verificação",
"Origem",
"Metadados",
] as const
const REMOTE_ACCESS_COLUMN_WIDTHS = [22, 22, 24, 24, 20, 32, 28, 22, 16, 36] as const
const SERVICE_HEADERS = ["Hostname", "Nome", "Exibição", "Status", "Origem"] as const const SERVICE_HEADERS = ["Hostname", "Nome", "Exibição", "Status", "Origem"] as const
const SERVICE_COLUMN_WIDTHS = [22, 28, 36, 18, 18] as const const SERVICE_COLUMN_WIDTHS = [22, 28, 36, 18, 18] as const
@ -756,6 +767,8 @@ function buildRemoteAccessRows(machines: MachineInventoryRecord[]): WorksheetRow
machine.hostname, machine.hostname,
entry.provider ?? "—", entry.provider ?? "—",
entry.identifier ?? "—", entry.identifier ?? "—",
entry.username ?? "—",
entry.password ?? "—",
entry.url ?? "—", entry.url ?? "—",
entry.notes ?? "—", entry.notes ?? "—",
entry.lastVerifiedAt ? formatDateTime(entry.lastVerifiedAt) ?? "—" : "—", entry.lastVerifiedAt ? formatDateTime(entry.lastVerifiedAt) ?? "—" : "—",
@ -1229,6 +1242,8 @@ function stringifyMetadata(metadata: Record<string, unknown> | null | undefined)
type RemoteAccessNormalized = { type RemoteAccessNormalized = {
provider: string | null provider: string | null
identifier: string | null identifier: string | null
username: string | null
password: string | null
url: string | null url: string | null
notes: string | null notes: string | null
lastVerifiedAt: number | null lastVerifiedAt: number | null
@ -1249,6 +1264,8 @@ function normalizeRemoteAccessEntry(
return { return {
provider: providerHint ?? null, provider: providerHint ?? null,
identifier: isUrl ? null : trimmed, identifier: isUrl ? null : trimmed,
username: null,
password: null,
url: isUrl ? trimmed : null, url: isUrl ? trimmed : null,
notes: null, notes: null,
lastVerifiedAt: null, lastVerifiedAt: null,
@ -1273,6 +1290,19 @@ function normalizeRemoteAccessEntry(
ensureString(record["value"]) ?? ensureString(record["value"]) ??
ensureString(record["label"]) ?? ensureString(record["label"]) ??
null 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 = const url =
ensureString(record["url"]) ?? ensureString(record["url"]) ??
ensureString(record["link"]) ?? ensureString(record["link"]) ??
@ -1296,6 +1326,8 @@ function normalizeRemoteAccessEntry(
return { return {
provider, provider,
identifier, identifier,
username,
password,
url, url,
notes, notes,
lastVerifiedAt, lastVerifiedAt,

View file

@ -13,6 +13,8 @@ describe("normalizeDeviceRemoteAccess", () => {
expect(result).toEqual({ expect(result).toEqual({
provider: null, provider: null,
identifier: "PC-001", identifier: "PC-001",
username: null,
password: null,
url: null, url: null,
notes: null, notes: null,
lastVerifiedAt: null, lastVerifiedAt: null,
@ -25,6 +27,8 @@ describe("normalizeDeviceRemoteAccess", () => {
expect(result).toEqual({ expect(result).toEqual({
provider: null, provider: null,
identifier: null, identifier: null,
username: null,
password: null,
url: "https://remote.example.com/session/123", url: "https://remote.example.com/session/123",
notes: null, notes: null,
lastVerifiedAt: null, lastVerifiedAt: null,
@ -39,12 +43,16 @@ describe("normalizeDeviceRemoteAccess", () => {
code: "123-456-789", code: "123-456-789",
remoteUrl: "https://anydesk.com/session/123", remoteUrl: "https://anydesk.com/session/123",
note: "Suporte avançado", note: "Suporte avançado",
user: "admin",
secret: "S3nh@",
verifiedAt: timestamp, verifiedAt: timestamp,
extraTag: "vip", extraTag: "vip",
}) })
expect(result).toEqual({ expect(result).toEqual({
provider: "AnyDesk", provider: "AnyDesk",
identifier: "123-456-789", identifier: "123-456-789",
username: "admin",
password: "S3nh@",
url: "https://anydesk.com/session/123", url: "https://anydesk.com/session/123",
notes: "Suporte avançado", notes: "Suporte avançado",
lastVerifiedAt: timestamp, lastVerifiedAt: timestamp,
@ -53,6 +61,8 @@ describe("normalizeDeviceRemoteAccess", () => {
code: "123-456-789", code: "123-456-789",
remoteUrl: "https://anydesk.com/session/123", remoteUrl: "https://anydesk.com/session/123",
note: "Suporte avançado", note: "Suporte avançado",
user: "admin",
secret: "S3nh@",
verifiedAt: timestamp, verifiedAt: timestamp,
extraTag: "vip", extraTag: "vip",
}, },