feat(frontend): Implement HttpOnly cookie authentication and API v1 integration. Update AuthService for cookie-based session management, configure Axios with v1 prefix and credentials, and enhance OIDC callback logic.

This commit is contained in:
Yunxiao Xu
2026-02-12 01:33:32 -08:00
parent 2545f6df13
commit dcfc090f1c
5 changed files with 52 additions and 165 deletions

View File

@@ -5,6 +5,7 @@ import { LoginForm } from "./components/auth/LoginForm"
import { RegisterForm } from "./components/auth/RegisterForm"
import { AuthCallback } from "./components/auth/AuthCallback"
import { AuthService, type UserResponse } from "./services/auth"
import { registerUnauthorizedCallback } from "./services/api"
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false)
@@ -13,6 +14,12 @@ function App() {
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
// Register callback to handle session expiration from anywhere in the app
registerUnauthorizedCallback(() => {
setIsAuthenticated(false)
setUser(null)
})
const initAuth = async () => {
try {
const userData = await AuthService.getMe()

View File

@@ -7,9 +7,18 @@ export function AuthCallback() {
useEffect(() => {
const verifyAuth = async () => {
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get("code")
try {
// The cookie should have been set by the backend redirect
await AuthService.getMe()
if (code) {
// If we have a code, exchange it for a cookie
await AuthService.exchangeOIDCCode(code)
} else {
// If no code, just verify existing cookie (backend-driven redirect)
await AuthService.getMe()
}
// Success - go to home. We use window.location.href to ensure a clean reload of App state
window.location.href = "/"
} catch (err) {

View File

@@ -7,15 +7,26 @@ const api = axios.create({
withCredentials: true, // Crucial for HttpOnly cookies
})
// Optional callback for unauthorized errors
let onUnauthorized: (() => void) | null = null
export const registerUnauthorizedCallback = (callback: () => void) => {
onUnauthorized = callback
}
// Add a response interceptor to handle 401s
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Unauthorized - session likely expired
// We can't use useNavigate here as it's not a React component
// But we can redirect to home which will trigger the login view in App.tsx
window.location.href = "/"
// Only handle if it's not an auth endpoint
// This prevents loops during bootstrap and allows login form to show errors
const isAuthEndpoint = /^\/auth\//.test(error.config?.url)
if (error.response?.status === 401 && !isAuthEndpoint) {
// Unauthorized - session likely expired on a protected data route
if (onUnauthorized) {
onUnauthorized()
}
}
return Promise.reject(error)
}

View File

@@ -28,6 +28,11 @@ export const AuthService = {
}
},
async exchangeOIDCCode(code: string): Promise<AuthResponse> {
const response = await api.get<AuthResponse>(`/auth/oidc/callback?code=${code}`)
return response.data
},
async register(email: string, password: string): Promise<UserResponse> {
const response = await api.post<UserResponse>("/auth/register", {
email,