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:
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user