session.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import type { ProviderProps } from 'react'
  2. import type { ValueStorage } from '@/lib/valueStorage'
  3. import api from '@/api'
  4. import { useConnection } from '@/hooks'
  5. import { createContext, createElement, useLayoutEffect, useState } from 'react'
  6. export interface SessionData {
  7. account?: api.WithId<api.Account>
  8. loggedIn?: boolean
  9. ready?: boolean
  10. }
  11. export interface SessionProps {
  12. authStorage: ValueStorage<api.LoginAccountResponse>
  13. }
  14. export interface SessionState extends SessionData {
  15. heartbeat(): Promise<void>
  16. login(email: string, password: string): Promise<void>
  17. logout(): void
  18. }
  19. export function SessionProvider({ children, value: { authStorage } }: ProviderProps<SessionProps>) {
  20. const { options, setToken } = useConnection()
  21. const [account, setAccount] = useState<api.WithId<api.Account>>()
  22. const [loggedIn, setLoggedIn] = useState(false)
  23. const [ready, setReady] = useState(false)
  24. function reset() {
  25. authStorage.del()
  26. setAccount(undefined)
  27. setToken(undefined)
  28. setLoggedIn(false)
  29. }
  30. async function heartbeat(token?: string) {
  31. try {
  32. const res = await api.getAccount({ ...options, token: token || options.token })
  33. setAccount(res.account)
  34. if (token) setToken(token)
  35. setLoggedIn(true)
  36. } catch (err) {
  37. const status = (err as api.RequestError).xhr?.status || 500
  38. // If the response code indicates a client-side issue, reset session state
  39. if (status >= 400 && status < 500) {
  40. reset()
  41. }
  42. throw err
  43. }
  44. }
  45. async function login(email: string, password: string) {
  46. const res = await api.loginAccount(options, {
  47. account: { email, password },
  48. })
  49. authStorage.set(res)
  50. setAccount(res.account)
  51. setToken(res.token)
  52. setLoggedIn(true)
  53. }
  54. function logout() {
  55. reset()
  56. }
  57. // Check whether a session exists in storage, and if so, send a heartbeat.
  58. // This effect triggers once only when the component is mounted.
  59. useLayoutEffect(() => {
  60. if (!ready) {
  61. const auth = authStorage.get()
  62. if (auth?.token) {
  63. heartbeat(auth.token)
  64. .catch(err => {
  65. console.log(err)
  66. })
  67. .finally(() => {
  68. setReady(true)
  69. })
  70. } else setReady(true)
  71. }
  72. // eslint-disable-next-line react-hooks/exhaustive-deps
  73. }, [])
  74. const value = {
  75. account, loggedIn, ready,
  76. heartbeat, login, logout,
  77. }
  78. return createElement(SessionContext.Provider, { value }, children)
  79. }
  80. export const SessionContext = createContext({} as SessionState)