feat(frontend): Refactor to cookie-based auth and add /api/v1 prefix

This commit is contained in:
Yunxiao Xu
2026-02-11 22:06:14 -08:00
parent 42f982e373
commit 7fe020f26c
8 changed files with 153 additions and 49 deletions

View File

@@ -0,0 +1,10 @@
import axios from "axios"
const API_URL = import.meta.env.VITE_API_URL || ""
const api = axios.create({
baseURL: `${API_URL}/api/v1`,
withCredentials: true, // Crucial for HttpOnly cookies
})
export default api

View File

@@ -1,47 +1,37 @@
import { describe, it, expect, vi, beforeEach, type MockInstance } from "vitest"
import axios from "axios"
import api from "./api"
import { AuthService } from "./auth"
vi.mock("axios")
const mockedAxios = axios as unknown as {
post: MockInstance<typeof axios.post>
vi.mock("./api")
const mockedApi = api as unknown as {
post: MockInstance<typeof api.post>
get: MockInstance<typeof api.get>
}
describe("AuthService", () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
})
it("successfully logs in and stores the token", async () => {
it("successfully logs in", async () => {
const mockResponse = {
data: {
access_token: "fake-jwt-token",
token_type: "bearer",
},
}
mockedAxios.post.mockResolvedValueOnce(mockResponse)
mockedApi.post.mockResolvedValueOnce(mockResponse)
const result = await AuthService.login("test@example.com", "password123")
expect(mockedAxios.post).toHaveBeenCalledWith("/auth/login", expect.any(FormData))
expect(mockedApi.post).toHaveBeenCalledWith("/auth/login", expect.any(FormData))
// Validate FormData content
const formData = (mockedAxios.post.mock.calls[0][1] as FormData)
const formData = (mockedApi.post.mock.calls[0][1] as FormData)
expect(formData.get("username")).toBe("test@example.com")
expect(formData.get("password")).toBe("password123")
expect(result.access_token).toBe("fake-jwt-token")
expect(localStorage.getItem("token")).toBe("fake-jwt-token")
expect(AuthService.isAuthenticated()).toBe(true)
})
it("handles login failure", async () => {
mockedAxios.post.mockRejectedValueOnce(new Error("Invalid credentials"))
await expect(AuthService.login("test@example.com", "wrong")).rejects.toThrow("Invalid credentials")
expect(localStorage.getItem("token")).toBeNull()
expect(AuthService.isAuthenticated()).toBe(false)
})
it("successfully registers a user", async () => {
@@ -51,22 +41,34 @@ describe("AuthService", () => {
email: "test@example.com",
},
}
mockedAxios.post.mockResolvedValueOnce(mockResponse)
mockedApi.post.mockResolvedValueOnce(mockResponse)
const result = await AuthService.register("test@example.com", "password123")
expect(mockedAxios.post).toHaveBeenCalledWith("/auth/register", {
expect(mockedApi.post).toHaveBeenCalledWith("/auth/register", {
email: "test@example.com",
password: "password123",
})
expect(result.email).toBe("test@example.com")
})
it("logs out and clears the token", () => {
localStorage.setItem("token", "some-token")
expect(AuthService.isAuthenticated()).toBe(true)
AuthService.logout()
expect(localStorage.getItem("token")).toBeNull()
expect(AuthService.isAuthenticated()).toBe(false)
it("successfully fetches current user", async () => {
const mockUser = {
id: "user-123",
email: "test@example.com",
display_name: "Test User"
}
mockedApi.get.mockResolvedValueOnce({ data: mockUser })
const result = await AuthService.getMe()
expect(mockedApi.get).toHaveBeenCalledWith("/auth/me")
expect(result).toEqual(mockUser)
})
it("logs out and calls backend", async () => {
mockedApi.post.mockResolvedValueOnce({ data: { detail: "success" } })
await AuthService.logout()
expect(mockedApi.post).toHaveBeenCalledWith("/auth/logout")
})
})

View File

@@ -1,6 +1,4 @@
import axios from "axios"
const API_URL = import.meta.env.VITE_API_URL || ""
import api from "./api"
export interface AuthResponse {
access_token: string
@@ -10,38 +8,40 @@ export interface AuthResponse {
export interface UserResponse {
id: string
email: string
display_name?: string
}
export const AuthService = {
async login(email: string, password: string): Promise<AuthResponse> {
const formData = new FormData()
formData.append("username", email) // OAuth2PasswordRequestForm uses 'username'
formData.append("username", email)
formData.append("password", password)
const response = await axios.post<AuthResponse>(`${API_URL}/auth/login`, formData)
if (response.data.access_token) {
localStorage.setItem("token", response.data.access_token)
}
const response = await api.post<AuthResponse>("/auth/login", formData)
return response.data
},
async loginWithOIDC() {
const response = await api.get<{ url: string }>("/auth/oidc/login")
if (response.data.url) {
window.location.href = response.data.url
}
},
async register(email: string, password: string): Promise<UserResponse> {
const response = await axios.post<UserResponse>(`${API_URL}/auth/register`, {
const response = await api.post<UserResponse>("/auth/register", {
email,
password,
})
return response.data
},
logout() {
localStorage.removeItem("token")
async getMe(): Promise<UserResponse> {
const response = await api.get<UserResponse>("/auth/me")
return response.data
},
getToken() {
return localStorage.getItem("token")
},
isAuthenticated() {
return !!this.getToken()
async logout() {
await api.post("/auth/logout")
},
}