feat: integrar credenciais rustdesk aos acessos remotos
This commit is contained in:
parent
4079f67fcb
commit
07d631de40
5 changed files with 243 additions and 5 deletions
|
|
@ -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<string, unknown>, ...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<Record<string, boolean>>({})
|
||||
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 (
|
||||
<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">
|
||||
|
|
@ -3828,6 +3918,51 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
</Button>
|
||||
) : null}
|
||||
</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 ? (
|
||||
<a
|
||||
href={entry.url}
|
||||
|
|
@ -3838,6 +3973,16 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
Abrir console remoto
|
||||
</a>
|
||||
) : 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 ? (
|
||||
<p className="whitespace-pre-wrap text-[11px] text-slate-600">{entry.notes}</p>
|
||||
) : null}
|
||||
|
|
@ -4120,6 +4265,26 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
|
|||
required
|
||||
/>
|
||||
</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">
|
||||
<label className="text-sm font-medium">Link (opcional)</label>
|
||||
<Input
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue