feat: implement httpOnly cookie for JWT authentication and update related API calls
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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' }));
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function GlobalAiControl() {
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-4">
|
||||
Token en localStorage: {localStorage.getItem('studio_token') ? '✓ Existe' : '✗ No existe'}
|
||||
Por favor verifica que tu sesión esté activa.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,9 +38,7 @@ export default function AdminDashboard() {
|
||||
cmsApi.getOrganization(),
|
||||
cmsApi.getAllUsers(),
|
||||
fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/admin/token-usage`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('studio_token')}`,
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
@@ -54,24 +54,14 @@ export default function AdminTokenTracking() {
|
||||
|
||||
const loadTokenUsage = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('studio_token');
|
||||
|
||||
if (!token) {
|
||||
console.error('[TokenUsage] No authentication token found!');
|
||||
alert('No authentication token found. Please login again.');
|
||||
window.location.href = '/auth/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/admin/token-usage`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
console.error('[TokenUsage] Unauthorized - Token may be expired');
|
||||
alert('Session expired. Please login again.');
|
||||
window.location.href = '/auth/login';
|
||||
return;
|
||||
@@ -88,11 +78,7 @@ export default function AdminTokenTracking() {
|
||||
try {
|
||||
const limitResp = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/admin/users/${user.user_id}/token-limit/check`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('studio_token')}`,
|
||||
},
|
||||
}
|
||||
{ credentials: 'include' }
|
||||
);
|
||||
if (limitResp.ok) {
|
||||
const limitData = await limitResp.json();
|
||||
@@ -127,13 +113,13 @@ export default function AdminTokenTracking() {
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('studio_token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
monthly_token_limit: editValue,
|
||||
token_limit_reset_day: 1,
|
||||
}),
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -141,11 +127,7 @@ export default function AdminTokenTracking() {
|
||||
// Reload limits
|
||||
const limitResp = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/admin/users/${userId}/token-limit/check`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('studio_token')}`,
|
||||
},
|
||||
}
|
||||
{ credentials: 'include' }
|
||||
);
|
||||
if (limitResp.ok) {
|
||||
const limitData = await limitResp.json();
|
||||
|
||||
@@ -52,11 +52,7 @@ export default function UsersPage() {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/admin/users/${user.id}/token-limit/check`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('studio_token')}`,
|
||||
},
|
||||
}
|
||||
{ credentials: 'include' }
|
||||
);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -14,9 +14,8 @@ function CallbackHandler() {
|
||||
useEffect(() => {
|
||||
const token = searchParams.get("token");
|
||||
if (token) {
|
||||
// Temporarily store token so getMe can use it
|
||||
localStorage.setItem('studio_token', token);
|
||||
|
||||
// El token JWT viene como cookie httpOnly del backend.
|
||||
// Lo pasamos al AuthContext para el estado en memoria.
|
||||
cmsApi.getMe()
|
||||
.then((user) => {
|
||||
login(user, token);
|
||||
@@ -24,7 +23,6 @@ function CallbackHandler() {
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("SSO Error:", err);
|
||||
localStorage.removeItem('studio_token');
|
||||
router.push("/auth/login?error=sso_failed");
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -85,13 +85,12 @@ export default function GradebookPage() {
|
||||
|
||||
const exportCSV = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('studio_token');
|
||||
const selectedOrgId = localStorage.getItem('studio_selected_org_id');
|
||||
const res = await fetch(lmsApi.exportGradesUrl(id), {
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...(selectedOrgId ? { 'X-Organization-Id': selectedOrgId } : {})
|
||||
}
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error('Export failed');
|
||||
const blob = await res.blob();
|
||||
|
||||
@@ -20,7 +20,8 @@ function DeepLinkingPickerContent() {
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
localStorage.setItem("studio_token", token);
|
||||
// El token JWT llega como cookie httpOnly del backend SSO/LTI.
|
||||
// No se persiste en localStorage.
|
||||
}
|
||||
loadCourses();
|
||||
}, [token]);
|
||||
|
||||
@@ -73,10 +73,9 @@ export default function QuestionBankEditor({ question, onSuccess, onCancel }: Qu
|
||||
|
||||
const uploadResponse = await fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/assets/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
headers: {},
|
||||
body: audioFormData,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (uploadResponse.ok) {
|
||||
@@ -166,19 +165,17 @@ export default function QuestionBankEditor({ question, onSuccess, onCancel }: Qu
|
||||
try {
|
||||
setGeneratingAI(true);
|
||||
|
||||
// AI generation con el nuevo endpoint de question bank
|
||||
const token = localStorage.getItem('studio_token');
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/question-bank/ai-generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
question_text: formData.question_text,
|
||||
difficulty: formData.difficulty,
|
||||
skill: randomSkill,
|
||||
}),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -438,27 +438,20 @@ export default function TestTemplateForm({ templateId, onSuccess, onCancel }: Te
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('studio_token');
|
||||
if (!token) {
|
||||
alert('No hay sesión activa. Por favor inicia sesión nuevamente.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setGeneratingAI(true);
|
||||
|
||||
// Usar el endpoint RAG de generación de preguntas desde banco MySQL
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_API_URL || 'http://localhost:3001'}/test-templates/generate-with-rag`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
topic: aiContext,
|
||||
num_questions: aiQuestionCount,
|
||||
question_type: aiQuestionType,
|
||||
}),
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -39,8 +39,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setUser(newUser);
|
||||
setToken(newToken);
|
||||
setSelectedOrgId(newUser.organization_id || null);
|
||||
// El token JWT se guarda en httpOnly cookie por el backend.
|
||||
// Solo se persiste el usuario (sin datos sensibles de auth) y el orgId.
|
||||
localStorage.setItem('studio_user', JSON.stringify(newUser));
|
||||
localStorage.setItem('studio_token', newToken);
|
||||
if (newUser.organization_id) {
|
||||
localStorage.setItem('studio_selected_org_id', newUser.organization_id);
|
||||
}
|
||||
@@ -51,8 +52,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setToken(null);
|
||||
setSelectedOrgId(null);
|
||||
localStorage.removeItem('studio_user');
|
||||
localStorage.removeItem('studio_token');
|
||||
localStorage.removeItem('studio_selected_org_id');
|
||||
// Borrar la httpOnly cookie desde el backend
|
||||
fetch('/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {});
|
||||
};
|
||||
|
||||
const setOrganizationId = (id: string | null) => {
|
||||
|
||||
@@ -979,7 +979,7 @@ export const apiFetch = (url: string, options: ApiFetchOptions = {}, isLms: bool
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(finalUrl, { ...options, headers }).then(async res => {
|
||||
return fetch(finalUrl, { ...options, headers, credentials: 'include' }).then(async res => {
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
@@ -1261,6 +1261,7 @@ export const cmsApi = {
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `${API_BASE_URL}/api/assets/import-zip`);
|
||||
xhr.withCredentials = true;
|
||||
|
||||
const token = getToken();
|
||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
@@ -1327,6 +1328,7 @@ export const cmsApi = {
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `${API_BASE_URL}/api/assets/upload`);
|
||||
xhr.withCredentials = true;
|
||||
|
||||
const token = getToken();
|
||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
|
||||
Reference in New Issue
Block a user