localAuth.test.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import type Koa from 'koa'
  5. import { afterEach, describe, expect, it, vi } from 'vitest'
  6. import { LocalAuthStore } from '../../src/server/localAuth.js'
  7. const homes: string[] = []
  8. function context(cookie = ''): Koa.Context {
  9. const headers: Record<string, string | string[]> = {}
  10. return {
  11. state: {},
  12. get: (name: string) => name.toLowerCase() === 'cookie' ? cookie : '',
  13. set: (name: string, value: string | string[]) => { headers[name.toLowerCase()] = value },
  14. response: { headers },
  15. } as unknown as Koa.Context
  16. }
  17. function issuedCookie(ctx: Koa.Context): string {
  18. const value = ctx.response.headers['set-cookie']
  19. const raw = Array.isArray(value) ? value.at(-1) : String(value)
  20. return raw.split(';', 1)[0]!
  21. }
  22. afterEach(() => {
  23. vi.restoreAllMocks()
  24. for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true })
  25. })
  26. describe('15300 local users', () => {
  27. it('requires an explicit first administrator and persists its session', () => {
  28. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-'))
  29. homes.push(home)
  30. const store = new LocalAuthStore(home)
  31. expect(store.setupRequired).toBe(true)
  32. const setup = context()
  33. const admin = store.setupAdmin(setup, 'owner', 'new-password')
  34. expect(admin).toMatchObject({ username: 'owner', role: 'admin', mustChangePassword: false })
  35. expect(store.setupRequired).toBe(false)
  36. expect(() => store.setupAdmin(context(), 'second', 'new-password')).toThrow(/已经初始化/)
  37. expect(store.require(context(issuedCookie(setup))).username).toBe('owner')
  38. const restarted = new LocalAuthStore(home)
  39. expect(restarted.require(context(issuedCookie(setup))).username).toBe('owner')
  40. })
  41. it('lets the administrator create, reset, disable and delete ordinary users', () => {
  42. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-'))
  43. homes.push(home)
  44. const store = new LocalAuthStore(home)
  45. const first = context()
  46. store.setupAdmin(first, 'admin', 'new-password')
  47. const admin = store.require(context(issuedCookie(first)))
  48. const user = store.create(admin, 'member', 'temporary-password')
  49. expect(user.mustChangePassword).toBe(true)
  50. expect(store.updateUser(admin, user.id, { enabled: false }).enabled).toBe(false)
  51. store.deleteUser(admin, user.id)
  52. expect(store.list(admin).map(item => item.username)).toEqual(['admin'])
  53. })
  54. it('persists an account image without invalidating the existing session', () => {
  55. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-')); homes.push(home)
  56. const store = new LocalAuthStore(home), login = context()
  57. store.setupAdmin(login, 'admin', 'new-password')
  58. const active = context(issuedCookie(login))
  59. const avatar = 'data:image/png;base64,aGVsbG8='
  60. expect(store.setAvatar(active, avatar).avatar).toBe(avatar)
  61. expect(store.require(active).avatar).toBe(avatar)
  62. expect(new LocalAuthStore(home).require(context(issuedCookie(login))).avatar).toBe(avatar)
  63. expect(() => store.setAvatar(active, 'https://example.test/avatar.png')).toThrow(/PNG、JPEG/)
  64. expect(store.setAvatar(active, null).avatar).toBeUndefined()
  65. })
  66. it('encrypts the 9119 service password at rest', () => {
  67. const home = mkdtempSync(join(tmpdir(), 'yaoyao-local-auth-'))
  68. homes.push(home)
  69. const store = new LocalAuthStore(home)
  70. const first = context()
  71. store.setupAdmin(first, 'admin', 'new-password')
  72. const admin = store.require(context(issuedCookie(first)))
  73. store.setUpstreamCredentials(admin, { username: 'service', password: 'very-secret' })
  74. expect(store.upstreamCredentials()).toEqual({ username: 'service', password: 'very-secret' })
  75. expect(readFileSync(join(home, 'upstream-credentials.enc'), 'utf8')).not.toContain('very-secret')
  76. })
  77. })
  78. it('desktop authorizations survive time and server restart, but logout and password changes revoke them', () => {
  79. const home = mkdtempSync(join(tmpdir(), 'yaoyao-desktop-auth-')); homes.push(home)
  80. let store = new LocalAuthStore(home)
  81. store.setupAdmin(context(), 'admin', 'initial-password')
  82. const login = context(); store.login(login, 'admin', 'initial-password', true)
  83. const cookie = issuedCookie(login)
  84. const sessions = JSON.parse(readFileSync(join(home, 'sessions.json'), 'utf8'))
  85. expect(sessions.sessions.some((s: { desktop?: boolean; expiresAt: number }) => s.desktop && s.expiresAt === Number.MAX_SAFE_INTEGER)).toBe(true)
  86. const later = Date.now() + 500 * 86400 * 1000
  87. vi.spyOn(Date, 'now').mockReturnValue(later)
  88. store = new LocalAuthStore(home)
  89. expect(store.require(context(cookie)).role).toBe('admin')
  90. const change = context(cookie)
  91. store.changeCredentials(change, 'initial-password', 'updated-password')
  92. expect(store.current(context(cookie))).toBeUndefined()
  93. expect(store.current(change)).toBeUndefined()
  94. const next = context(); store.login(next, 'admin', 'updated-password', true)
  95. const nextCookie = issuedCookie(next)
  96. store.logout(context(nextCookie))
  97. expect(new LocalAuthStore(home).current(context(nextCookie))).toBeUndefined()
  98. })
  99. it('upgrades an existing browser session and invalidates the replaced token', () => {
  100. const home = mkdtempSync(join(tmpdir(), 'yaoyao-upgrade-auth-')); homes.push(home)
  101. const store = new LocalAuthStore(home), setup = context()
  102. store.setupAdmin(setup, 'owner', 'initial-password')
  103. const oldCookie = issuedCookie(setup), upgrade = context(oldCookie)
  104. store.persistDesktopSession(upgrade)
  105. expect(store.current(context(oldCookie))).toBeUndefined()
  106. expect(new LocalAuthStore(home).require(context(issuedCookie(upgrade))).username).toBe('owner')
  107. expect(() => store.persistDesktopSession(context())).toThrow(/请先登录/)
  108. })
  109. it('local desktop creates or reuses the real administrator, without a public default password', () => {
  110. const home = mkdtempSync(join(tmpdir(), 'yaoyao-native-admin-')); homes.push(home)
  111. let store = new LocalAuthStore(home)
  112. const native = context(), admin = store.localDesktopSession(native)
  113. expect(admin).toMatchObject({ username: 'admin', role: 'admin', localDesktop: true, mustChangePassword: false })
  114. expect(store.current(context())).toBeUndefined()
  115. expect(() => store.login(context(), 'admin', 'admin')).toThrow()
  116. const active = context(issuedCookie(native))
  117. store.changeCredentials(active, '', 'remote-password')
  118. store = new LocalAuthStore(home)
  119. expect(store.localDesktopSession(context()).id).toBe(admin.id)
  120. const remote = context(); store.login(remote, 'admin', 'remote-password', true)
  121. expect(store.require(context(issuedCookie(remote))).localDesktop).toBeUndefined()
  122. expect(() => store.changeCredentials(context(issuedCookie(remote)), '', 'another-password')).toThrow('当前密码错误')
  123. })