auth.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. const API_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001'
  2. export interface AuthUser {
  3. id: string
  4. email: string
  5. name: string
  6. avatarUrl: string | null
  7. }
  8. export interface AuthTokens {
  9. accessToken: string
  10. refreshToken: string
  11. expiresIn: number
  12. user: AuthUser
  13. }
  14. const ACCESS_TOKEN_KEY = 'timelapse_access_token'
  15. const REFRESH_TOKEN_KEY = 'timelapse_refresh_token'
  16. const USER_KEY = 'timelapse_user'
  17. // ── Token storage ────────────────────────────────────────────────
  18. export function getAccessToken(): string | null {
  19. if (typeof window === 'undefined') return null
  20. return localStorage.getItem(ACCESS_TOKEN_KEY)
  21. }
  22. export function getRefreshToken(): string | null {
  23. if (typeof window === 'undefined') return null
  24. return localStorage.getItem(REFRESH_TOKEN_KEY)
  25. }
  26. export function getStoredUser(): AuthUser | null {
  27. if (typeof window === 'undefined') return null
  28. const raw = localStorage.getItem(USER_KEY)
  29. if (!raw) return null
  30. try { return JSON.parse(raw) } catch { return null }
  31. }
  32. export function storeTokens(tokens: AuthTokens): void {
  33. localStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken)
  34. localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken)
  35. localStorage.setItem(USER_KEY, JSON.stringify(tokens.user))
  36. }
  37. export function clearAuth(): void {
  38. localStorage.removeItem(ACCESS_TOKEN_KEY)
  39. localStorage.removeItem(REFRESH_TOKEN_KEY)
  40. localStorage.removeItem(USER_KEY)
  41. }
  42. // ── API helpers ──────────────────────────────────────────────────
  43. async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
  44. const token = getAccessToken()
  45. const headers: Record<string, string> = {
  46. 'Content-Type': 'application/json',
  47. ...(options.headers as Record<string, string>),
  48. }
  49. if (token) headers['Authorization'] = `Bearer ${token}`
  50. const res = await fetch(`${API_URL}${path}`, { ...options, headers })
  51. if (res.status === 401) {
  52. // Try refresh
  53. const refreshed = await tryRefresh()
  54. if (refreshed) {
  55. const retryHeaders = { ...headers, Authorization: `Bearer ${getAccessToken()}` }
  56. const retry = await fetch(`${API_URL}${path}`, { ...options, headers: retryHeaders })
  57. if (!retry.ok) throw new Error(`API error ${retry.status}`)
  58. return retry.json() as Promise<T>
  59. }
  60. clearAuth()
  61. if (typeof window !== 'undefined') window.location.href = '/login'
  62. throw new Error('Unauthorized')
  63. }
  64. if (!res.ok) {
  65. const body = await res.json().catch(() => ({ message: 'Unknown error' }))
  66. throw new Error(body.message || `API error ${res.status}`)
  67. }
  68. return res.json() as Promise<T>
  69. }
  70. export async function tryRefresh(): Promise<boolean> {
  71. const refreshToken = getRefreshToken()
  72. if (!refreshToken) return false
  73. try {
  74. const res = await fetch(`${API_URL}/v1/auth/refresh`, {
  75. method: 'POST',
  76. headers: { 'Content-Type': 'application/json' },
  77. body: JSON.stringify({ refreshToken }),
  78. })
  79. if (!res.ok) return false
  80. const tokens: AuthTokens = await res.json()
  81. storeTokens(tokens)
  82. return true
  83. } catch {
  84. return false
  85. }
  86. }
  87. // ── Auth API calls ──────────────────────────────────────────────
  88. export const authApi = {
  89. login: (email: string, password: string) =>
  90. apiFetch<AuthTokens>('/v1/auth/login', {
  91. method: 'POST',
  92. body: JSON.stringify({ email, password }),
  93. }),
  94. register: (email: string, password: string, name: string) =>
  95. apiFetch<AuthTokens>('/v1/auth/register', {
  96. method: 'POST',
  97. body: JSON.stringify({ email, password, name }),
  98. }),
  99. logout: () =>
  100. apiFetch('/v1/auth/logout', {
  101. method: 'POST',
  102. body: JSON.stringify({ refreshToken: getRefreshToken() }),
  103. }).finally(() => clearAuth()),
  104. me: () => apiFetch<AuthUser>('/v1/auth/me'),
  105. }
  106. // ── Devices API (JWT-authenticated) ─────────────────────────────
  107. export const devicesApi = {
  108. list: (projectId?: string) => {
  109. const qs = projectId ? `?projectId=${projectId}` : ''
  110. return apiFetch<any[]>(`/v1/devices${qs}`)
  111. },
  112. get: (id: string) => apiFetch<any>(`/v1/devices/${id}`),
  113. stats: () => apiFetch<{ devicesOnline: number; devicesOffline: number; capturesToday: number; devicesTotal: number }>('/v1/devices/stats'),
  114. heartbeats: (id: string, limit?: number) => {
  115. const qs = limit ? `?limit=${limit}` : ''
  116. return apiFetch<any[]>(`/v1/devices/${id}/heartbeats${qs}`)
  117. },
  118. }