auth.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { computed, ref } from 'vue'
  2. import { defineStore } from 'pinia'
  3. import type { AuthStatus, BootstrapResponse, CurrentUser, Profile } from '@shared/types'
  4. import * as authApi from '@/api/auth'
  5. import { ApiError, clearApiSecurityContext, onApiUnauthorized } from '@/api/client'
  6. function message(error: unknown): string {
  7. return error instanceof Error ? error.message : '认证请求失败'
  8. }
  9. export const useAuthStore = defineStore('auth', () => {
  10. const status = ref<AuthStatus>('checking')
  11. const user = ref<CurrentUser>()
  12. const profiles = ref<Profile[]>([])
  13. const activeProfileName = ref('')
  14. const csrfToken = ref('')
  15. const error = ref<string>()
  16. const authRequired = ref(true)
  17. const insecureLan = ref(false)
  18. const groupUploadsEnabled = ref(false)
  19. const activeProfile = computed(() => profiles.value.find(profile => profile.name === activeProfileName.value)
  20. ?? profiles.value.find(profile => profile.isDefault)
  21. ?? profiles.value[0])
  22. const isAuthenticated = computed(() => status.value === 'authenticated')
  23. function publish(response: BootstrapResponse): void {
  24. authRequired.value = response.authRequired
  25. csrfToken.value = response.csrfToken
  26. insecureLan.value = Boolean(response.insecureLan)
  27. groupUploadsEnabled.value = Boolean(response.groupUploadsEnabled)
  28. profiles.value = response.profiles
  29. user.value = response.user ?? (!response.authRequired
  30. ? { id: 'local', username: '本机 Hermes', role: 'local' }
  31. : undefined)
  32. if (!profiles.value.some(profile => profile.name === activeProfileName.value)) {
  33. activeProfileName.value = profiles.value.find(profile => profile.isDefault)?.name ?? profiles.value[0]?.name ?? ''
  34. }
  35. status.value = user.value ? 'authenticated' : 'anonymous'
  36. error.value = undefined
  37. }
  38. function expire(): void {
  39. if (status.value === 'anonymous' || status.value === 'checking') return
  40. status.value = 'expired'
  41. user.value = undefined
  42. profiles.value = []
  43. activeProfileName.value = ''
  44. csrfToken.value = ''
  45. clearApiSecurityContext()
  46. }
  47. onApiUnauthorized(expire)
  48. async function bootstrap(): Promise<void> {
  49. status.value = 'checking'
  50. error.value = undefined
  51. try { publish(await authApi.bootstrap()) }
  52. catch (cause) {
  53. if (cause instanceof ApiError && [401, 403].includes(cause.status)) status.value = 'anonymous'
  54. else status.value = 'error'
  55. error.value = message(cause)
  56. user.value = undefined
  57. profiles.value = []
  58. }
  59. }
  60. async function login(input: authApi.LoginInput): Promise<void> {
  61. status.value = 'authenticating'
  62. error.value = undefined
  63. try { publish(await authApi.login(input)) }
  64. catch (cause) {
  65. status.value = 'anonymous'
  66. error.value = message(cause)
  67. throw cause
  68. }
  69. }
  70. async function logout(): Promise<void> {
  71. try { await authApi.logout() } catch { /* local state still signs out */ }
  72. status.value = 'anonymous'
  73. user.value = undefined
  74. profiles.value = []
  75. activeProfileName.value = ''
  76. csrfToken.value = ''
  77. clearApiSecurityContext()
  78. try { publish(await authApi.bootstrap()) } catch { status.value = 'anonymous' }
  79. }
  80. function selectProfile(name: string): void {
  81. if (!profiles.value.some(profile => profile.name === name)) throw new Error(`未知 Profile:${name}`)
  82. activeProfileName.value = name
  83. }
  84. return {
  85. status, user, profiles, activeProfileName, activeProfile, csrfToken, error, authRequired, insecureLan, groupUploadsEnabled,
  86. isAuthenticated, bootstrap, login, logout, selectProfile, expire,
  87. }
  88. })