Browse Source

fix(desktop): 同一台卫星电脑只登记一行

再次登录沿用安装编号,不再新开一台。同名且没有编号的旧记录会被停用。
Samien 1 month ago
parent
commit
ea7993f

+ 8 - 0
desktop/host-manager.mjs

@@ -44,6 +44,14 @@ export class DesktopHostManager {
   this.generation=0
  }
  publish(message){this.state=message;this.options.onState?.(message)}
+ async installIdentity(){
+  const path=join(this.options.home,'desktop-install-id')
+  try{const id=(await readFile(path,'utf8')).trim();if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id))return id}catch(error){if(error.code!=='ENOENT')throw error}
+  const id=randomUUID()
+  await mkdir(this.options.home,{recursive:true,mode:0o700})
+  await writeFile(path,id,{mode:0o600})
+  return id
+ }
  serial(work){const result=this.chain.catch(()=>{}).then(work);this.chain=result;return result}
  async read(){
   let encrypted

+ 1 - 1
desktop/main.mjs

@@ -345,7 +345,7 @@ else {
         const auth=await remoteSession(server,{username:value?.username,password:value?.password})
         let registered=false
         if(auth.user?.role==='admin'){
-          const registration=await enrollDesktopHost(auth,{name:hostname()})
+          const registration=await enrollDesktopHost(auth,{name:hostname(),installId:await hostManager.installIdentity()})
           await hostManager.adopt({protocol:1,serverURL:server,hostId:registration.host.id,token:registration.token,...(server.startsWith('http://')?{allowInsecureLan:true}:{})})
           registered=true
         }

+ 2 - 2
desktop/remote-login.mjs

@@ -47,12 +47,12 @@ export async function remoteSession(serverURL,{username,password},fetchImpl=glob
   return {origin:base,user:result.user,csrfToken:String(result.csrfToken??info.csrfToken),cookies:[...jar.cookies.entries()]}
 }
 
-export async function enrollDesktopHost(session,{name},fetchImpl=globalThis.fetch){
+export async function enrollDesktopHost(session,{name,installId},fetchImpl=globalThis.fetch){
   const cookie=session.cookies.map(([key,value])=>`${key}=${value}`).join('; ')
   const response=await fetchImpl(new URL('/api/app/admin/desktop-hosts',session.origin),{
     method:'POST',redirect:'error',
     headers:{accept:'application/json','content-type':'application/json',cookie,'x-csrf-token':session.csrfToken,origin:session.origin},
-    body:JSON.stringify({name:String(name).slice(0,100)}),
+    body:JSON.stringify({name:String(name).slice(0,100),...(installId?{installId}:{})}),
   })
   if(response.status===403)throw new Error('需要管理员账号,才能把这台 Mac 注册为可远程控制的桌面主机')
   if(!response.ok)throw new Error(`注册桌面主机失败(HTTP ${response.status})`)

+ 24 - 4
src/server/desktopHosts.ts

@@ -9,7 +9,7 @@ import { DESKTOP_HOST_PROTOCOL, type DesktopHostExchange, type DesktopHostRecord
 import { readComputerNames, saveComputerName } from './computerNames.js'
 
 const hash = (value: string) => createHash('sha256').update(value).digest('hex')
-const enrollInput = z.object({ name: z.string().trim().min(1).max(100) }).strict()
+const enrollInput = z.object({ name: z.string().trim().min(1).max(100), installId: z.string().uuid().optional() }).strict()
 const exchangeInput = z.object({
   host: z.object({ id: z.string().uuid(), name: z.string().max(128), platform: z.string().max(16), screen: z.boolean(), accessibility: z.boolean(), approved: z.array(z.string().regex(/^[a-f0-9]{64}$/)).max(100), full: z.array(z.string().regex(/^[a-f0-9]{64}$/)).max(100).optional() }).strict(),
   results: z.array(z.object({ id: z.string().uuid(), value: z.unknown().optional(), error: z.string().max(500).optional() }).strict()).max(20),
@@ -29,9 +29,29 @@ export class DesktopHostHub {
   enroll(value: unknown) {
     const body = parse(enrollInput, value)
     const token = randomBytes(32).toString('base64url')
-    const record: DesktopHostRecord = { ...body, id: randomUUID(), tokenHash: hash(token), enabled: true, createdAt: Date.now() }
-    this.store.put('_system', 'desktop-host', record.id, record)
-    return { host: this.summary(record), token }
+    const records = this.records()
+    let record = body.installId ? records.find(item => item.installId === body.installId) : undefined
+    if (!record && body.installId) {
+      const unnamed = records.filter(item => item.enabled && item.name === body.name && !item.installId).sort((a, b) => a.createdAt - b.createdAt)
+      record = unnamed[0]
+      for (const extra of unnamed.slice(1)) {
+        extra.enabled = false
+        this.store.put('_system', 'desktop-host', extra.id, extra)
+        this.online.delete(extra.id)
+        this.drop(extra.id)
+      }
+    }
+    if (record) {
+      record.name = body.name
+      record.installId = body.installId
+      record.tokenHash = hash(token)
+      record.enabled = true
+      this.store.put('_system', 'desktop-host', record.id, record)
+      return { host: this.summary(record), token }
+    }
+    const created: DesktopHostRecord = { name: body.name, installId: body.installId, id: randomUUID(), tokenHash: hash(token), enabled: true, createdAt: Date.now() }
+    this.store.put('_system', 'desktop-host', created.id, created)
+    return { host: this.summary(created), token }
   }
   summary(record: DesktopHostRecord): DesktopHostSummary {
     const { tokenHash: _secret, ...publicRecord } = record

+ 2 - 0
src/shared/desktopHost.ts

@@ -5,6 +5,8 @@ export interface DesktopHostRecord {
   tokenHash: string
   enabled: boolean
   createdAt: number
+  /** Stable id of the installed Mac. Re-login reuses this row instead of adding another. */
+  installId?: string
 }
 export interface DesktopHostSummary extends Omit<DesktopHostRecord, 'tokenHash'> {
   online: boolean

+ 10 - 0
tests/server/desktopHosts.test.ts

@@ -73,3 +73,13 @@ it('validates the exchange payload shape',async()=>{
   await post(`/api/desktop-host/v1/${hostId}/exchange`).send({host:{id:'not-a-uuid',name:'x',platform:'darwin',screen:true,accessibility:true,approved:[]},results:[]}).expect(400)
   await post(`/api/desktop-host/v1/${hostId}/exchange`).send('string').expect(400)
 })
+
+it('reuses the same computer when the installed Mac logs in again',async()=>{
+  const installId = '22222222-2222-4222-8222-222222222222'
+  const first = await request(app()).post('/api/app/admin/desktop-hosts').send({ name: '书房 iMac', installId }).expect(201)
+  const again = await request(app()).post('/api/app/admin/desktop-hosts').send({ name: '书房 iMac', installId }).expect(201)
+  expect(again.body.host.id).toBe(first.body.host.id)
+  expect(again.body.token).not.toBe(first.body.token)
+  const list = (await request(app()).get('/api/app/admin/desktop-hosts').expect(200)).body
+  expect(list.hosts.filter((host: { enabled: boolean }) => host.enabled)).toHaveLength(1)
+})