feat: implement httpOnly cookie for JWT authentication and update related API calls

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-28 14:36:06 -04:00
parent 2eb887c486
commit 567fa66428
27 changed files with 207 additions and 123 deletions
@@ -14,9 +14,7 @@ function CallbackHandler() {
useEffect(() => {
const token = searchParams.get("token");
if (token) {
// Temporarily store token so getMe can use it
localStorage.setItem('experience_token', token);
// El token JWT viene como cookie httpOnly del backend.
lmsApi.getMe()
.then((user) => {
login(user, token);
@@ -24,7 +22,6 @@ function CallbackHandler() {
})
.catch((err) => {
console.error("SSO Error:", err);
localStorage.removeItem('experience_token');
router.push("/auth/login?error=sso_failed");
});
} else {
+3 -6
View File
@@ -22,16 +22,13 @@ function LtiLaunchContent() {
}
try {
// 1. Temporarily save token so api client can use it for getMe
localStorage.setItem("experience_token", token);
// 2. Fetch user details
// Fetch user details - la cookie httpOnly se envía automáticamente
const user = await lmsApi.getMe();
// 3. Initialize session in AuthContext
// Initialize session in AuthContext
login(user, token);
// 4. Redirect to final destination
// Redirect to final destination
router.replace(target);
} catch (err: any) {
console.error("LTI Launch Error:", err);
@@ -132,7 +132,7 @@ export default function MediaPlayer({ id, lessonId, title, url, media_type, conf
return rawUrl;
};
const getToken = () => typeof window !== 'undefined' ? localStorage.getItem('experience_token') : null;
const getToken = () => null; // Token se envía automáticamente via httpOnly cookie
const selectedOrgId = typeof window !== 'undefined' ? localStorage.getItem('experience_selected_org_id') : null;
// Construct VTT URLs with auth if possible, or assume public/handled by backend
+4 -5
View File
@@ -20,10 +20,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const savedUser = localStorage.getItem('experience_user');
const savedToken = localStorage.getItem('experience_token');
if (savedUser && savedToken) {
if (savedUser) {
setUser(JSON.parse(savedUser));
setToken(savedToken);
}
setLoading(false);
}, []);
@@ -31,15 +29,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const login = (newUser: User, newToken: string) => {
setUser(newUser);
setToken(newToken);
// El token JWT se guarda en httpOnly cookie por el backend.
localStorage.setItem('experience_user', JSON.stringify(newUser));
localStorage.setItem('experience_token', newToken);
};
const logout = () => {
setUser(null);
setToken(null);
localStorage.removeItem('experience_user');
localStorage.removeItem('experience_token');
// Borrar la httpOnly cookie desde el backend
fetch('/lms-api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {});
};
return (
+6 -4
View File
@@ -614,7 +614,7 @@ export const getToken = () => {
return previewToken;
}
return sessionStorage.getItem('preview_token') || localStorage.getItem('experience_token');
return sessionStorage.getItem('preview_token') || null;
};
const OFFLINE_QUEUE_KEY = 'experience_offline_mutation_queue_v1';
@@ -854,7 +854,7 @@ const apiFetch = async (url: string, options: RequestInit = {}, isCMS: boolean =
const baseUrl = isCMS ? getCmsApiUrl() : getLmsApiUrl();
const headers = buildApiHeaders(options);
const response = await fetch(`${baseUrl}${url}`, { ...options, headers });
const response = await fetch(`${baseUrl}${url}`, { ...options, headers, credentials: 'include' });
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(error.message || 'An error occurred');
@@ -1091,7 +1091,8 @@ export const lmsApi = {
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
},
body: formData
body: formData,
credentials: 'include'
}).then(res => res.json());
},
@@ -1163,7 +1164,8 @@ export const lmsApi = {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
// Don't set Content-Type for FormData - browser sets it with boundary
},
body: formData
body: formData,
credentials: 'include'
}).then(async res => {
if (!res.ok) {
const err = await res.json().catch(() => ({ message: 'Audio evaluation failed' }));