Merge pull request #35 from StewKI/feature-client-with-working-auth
Feature client with working auth
This commit is contained in:
@@ -1,37 +1,90 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
// A small fetch-based HTTP client with automatic token attach and refresh-on-401.
|
||||
// This avoids circular imports by reading/writing tokens directly from localStorage.
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api'
|
||||
const ACCESS_TOKEN = 'auth_token'
|
||||
const REFRESH_TOKEN = 'refresh_token'
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const auth = useAuthStore()
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
async function parseJsonOrThrow(res: Response) {
|
||||
const text = await res.text()
|
||||
try {
|
||||
return text ? JSON.parse(text) : undefined
|
||||
} catch (e) {
|
||||
// non-JSON response
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
if (auth.token) {
|
||||
headers['Authorization'] = `Bearer ${auth.token}`
|
||||
}
|
||||
async function refreshTokens(): Promise<{ accessToken?: string; refreshToken?: string } | null> {
|
||||
const refreshToken = localStorage.getItem(REFRESH_TOKEN)
|
||||
if (!refreshToken) return null
|
||||
|
||||
const res = await fetch(`${BASE_URL}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
const res = await fetch('/api/User/refresh-login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
throw new Error(text || `Request failed: ${res.status}`)
|
||||
if (!res.ok) return null
|
||||
const data = await parseJsonOrThrow(res)
|
||||
const accessToken = data?.accessToken ?? data?.token ?? null
|
||||
const newRefresh = data?.refreshToken ?? null
|
||||
if (accessToken) {
|
||||
localStorage.setItem(ACCESS_TOKEN, accessToken)
|
||||
}
|
||||
if (newRefresh) {
|
||||
localStorage.setItem(REFRESH_TOKEN, newRefresh)
|
||||
}
|
||||
return { accessToken, refreshToken: newRefresh }
|
||||
}
|
||||
|
||||
async function request<T = any>(method: string, url: string, body?: any, attemptRefresh = true): Promise<T> {
|
||||
const headers: Record<string, string> = {}
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
const token = localStorage.getItem(ACCESS_TOKEN)
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
|
||||
const payload = body ?? {}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(payload) : null,
|
||||
})
|
||||
|
||||
if (res.status === 401 && attemptRefresh) {
|
||||
// try refreshing tokens once
|
||||
const refreshed = await refreshTokens()
|
||||
if (refreshed && refreshed.accessToken) {
|
||||
// retry original request with new token
|
||||
const retryHeaders: Record<string, string> = {}
|
||||
if (body !== undefined && body !== null) retryHeaders['Content-Type'] = 'application/json'
|
||||
retryHeaders['Authorization'] = `Bearer ${refreshed.accessToken}`
|
||||
|
||||
const retryRes = await fetch(url, {
|
||||
method,
|
||||
headers: retryHeaders,
|
||||
body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
if (!retryRes.ok) {
|
||||
const errorBody = await parseJsonOrThrow(retryRes)
|
||||
throw Object.assign(new Error(retryRes.statusText || 'Request failed'), { status: retryRes.status, body: errorBody })
|
||||
}
|
||||
return await parseJsonOrThrow(retryRes)
|
||||
}
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T
|
||||
if (!res.ok) {
|
||||
const errorBody = await parseJsonOrThrow(res)
|
||||
throw Object.assign(new Error(res.statusText || 'Request failed'), { status: res.status, body: errorBody })
|
||||
}
|
||||
|
||||
return res.json() as Promise<T>
|
||||
return await parseJsonOrThrow(res)
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>('GET', path),
|
||||
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
|
||||
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
|
||||
delete: <T>(path: string) => request<T>('DELETE', path),
|
||||
get: <T = any>(url: string) => request<T>('GET', url),
|
||||
post: <T = any>(url: string, body?: any) => request<T>('POST', url, body),
|
||||
put: <T = any>(url: string, body?: any) => request<T>('PUT', url, body),
|
||||
delete: <T = any>(url: string, body?: any) => request<T>('DELETE', url, body),
|
||||
}
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import type { AuthResponse, LoginCredentials, SignupCredentials, User } from '@/types'
|
||||
import { api } from './api'
|
||||
|
||||
// TODO: Wire up to real API endpoints via `api` helper
|
||||
// import { api } from './api'
|
||||
function normalizeAuthResponse(raw: any): AuthResponse {
|
||||
const accessToken = raw?.accessToken ?? raw?.AccessToken ?? raw?.token ?? raw?.Token ?? raw?.access_token ?? null
|
||||
const refreshToken = raw?.refreshToken ?? raw?.RefreshToken ?? raw?.refresh_token ?? null
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
|
||||
export const authService = {
|
||||
async login(_credentials: LoginCredentials): Promise<AuthResponse> {
|
||||
// TODO: return api.post<AuthResponse>('/auth/login', credentials)
|
||||
throw new Error('Not implemented')
|
||||
async login(credentials: LoginCredentials): Promise<AuthResponse> {
|
||||
const raw = await api.post<any>('/api/User/login', credentials)
|
||||
return normalizeAuthResponse(raw)
|
||||
},
|
||||
|
||||
async signup(_credentials: SignupCredentials): Promise<AuthResponse> {
|
||||
// TODO: return api.post<AuthResponse>('/auth/signup', credentials)
|
||||
throw new Error('Not implemented')
|
||||
async refreshLogin(refreshToken: string): Promise<AuthResponse> {
|
||||
const raw = await api.post<any>('/api/User/refresh-login', { refreshToken })
|
||||
return normalizeAuthResponse(raw)
|
||||
},
|
||||
|
||||
async logout(): Promise<void> {
|
||||
// TODO: return api.post<void>('/auth/logout')
|
||||
throw new Error('Not implemented')
|
||||
async signup(credentials: SignupCredentials): Promise<AuthResponse> {
|
||||
const raw = await api.post<any>('/api/User/signup', credentials)
|
||||
return normalizeAuthResponse(raw)
|
||||
},
|
||||
|
||||
async getCurrentUser(): Promise<User> {
|
||||
// TODO: return api.get<User>('/auth/me')
|
||||
throw new Error('Not implemented')
|
||||
async logout(refreshToken: string): Promise<void> {
|
||||
return api.delete<void>('/api/User/logout', {refreshToken: refreshToken})
|
||||
},
|
||||
|
||||
async logoutAll(): Promise<void> {
|
||||
return api.delete<void>('/api/User/logout-all')
|
||||
},
|
||||
|
||||
async getMe() : Promise<User> {
|
||||
const raw = await api.get<any>('/api/User/me')
|
||||
// backend User may have fields like userName / UserName and email / Email
|
||||
const username = raw?.userName ?? raw?.UserName ?? raw?.username ?? raw?.name ?? ''
|
||||
const email = raw?.email ?? raw?.Email ?? ''
|
||||
return { username, email }
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user