remote-login.mjs 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. /** Native remote login: username/password against a yaoyao Web server.
  2. * Mirrors the browser's CSRF/Origin dance so no server changes are needed,
  3. * and can enroll this Mac as a desktop host with the logged-in session. */
  4. export function normalizeServerURL(input){
  5. const raw=String(input??'').trim()
  6. if(!raw)throw new Error('请填写夭夭服务器地址')
  7. const withScheme=/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(raw)?raw:`http://${raw}`
  8. let url
  9. try{url=new URL(withScheme)}catch{throw new Error('服务器地址无效')}
  10. if(!['http:','https:'].includes(url.protocol)||url.username||url.password||url.search||url.hash||(url.pathname!==''&&url.pathname!=='/'))throw new Error('服务器地址无效')
  11. return url.origin
  12. }
  13. /** Minimal cookie jar across the bootstrap → login dance. */
  14. class CookieJar{
  15. constructor(){this.cookies=new Map()}
  16. absorb(response){
  17. for(const line of response.headers.getSetCookie?.()??[]){
  18. const pair=line.split(';')[0],eq=pair.indexOf('=')
  19. if(eq>0)this.cookies.set(pair.slice(0,eq).trim(),pair.slice(eq+1).trim())
  20. }
  21. }
  22. header(){return [...this.cookies.entries()].map(([name,value])=>`${name}=${value}`).join('; ')}
  23. }
  24. export async function remoteSession(serverURL,{username,password},fetchImpl=globalThis.fetch){
  25. const base=normalizeServerURL(serverURL)
  26. const user=String(username??'').trim(),pass=String(password??'')
  27. if(!user||!pass)throw new Error('请填写用户名和密码')
  28. const jar=new CookieJar()
  29. const bootstrap=await fetchImpl(new URL('/api/app/bootstrap',base),{headers:{accept:'application/json'},redirect:'error'})
  30. jar.absorb(bootstrap)
  31. if(!bootstrap.ok)throw new Error(`无法连接服务器(HTTP ${bootstrap.status})`)
  32. const info=await bootstrap.json().catch(()=>{throw new Error('服务器响应无效,请确认这是夭夭 Web 地址')})
  33. if(typeof info?.csrfToken!=='string')throw new Error('服务器响应无效,请确认这是夭夭 Web 地址')
  34. const response=await fetchImpl(new URL('/api/app/login',base),{
  35. method:'POST',redirect:'error',
  36. headers:{accept:'application/json','content-type':'application/json',cookie:jar.header(),'x-csrf-token':info.csrfToken,origin:base},
  37. body:JSON.stringify({username:user,password:pass}),
  38. })
  39. jar.absorb(response)
  40. if(response.status===401||response.status===403)throw new Error('用户名或密码不正确')
  41. if(!response.ok)throw new Error(`登录失败(HTTP ${response.status})`)
  42. const result=await response.json().catch(()=>{throw new Error('登录响应无效')})
  43. if(!result?.user?.id)throw new Error('登录响应无效')
  44. return {origin:base,user:result.user,csrfToken:String(result.csrfToken??info.csrfToken),cookies:[...jar.cookies.entries()]}
  45. }
  46. export async function enrollDesktopHost(session,{name,installId},fetchImpl=globalThis.fetch){
  47. const cookie=session.cookies.map(([key,value])=>`${key}=${value}`).join('; ')
  48. const response=await fetchImpl(new URL('/api/app/admin/desktop-hosts',session.origin),{
  49. method:'POST',redirect:'error',
  50. headers:{accept:'application/json','content-type':'application/json',cookie,'x-csrf-token':session.csrfToken,origin:session.origin},
  51. body:JSON.stringify({name:String(name).slice(0,100),...(installId?{installId}:{})}),
  52. })
  53. if(response.status===403)throw new Error('需要管理员账号,才能把这台 Mac 注册为可远程控制的桌面主机')
  54. if(!response.ok)throw new Error(`注册桌面主机失败(HTTP ${response.status})`)
  55. const result=await response.json()
  56. if(typeof result?.token!=='string'||!result?.host?.id)throw new Error('注册桌面主机响应无效')
  57. return result
  58. }