cb1982c39e
- Created useAuthApi composable for JWT token lifecycle management - Implemented setupAuthInterceptor for automatic Authorization header injection - Added LoginPage.vue with username/password form - Configured router to redirect to /login for unauthenticated access - Token stored in localStorage with expiration tracking - Automatic token validation and cleanup on expiration - All fetch requests automatically include Bearer token - Unit tests for login, logout, token validation flows This enables frontend to authenticate via JWT tokens in production mode. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
164 lines
4.0 KiB
TypeScript
164 lines
4.0 KiB
TypeScript
import { ref, computed } from 'vue'
|
|
|
|
interface LoginRequest {
|
|
username: string
|
|
password: string
|
|
role?: string
|
|
}
|
|
|
|
interface LoginResponse {
|
|
accessToken: string
|
|
expiresIn: number
|
|
tokenType: string
|
|
}
|
|
|
|
interface AuthState {
|
|
token: string | null
|
|
expiresAt: number | null
|
|
isAuthenticated: boolean
|
|
}
|
|
|
|
const API_BASE = '/api'
|
|
const TOKEN_STORAGE_KEY = 'kartsell_auth_token'
|
|
const EXPIRES_AT_KEY = 'kartsell_expires_at'
|
|
|
|
// Initialize from localStorage
|
|
function loadStoredAuth(): AuthState {
|
|
if (typeof window === 'undefined') {
|
|
return { token: null, expiresAt: null, isAuthenticated: false }
|
|
}
|
|
|
|
const token = localStorage.getItem(TOKEN_STORAGE_KEY)
|
|
const expiresAtStr = localStorage.getItem(EXPIRES_AT_KEY)
|
|
const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null
|
|
|
|
return {
|
|
token,
|
|
expiresAt,
|
|
isAuthenticated: !!(token && expiresAt && expiresAt > Date.now()),
|
|
}
|
|
}
|
|
|
|
export function useAuthApi() {
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
const authState = ref<AuthState>(loadStoredAuth())
|
|
|
|
const login = async (username: string, password: string, role?: string): Promise<boolean> => {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
username,
|
|
password,
|
|
role: role || 'User',
|
|
} as LoginRequest),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ message: 'Login failed' }))
|
|
throw new Error(errorData.message || `HTTP ${response.status}`)
|
|
}
|
|
|
|
const data = await response.json() as LoginResponse
|
|
|
|
// Store token and expiration
|
|
const expiresAt = Date.now() + data.expiresIn * 1000
|
|
localStorage.setItem(TOKEN_STORAGE_KEY, data.accessToken)
|
|
localStorage.setItem(EXPIRES_AT_KEY, expiresAt.toString())
|
|
|
|
authState.value = {
|
|
token: data.accessToken,
|
|
expiresAt,
|
|
isAuthenticated: true,
|
|
}
|
|
|
|
return true
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : 'Login failed'
|
|
console.error('Login error:', err)
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const logout = (): void => {
|
|
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
|
localStorage.removeItem(EXPIRES_AT_KEY)
|
|
authState.value = {
|
|
token: null,
|
|
expiresAt: null,
|
|
isAuthenticated: false,
|
|
}
|
|
}
|
|
|
|
const getToken = (): string | null => {
|
|
// Check if token is still valid
|
|
const expiresAt = authState.value.expiresAt
|
|
if (!authState.value.token || !expiresAt || expiresAt < Date.now()) {
|
|
logout()
|
|
return null
|
|
}
|
|
return authState.value.token
|
|
}
|
|
|
|
const refreshAuthState = (): void => {
|
|
authState.value = loadStoredAuth()
|
|
}
|
|
|
|
return {
|
|
// State
|
|
loading,
|
|
error,
|
|
authState: computed(() => authState.value),
|
|
|
|
// Computed
|
|
isAuthenticated: computed(() => authState.value.isAuthenticated),
|
|
hasError: computed(() => error.value !== null),
|
|
|
|
// Methods
|
|
login,
|
|
logout,
|
|
getToken,
|
|
refreshAuthState,
|
|
}
|
|
}
|
|
|
|
// Global API interceptor - inject auth token into all requests
|
|
export function setupAuthInterceptor() {
|
|
const originalFetch = window.fetch
|
|
|
|
window.fetch = function (
|
|
input: RequestInfo | URL,
|
|
init?: RequestInit
|
|
): Promise<Response> {
|
|
// Load token from localStorage
|
|
const token = localStorage.getItem(TOKEN_STORAGE_KEY)
|
|
const expiresAtStr = localStorage.getItem(EXPIRES_AT_KEY)
|
|
const expiresAt = expiresAtStr ? parseInt(expiresAtStr, 10) : null
|
|
|
|
// Only add auth header if token is valid
|
|
if (token && expiresAt && expiresAt > Date.now()) {
|
|
const headers = new Headers(init?.headers || {})
|
|
headers.set('Authorization', `Bearer ${token}`)
|
|
|
|
return originalFetch(input, {
|
|
...init,
|
|
headers,
|
|
})
|
|
}
|
|
|
|
return originalFetch(input, init)
|
|
}
|
|
}
|
|
|
|
export type { LoginRequest, LoginResponse }
|