| 1 | // Thin wrapper over fetch: attaches the JWT, unwraps JSON, surfaces the
|
|---|
| 2 | // backend's error message instead of a bare "500".
|
|---|
| 3 |
|
|---|
| 4 | const TOKEN_KEY = 'nutritioneer.token'
|
|---|
| 5 | const USER_KEY = 'nutritioneer.user'
|
|---|
| 6 |
|
|---|
| 7 | export const auth = {
|
|---|
| 8 | token: () => localStorage.getItem(TOKEN_KEY),
|
|---|
| 9 | user: () => {
|
|---|
| 10 | const raw = localStorage.getItem(USER_KEY)
|
|---|
| 11 | return raw ? JSON.parse(raw) : null
|
|---|
| 12 | },
|
|---|
| 13 | save: (res) => {
|
|---|
| 14 | localStorage.setItem(TOKEN_KEY, res.token)
|
|---|
| 15 | localStorage.setItem(USER_KEY, JSON.stringify({
|
|---|
| 16 | email: res.email, username: res.username, role: res.role
|
|---|
| 17 | }))
|
|---|
| 18 | },
|
|---|
| 19 | clear: () => {
|
|---|
| 20 | localStorage.removeItem(TOKEN_KEY)
|
|---|
| 21 | localStorage.removeItem(USER_KEY)
|
|---|
| 22 | }
|
|---|
| 23 | }
|
|---|
| 24 |
|
|---|
| 25 | async function request(method, path, body) {
|
|---|
| 26 | const headers = { 'Content-Type': 'application/json' }
|
|---|
| 27 | const token = auth.token()
|
|---|
| 28 | if (token) headers.Authorization = `Bearer ${token}`
|
|---|
| 29 |
|
|---|
| 30 | const res = await fetch(`/api${path}`, {
|
|---|
| 31 | method,
|
|---|
| 32 | headers,
|
|---|
| 33 | body: body === undefined ? undefined : JSON.stringify(body)
|
|---|
| 34 | })
|
|---|
| 35 |
|
|---|
| 36 | if (res.status === 204) return null
|
|---|
| 37 |
|
|---|
| 38 | const text = await res.text()
|
|---|
| 39 | const data = text ? JSON.parse(text) : null
|
|---|
| 40 |
|
|---|
| 41 | if (res.status === 401) {
|
|---|
| 42 | // A 401 while signing in means the credentials were rejected. Only treat
|
|---|
| 43 | // it as an expired session if we actually sent a token.
|
|---|
| 44 | if (token) {
|
|---|
| 45 | auth.clear()
|
|---|
| 46 | throw new Error(data?.message || 'Session expired, please sign in again')
|
|---|
| 47 | }
|
|---|
| 48 | throw new Error(data?.message || 'Invalid email or password')
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | if (!res.ok) throw new Error(data?.message || `Request failed (${res.status})`)
|
|---|
| 52 | return data
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | export const api = {
|
|---|
| 56 | get: (p) => request('GET', p),
|
|---|
| 57 | post: (p, b) => request('POST', p, b),
|
|---|
| 58 | patch: (p, b) => request('PATCH', p, b),
|
|---|
| 59 | del: (p) => request('DELETE', p)
|
|---|
| 60 | }
|
|---|