feat: event-driven rustdesk sync
This commit is contained in:
parent
e410a4874c
commit
e0bb6bb80f
4 changed files with 263 additions and 96 deletions
|
|
@ -2,8 +2,9 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { listen } from "@tauri-apps/api/event"
|
||||
import { Store } from "@tauri-apps/plugin-store"
|
||||
import { appLocalDataDir, executableDir, join } from "@tauri-apps/api/path"
|
||||
import { appLocalDataDir, join } from "@tauri-apps/api/path"
|
||||
import { ExternalLink, Eye, EyeOff, Loader2, RefreshCw } from "lucide-react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"
|
||||
import { cn } from "./lib/utils"
|
||||
|
|
@ -82,10 +83,10 @@ type RustdeskProvisioningResult = {
|
|||
password: string
|
||||
installedVersion?: string | null
|
||||
updated: boolean
|
||||
lastProvisionedAt: number
|
||||
}
|
||||
|
||||
type RustdeskInfo = RustdeskProvisioningResult & {
|
||||
lastProvisionedAt: number
|
||||
lastSyncedAt?: number | null
|
||||
lastError?: string | null
|
||||
}
|
||||
|
|
@ -96,6 +97,8 @@ declare global {
|
|||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_URL?: string
|
||||
readonly VITE_API_BASE_URL?: string
|
||||
readonly VITE_RUSTDESK_CONFIG_STRING?: string
|
||||
readonly VITE_RUSTDESK_DEFAULT_PASSWORD?: string
|
||||
}
|
||||
interface ImportMeta { readonly env: ImportMetaEnv }
|
||||
}
|
||||
|
|
@ -111,6 +114,8 @@ function normalizeUrl(value?: string | null, fallback = DEFAULT_APP_URL) {
|
|||
|
||||
const appUrl = normalizeUrl(import.meta.env.VITE_APP_URL, DEFAULT_APP_URL)
|
||||
const apiBaseUrl = normalizeUrl(import.meta.env.VITE_API_BASE_URL, appUrl)
|
||||
const RUSTDESK_CONFIG_STRING = import.meta.env.VITE_RUSTDESK_CONFIG_STRING?.trim() || null
|
||||
const RUSTDESK_DEFAULT_PASSWORD = import.meta.env.VITE_RUSTDESK_DEFAULT_PASSWORD?.trim() || null
|
||||
|
||||
const RUSTDESK_SYNC_INTERVAL_MS = 60 * 60 * 1000 // 1h
|
||||
const TOKEN_SELF_HEAL_DEBOUNCE_MS = 30 * 1000
|
||||
|
|
@ -157,17 +162,9 @@ function buildRemoteAccessSnapshot(info: RustdeskInfo | null) {
|
|||
}
|
||||
|
||||
async function loadStore(): Promise<Store> {
|
||||
// Tenta usar uma pasta "data" ao lado do executável (ex.: C:\Raven\data)
|
||||
try {
|
||||
const exeDir = await executableDir()
|
||||
const storePath = await join(exeDir, "data", STORE_FILENAME)
|
||||
return await Store.load(storePath)
|
||||
} catch {
|
||||
// Fallback: AppData local do usuário
|
||||
const appData = await appLocalDataDir()
|
||||
const storePath = await join(appData, STORE_FILENAME)
|
||||
return await Store.load(storePath)
|
||||
}
|
||||
const appData = await appLocalDataDir()
|
||||
const storePath = await join(appData, STORE_FILENAME)
|
||||
return await Store.load(storePath)
|
||||
}
|
||||
|
||||
async function readToken(store: Store): Promise<string | null> {
|
||||
|
|
@ -629,6 +626,7 @@ useEffect(() => {
|
|||
rustdeskInfoRef.current = rustdeskInfo
|
||||
}, [rustdeskInfo])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!store || !config) return
|
||||
const email = collabEmail.trim()
|
||||
|
|
@ -736,69 +734,124 @@ const resolvedAppUrl = useMemo(() => {
|
|||
return normalized
|
||||
}, [config?.appUrl])
|
||||
|
||||
const syncRustdeskAccess = useCallback(
|
||||
async (machineToken: string, info: RustdeskInfo, allowRetry = true) => {
|
||||
if (!store || !machineToken) return
|
||||
const syncRemoteAccessNow = useCallback(
|
||||
async (info: RustdeskInfo, allowRetry = true) => {
|
||||
if (!store) return
|
||||
const payload = buildRemoteAccessPayload(info)
|
||||
if (!payload) return
|
||||
try {
|
||||
|
||||
const resolveToken = async (allowHeal: boolean): Promise<string | null> => {
|
||||
let currentToken = token
|
||||
if (!currentToken) {
|
||||
currentToken = (await readToken(store)) ?? null
|
||||
if (currentToken) {
|
||||
setToken(currentToken)
|
||||
}
|
||||
}
|
||||
if (!currentToken && allowHeal) {
|
||||
const healed = await attemptSelfHeal("remote-access")
|
||||
if (healed) {
|
||||
currentToken = (await readToken(store)) ?? null
|
||||
if (currentToken) {
|
||||
setToken(currentToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentToken
|
||||
}
|
||||
|
||||
const sendRequest = async (machineToken: string, retryAllowed: boolean): Promise<void> => {
|
||||
const response = await fetch(`${apiBaseUrl}/api/machines/remote-access`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": `${config?.machineId ?? "unknown"}:RustDesk:${info.id}`,
|
||||
},
|
||||
body: JSON.stringify({ machineToken, ...payload }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logDesktop("remoteAccess:sync:error", { status: response.status })
|
||||
const text = await response.text()
|
||||
if (allowRetry && isTokenRevokedMessage(text)) {
|
||||
if (retryAllowed && (response.status === 401 || isTokenRevokedMessage(text))) {
|
||||
const healed = await attemptSelfHeal("remote-access")
|
||||
if (healed) {
|
||||
const refreshedToken = (await readToken(store)) ?? machineToken
|
||||
return syncRustdeskAccess(refreshedToken, info, false)
|
||||
const refreshedToken = await resolveToken(false)
|
||||
if (refreshedToken) {
|
||||
return sendRequest(refreshedToken, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(text.slice(0, 300) || "Falha ao registrar acesso remoto")
|
||||
}
|
||||
|
||||
const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now(), lastError: null }
|
||||
logDesktop("remoteAccess:sync:success", { id: info.id })
|
||||
await writeRustdeskInfo(store, nextInfo)
|
||||
setRustdeskInfo(nextInfo)
|
||||
logDesktop("remoteAccess:sync:success", { id: info.id })
|
||||
}
|
||||
|
||||
try {
|
||||
const machineToken = await resolveToken(true)
|
||||
if (!machineToken) {
|
||||
const failedInfo: RustdeskInfo = {
|
||||
...info,
|
||||
lastError: "Token indisponível para sincronizar acesso remoto",
|
||||
}
|
||||
await writeRustdeskInfo(store, failedInfo)
|
||||
setRustdeskInfo(failedInfo)
|
||||
logDesktop("remoteAccess:sync:skipped", { reason: "missing-token" })
|
||||
return
|
||||
}
|
||||
await sendRequest(machineToken, allowRetry)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (allowRetry && isTokenRevokedMessage(message)) {
|
||||
const healed = await attemptSelfHeal("remote-access")
|
||||
if (healed) {
|
||||
const refreshedToken = (await readToken(store)) ?? machineToken
|
||||
return syncRustdeskAccess(refreshedToken, info, false)
|
||||
}
|
||||
}
|
||||
console.error("Falha ao sincronizar acesso remoto com a plataforma", error)
|
||||
const failedInfo: RustdeskInfo = { ...info, lastError: message }
|
||||
await writeRustdeskInfo(store, failedInfo)
|
||||
setRustdeskInfo(failedInfo)
|
||||
if (allowRetry && isTokenRevokedMessage(message)) {
|
||||
const healed = await attemptSelfHeal("remote-access")
|
||||
if (healed) {
|
||||
const refreshedToken = await resolveToken(false)
|
||||
if (refreshedToken) {
|
||||
return syncRemoteAccessNow(failedInfo, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
logDesktop("remoteAccess:sync:failed", { id: info.id, error: message })
|
||||
}
|
||||
},
|
||||
[store, attemptSelfHeal]
|
||||
[store, token, config?.machineId, attemptSelfHeal, setToken]
|
||||
)
|
||||
|
||||
const provisionRustdesk = useCallback(
|
||||
async (machineId: string, machineToken: string): Promise<RustdeskInfo | null> => {
|
||||
if (!store || !machineId) return null
|
||||
const handleRustdeskProvision = useCallback(
|
||||
async (payload: RustdeskProvisioningResult) => {
|
||||
if (!store) return
|
||||
const normalized: RustdeskInfo = {
|
||||
...payload,
|
||||
installedVersion: payload.installedVersion ?? null,
|
||||
lastSyncedAt: rustdeskInfoRef.current?.lastSyncedAt ?? null,
|
||||
lastError: null,
|
||||
}
|
||||
await writeRustdeskInfo(store, normalized)
|
||||
setRustdeskInfo(normalized)
|
||||
await syncRemoteAccessNow(normalized)
|
||||
},
|
||||
[store, syncRemoteAccessNow]
|
||||
)
|
||||
|
||||
const ensureRustdesk = useCallback(async () => {
|
||||
if (!store) return null
|
||||
setIsRustdeskProvisioning(true)
|
||||
try {
|
||||
const result = await invoke<RustdeskProvisioningResult>("provision_rustdesk", { machineId })
|
||||
const info: RustdeskInfo = {
|
||||
...result,
|
||||
lastProvisionedAt: Date.now(),
|
||||
lastSyncedAt: null,
|
||||
}
|
||||
await writeRustdeskInfo(store, info)
|
||||
setRustdeskInfo(info)
|
||||
if (machineToken) {
|
||||
await syncRustdeskAccess(machineToken, info)
|
||||
}
|
||||
return info
|
||||
const payload = await invoke<RustdeskProvisioningResult>("ensure_rustdesk_and_emit", {
|
||||
configString: RUSTDESK_CONFIG_STRING || null,
|
||||
password: RUSTDESK_DEFAULT_PASSWORD || null,
|
||||
machineId: config?.machineId ?? null,
|
||||
})
|
||||
await handleRustdeskProvision(payload)
|
||||
return payload
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (message.toLowerCase().includes("apenas no windows")) {
|
||||
|
|
@ -810,15 +863,40 @@ const provisionRustdesk = useCallback(
|
|||
} finally {
|
||||
setIsRustdeskProvisioning(false)
|
||||
}
|
||||
},
|
||||
[store, syncRustdeskAccess]
|
||||
)
|
||||
}, [store, config?.machineId, handleRustdeskProvision])
|
||||
|
||||
useEffect(() => {
|
||||
if (!store) return
|
||||
let disposed = false
|
||||
let unsubscribe: (() => void) | null = null
|
||||
|
||||
listen<RustdeskProvisioningResult>("raven://remote-access/provisioned", async (event) => {
|
||||
try {
|
||||
await handleRustdeskProvision(event.payload)
|
||||
} catch (error) {
|
||||
console.error("Falha ao processar evento de provisioning do RustDesk", error)
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (disposed) {
|
||||
unlisten()
|
||||
} else {
|
||||
unsubscribe = unlisten
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Falha ao registrar listener do RustDesk", error))
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
if (unsubscribe) unsubscribe()
|
||||
}
|
||||
}, [store, handleRustdeskProvision])
|
||||
|
||||
useEffect(() => {
|
||||
if (!store || !config?.machineId || !token) return
|
||||
if (!store) return
|
||||
if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) {
|
||||
rustdeskBootstrapRef.current = true
|
||||
provisionRustdesk(config.machineId, token).finally(() => {
|
||||
ensureRustdesk().finally(() => {
|
||||
rustdeskBootstrapRef.current = false
|
||||
})
|
||||
return
|
||||
|
|
@ -827,10 +905,10 @@ useEffect(() => {
|
|||
const lastSync = rustdeskInfo.lastSyncedAt ?? 0
|
||||
const needsSync = Date.now() - lastSync > RUSTDESK_SYNC_INTERVAL_MS
|
||||
if (needsSync) {
|
||||
syncRustdeskAccess(token, rustdeskInfo)
|
||||
syncRemoteAccessNow(rustdeskInfo)
|
||||
}
|
||||
}
|
||||
}, [store, config?.machineId, token, rustdeskInfo, provisionRustdesk, syncRustdeskAccess, isRustdeskProvisioning])
|
||||
}, [store, rustdeskInfo, ensureRustdesk, syncRemoteAccessNow, isRustdeskProvisioning])
|
||||
|
||||
async function register() {
|
||||
if (!profile) return
|
||||
|
|
@ -907,7 +985,7 @@ useEffect(() => {
|
|||
},
|
||||
})
|
||||
|
||||
await provisionRustdesk(data.machineId, data.machineToken)
|
||||
await ensureRustdesk()
|
||||
logDesktop("register:rustdesk:done", { machineId: data.machineId })
|
||||
|
||||
// Abre o sistema imediatamente após registrar (evita ficar com token inválido no fluxo antigo)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue