refactor: migrate main.py to modular routers and add project roadmap
- Migrated monolithic main.py to feature-scoped routers in app/routers/ - Added GEMINI.md for project context and AI instructional guidelines - Updated README.md with a comprehensive modernization plan (SQL migration, robust scraping DSL, frontend modernization) - Improved authentication with cookie support and modular JS - Updated test suite and documentation
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
|
||||
// Set up global window object for jsdom
|
||||
global.window = global.window || {};
|
||||
|
||||
// Define skeleton functions for testing (same as in auth-api.js)
|
||||
const API_BASE = '/api';
|
||||
|
||||
async function login(username, password) {
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
|
||||
async function register(username, password, email = null, full_name = null) {
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
|
||||
async function getMe(token) {
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
|
||||
// Set up window object
|
||||
window.authApi = {
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getMe,
|
||||
};
|
||||
|
||||
describe('authApi', () => {
|
||||
describe('login function', () => {
|
||||
it('should be a function', () => {
|
||||
expect(typeof window.authApi.login).toBe('function');
|
||||
});
|
||||
|
||||
it('should return a Promise', () => {
|
||||
const result = window.authApi.login('test', 'test');
|
||||
expect(result).toBeInstanceOf(Promise);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register function', () => {
|
||||
it('should be a function', () => {
|
||||
expect(typeof window.authApi.register).toBe('function');
|
||||
});
|
||||
|
||||
it('should return a Promise', () => {
|
||||
const result = window.authApi.register('testuser', 'password123', null, null);
|
||||
expect(result).toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
it('should handle optional parameters', async () => {
|
||||
try {
|
||||
await window.authApi.register('test', 'password');
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Not implemented yet');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout function', () => {
|
||||
it('should be a function', () => {
|
||||
expect(typeof window.authApi.logout).toBe('function');
|
||||
});
|
||||
|
||||
it('should return a Promise', () => {
|
||||
const result = window.authApi.logout();
|
||||
expect(result).toBeInstanceOf(Promise);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMe function', () => {
|
||||
it('should be a function', () => {
|
||||
expect(typeof window.authApi.getMe).toBe('function');
|
||||
});
|
||||
|
||||
it('should return a Promise', () => {
|
||||
const result = window.authApi.getMe('fake-token');
|
||||
expect(result).toBeInstanceOf(Promise);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
// Mock DOM elements for displayError tests
|
||||
const mockDocument = () => {
|
||||
const elements = {};
|
||||
global.document = {
|
||||
getElementById: (id) => elements[id] || null,
|
||||
};
|
||||
beforeEach(() => {
|
||||
elements.authError = {
|
||||
textContent: '',
|
||||
classList: {
|
||||
add: () => {},
|
||||
remove: () => {}
|
||||
}
|
||||
};
|
||||
elements.authSuccess = {
|
||||
textContent: '',
|
||||
classList: {
|
||||
add: () => {},
|
||||
remove: () => {}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
describe('safeJsonParse', () => {
|
||||
// Import the function - we'll need to make it work with Vitest
|
||||
// For now, we'll define it inline for testing
|
||||
const safeJsonParse = (text, fallback = null) => {
|
||||
try {
|
||||
if (text === undefined || text === null || text === '') {
|
||||
return fallback;
|
||||
}
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
it('should parse valid JSON string', () => {
|
||||
const result = safeJsonParse('{"key":"value"}');
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('should return fallback for invalid JSON', () => {
|
||||
const result = safeJsonParse('invalid json');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return custom fallback when provided', () => {
|
||||
const result = safeJsonParse('invalid', 'custom fallback');
|
||||
expect(result).toBe('custom fallback');
|
||||
});
|
||||
|
||||
it('should return fallback for undefined input', () => {
|
||||
const result = safeJsonParse(undefined);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return fallback for null input', () => {
|
||||
const result = safeJsonParse(null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return fallback for empty string', () => {
|
||||
const result = safeJsonParse('');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse valid JSON array', () => {
|
||||
const result = safeJsonParse('[1, 2, 3]');
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should parse nested JSON', () => {
|
||||
const result = safeJsonParse('{"user":{"name":"John","age":30}}');
|
||||
expect(result).toEqual({ user: { name: 'John', age: 30 } });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
// Smoke test to verify Vitest setup
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('smoke', () => {
|
||||
it('works', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
+27
-15
@@ -62,16 +62,13 @@ async function searchAnimeDetails(query, malId = null) {
|
||||
const providersData = await getProvidersInfo();
|
||||
|
||||
// Build results HTML
|
||||
const streamingParts = [
|
||||
`<div class="streaming-results-header">
|
||||
<h3>🎬 Résultats de streaming</h3>
|
||||
</div>
|
||||
<div class="search-results" style="margin-top: 20px;">`
|
||||
];
|
||||
const streamingParts = [];
|
||||
let hasResults = false;
|
||||
|
||||
// Display results from each provider - render all cards in parallel
|
||||
for (const [providerId, results] of Object.entries(streamingData.value.results)) {
|
||||
if (results && results.length > 0) {
|
||||
hasResults = true;
|
||||
const provider = providersData.anime_providers[providerId];
|
||||
|
||||
// Render all cards for this provider
|
||||
@@ -81,8 +78,17 @@ async function searchAnimeDetails(query, malId = null) {
|
||||
}
|
||||
}
|
||||
|
||||
streamingParts.push('</div>');
|
||||
streamingHtml = streamingParts.join('');
|
||||
// Only add header and wrapper if we have results
|
||||
if (hasResults) {
|
||||
streamingParts.unshift(
|
||||
`<div class="streaming-results-header">
|
||||
<h3>🎬 Résultats de streaming</h3>
|
||||
</div>
|
||||
<div class="search-results" style="margin-top: 20px;">`
|
||||
);
|
||||
streamingParts.push('</div>');
|
||||
streamingHtml = streamingParts.join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Display results
|
||||
@@ -150,16 +156,13 @@ async function getProviderSearchResults(query) {
|
||||
}
|
||||
|
||||
// Build results HTML
|
||||
const htmlParts = [
|
||||
`<div class="streaming-results-header">
|
||||
<h3>🎬 Résultats de streaming</h3>
|
||||
</div>
|
||||
<div class="search-results" style="margin-top: 20px;">`
|
||||
];
|
||||
const htmlParts = [];
|
||||
let hasResults = false;
|
||||
|
||||
// Display results from each provider
|
||||
for (const [providerId, results] of Object.entries(data.results)) {
|
||||
if (results && results.length > 0) {
|
||||
hasResults = true;
|
||||
const providersData = await getProvidersInfo();
|
||||
const provider = providersData.anime_providers[providerId];
|
||||
|
||||
@@ -170,7 +173,16 @@ async function getProviderSearchResults(query) {
|
||||
}
|
||||
}
|
||||
|
||||
htmlParts.push('</div>');
|
||||
// Only add header and wrapper if we have results
|
||||
if (hasResults) {
|
||||
htmlParts.unshift(
|
||||
`<div class="streaming-results-header">
|
||||
<h3>🎬 Résultats de streaming</h3>
|
||||
</div>
|
||||
<div class="search-results" style="margin-top: 20px;">`
|
||||
);
|
||||
htmlParts.push('</div>');
|
||||
}
|
||||
|
||||
return htmlParts.join('');
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Auth API client module
|
||||
* Following the pattern from static/js/watchlist.js (global exports)
|
||||
*/
|
||||
|
||||
// Use the global API_BASE from auth-utils.js, fallback to /api
|
||||
const AUTH_API_BASE = typeof window.API_BASE !== 'undefined' ? window.API_BASE : '/api';
|
||||
|
||||
async function login(username, password) {
|
||||
try {
|
||||
const response = await fetch(`${AUTH_API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
const data = window.safeJsonParse(text, {});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.detail || 'Erreur de connexion';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
throw new Error('Erreur de connexion au serveur');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function register(username, password, email = null, full_name = null) {
|
||||
try {
|
||||
const response = await fetch(`${AUTH_API_BASE}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, email, full_name }),
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
const data = window.safeJsonParse(text, {});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.detail || 'Erreur lors de l\'inscription';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
throw new Error('Erreur de connexion au serveur');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const response = await fetch(`${AUTH_API_BASE}/auth/logout`, { method: 'POST' });
|
||||
const text = await response.text();
|
||||
const data = window.safeJsonParse(text, { status: 'success' });
|
||||
return data;
|
||||
} catch (error) {
|
||||
return { status: 'success', message: 'Logged out locally' };
|
||||
}
|
||||
}
|
||||
|
||||
async function getMe(token) {
|
||||
try {
|
||||
const response = await fetch(`${AUTH_API_BASE}/auth/me`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
const data = window.safeJsonParse(text, {});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.detail || 'Erreur de connexion';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
throw new Error('Erreur de connexion au serveur');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
window.authApi = {
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getMe,
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Auth UI handlers module
|
||||
* Following the pattern from static/js/watchlist.js (global exports)
|
||||
*/
|
||||
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('loginUsername').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
const button = document.getElementById('loginSubmit');
|
||||
|
||||
if (!button) {
|
||||
console.error('Login button not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = button.textContent;
|
||||
setLoading('loginSubmit', true, { loadingText: 'Connexion...', originalText });
|
||||
|
||||
try {
|
||||
const data = await window.authApi.login(username, password);
|
||||
|
||||
if (data.access_token) {
|
||||
window.setToken(data.access_token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
window.displaySuccess('authSuccess', 'Connexion réussie! Redirection...');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/web';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
window.displayError('authError', error.message || 'Erreur lors de la connexion');
|
||||
} finally {
|
||||
setLoading('loginSubmit', false, { originalText });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('registerUsername').value;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const passwordConfirm = document.getElementById('registerPasswordConfirm').value;
|
||||
const email = document.getElementById('registerEmail').value || null;
|
||||
const full_name = document.getElementById('registerFullName').value || null;
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
window.displayError('authError', 'Les mots de passe ne correspondent pas');
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.getElementById('registerSubmit');
|
||||
if (!button) {
|
||||
console.error('Register button not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = button.textContent;
|
||||
setLoading('registerSubmit', true, { loadingText: 'Inscription...', originalText });
|
||||
|
||||
try {
|
||||
const data = await window.authApi.register(username, password, email, full_name);
|
||||
|
||||
window.displaySuccess('authSuccess', 'Inscription réussie! Vous pouvez maintenant vous connecter.');
|
||||
|
||||
setTimeout(() => {
|
||||
window.authUi.switchTab('login');
|
||||
document.getElementById('loginUsername').value = username;
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
window.displayError('authError', error.message || 'Erreur lors de l\'inscription');
|
||||
} finally {
|
||||
setLoading('registerSubmit', false, { originalText });
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(buttonId, isLoading, options = {}) {
|
||||
const button = document.getElementById(buttonId);
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultLoadingText = '...';
|
||||
const loadingText = options.loadingText || defaultLoadingText;
|
||||
|
||||
if (isLoading) {
|
||||
const origText = options.originalText || button.textContent;
|
||||
button.dataset.originalText = origText;
|
||||
button.textContent = loadingText;
|
||||
button.disabled = true;
|
||||
} else {
|
||||
const origText = button.dataset.originalText || options.originalText || 'Se connecter';
|
||||
button.textContent = origText;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetLoading(buttonId, originalText) {
|
||||
setLoading(buttonId, false, { originalText });
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
const tabs = document.querySelectorAll('.auth-tab');
|
||||
const forms = document.querySelectorAll('.auth-form');
|
||||
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
forms.forEach(f => f.classList.remove('active'));
|
||||
|
||||
if (tab === 'login') {
|
||||
tabs[0].classList.add('active');
|
||||
document.getElementById('loginForm').classList.add('active');
|
||||
} else {
|
||||
tabs[1].classList.add('active');
|
||||
document.getElementById('registerForm').classList.add('active');
|
||||
}
|
||||
|
||||
document.getElementById('authError').classList.remove('show');
|
||||
document.getElementById('authSuccess').classList.remove('show');
|
||||
}
|
||||
|
||||
window.authUi = {
|
||||
handleLogin,
|
||||
handleRegister,
|
||||
setLoading,
|
||||
resetLoading,
|
||||
switchTab,
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Auth utilities - safe JSON parsing and error display
|
||||
* Following the pattern from static/js/watchlist.js (global exports)
|
||||
*/
|
||||
|
||||
// API base URL - use relative path for same-origin
|
||||
const API_BASE = '/api';
|
||||
|
||||
/**
|
||||
* Safely parse JSON string with fallback
|
||||
* @param {string} text - The JSON string to parse
|
||||
* @param {*} fallback - The fallback value if parsing fails (default: null)
|
||||
* @returns {*} Parsed object or fallback value
|
||||
*/
|
||||
function safeJsonParse(text, fallback = null) {
|
||||
try {
|
||||
if (text === undefined || text === null || text === '') {
|
||||
return fallback;
|
||||
}
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
console.error('JSON parse error:', error.message);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display error message in the specified element
|
||||
* Handles string, object, and array errors properly
|
||||
* @param {string} elementId - The ID of the element to display error in
|
||||
* @param {*} error - The error (string, object, or array)
|
||||
* @param {string} defaultMessage - Default message if error is invalid
|
||||
*/
|
||||
function displayError(elementId, error, defaultMessage = 'Une erreur est survenue') {
|
||||
const errorDiv = document.getElementById(elementId);
|
||||
if (!errorDiv) {
|
||||
console.error('Error element not found:', elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
let message = defaultMessage;
|
||||
|
||||
if (error === null || error === undefined) {
|
||||
message = defaultMessage;
|
||||
} else if (typeof error === 'string') {
|
||||
message = error;
|
||||
} else if (typeof error === 'object') {
|
||||
// Handle array errors
|
||||
if (Array.isArray(error)) {
|
||||
message = error.join('\n');
|
||||
}
|
||||
// Handle FastAPI HTTPException detail (can be string or object)
|
||||
else if (error.detail) {
|
||||
if (typeof error.detail === 'string') {
|
||||
message = error.detail;
|
||||
} else if (typeof error.detail === 'object' && error.detail.msg) {
|
||||
message = error.detail.msg;
|
||||
} else {
|
||||
// Stringify the object to avoid "[object Object]"
|
||||
message = JSON.stringify(error.detail);
|
||||
}
|
||||
}
|
||||
// Handle generic object
|
||||
else {
|
||||
message = JSON.stringify(error);
|
||||
}
|
||||
}
|
||||
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.classList.add('show');
|
||||
|
||||
// Hide success message if visible
|
||||
const successDiv = document.getElementById(elementId.replace('Error', 'Success'));
|
||||
if (successDiv) {
|
||||
successDiv.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display success message in the specified element
|
||||
* @param {string} elementId - The ID of the element to display success in
|
||||
* @param {string} message - The success message
|
||||
*/
|
||||
function displaySuccess(elementId, message) {
|
||||
const successDiv = document.getElementById(elementId);
|
||||
if (!successDiv) {
|
||||
console.error('Success element not found:', elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
successDiv.textContent = message;
|
||||
successDiv.classList.add('show');
|
||||
|
||||
// Hide error message if visible
|
||||
const errorDiv = document.getElementById(elementId.replace('Success', 'Error'));
|
||||
if (errorDiv) {
|
||||
errorDiv.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
// Export globally (following watchlist.js pattern)
|
||||
window.safeJsonParse = safeJsonParse;
|
||||
window.displayError = displayError;
|
||||
window.displaySuccess = displaySuccess;
|
||||
window.API_BASE = API_BASE;
|
||||
+73
-7
@@ -5,9 +5,74 @@
|
||||
// Use relative path for API
|
||||
const AUTH_API_BASE = '/api';
|
||||
|
||||
const COOKIE_NAME = 'auth_token';
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
||||
|
||||
/**
|
||||
* Set token in HTTP-only cookie (via server)
|
||||
* Since we can't set HttpOnly cookies from JavaScript, we store in localStorage
|
||||
* but also try to set a non-HttpOnly cookie for compatibility
|
||||
*/
|
||||
function setToken(token) {
|
||||
// Store in localStorage as primary (for backward compatibility)
|
||||
localStorage.setItem('auth_token', token);
|
||||
|
||||
// Also try to set cookie (non-HttpOnly, but better than nothing)
|
||||
// Note: HttpOnly must be set by server, this is a fallback
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + COOKIE_MAX_AGE * 1000);
|
||||
document.cookie = `${COOKIE_NAME}=${token};expires=${expires.toUTCString()};path=/;SameSite=Strict`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token from cookie first, then fallback to localStorage
|
||||
*/
|
||||
function getToken() {
|
||||
// Try cookie first
|
||||
const cookieToken = getTokenFromCookie();
|
||||
if (cookieToken) {
|
||||
return cookieToken;
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
return localStorage.getItem('auth_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token from cookie
|
||||
*/
|
||||
function getTokenFromCookie() {
|
||||
const name = COOKIE_NAME + '=';
|
||||
const decodedCookie = decodeURIComponent(document.cookie);
|
||||
const cookieArray = decodedCookie.split(';');
|
||||
|
||||
for (let i = 0; i < cookieArray.length; i++) {
|
||||
let cookie = cookieArray[i];
|
||||
while (cookie.charAt(0) === ' ') {
|
||||
cookie = cookie.substring(1);
|
||||
}
|
||||
if (cookie.indexOf(name) === 0) {
|
||||
return cookie.substring(name.length, cookie.length);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove token from cookie and localStorage
|
||||
*/
|
||||
function removeToken() {
|
||||
// Remove from localStorage
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user');
|
||||
|
||||
// Remove cookie
|
||||
document.cookie = `${COOKIE_NAME}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`;
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
async function checkAuth() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
const userStr = localStorage.getItem('user');
|
||||
|
||||
if (!token) {
|
||||
@@ -31,8 +96,7 @@ async function checkAuth() {
|
||||
return true;
|
||||
} else {
|
||||
// Token invalid, remove it and redirect
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user');
|
||||
removeToken();
|
||||
redirectToLogin();
|
||||
return false;
|
||||
}
|
||||
@@ -97,9 +161,8 @@ async function handleLogout() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove token from localStorage
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user');
|
||||
// Remove token from localStorage and cookie
|
||||
removeToken();
|
||||
|
||||
// Call logout endpoint
|
||||
try {
|
||||
@@ -114,7 +177,7 @@ async function handleLogout() {
|
||||
|
||||
// Add authorization header to all fetch requests
|
||||
function addAuthHeader(options = {}) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
options.headers = options.headers || {};
|
||||
options.headers['Authorization'] = `Bearer ${token}`;
|
||||
@@ -135,6 +198,9 @@ window.showLoginPrompt = showLoginPrompt;
|
||||
window.handleLogout = handleLogout;
|
||||
window.authFetch = authFetch;
|
||||
window.addAuthHeader = addAuthHeader;
|
||||
window.getToken = getToken;
|
||||
window.setToken = setToken;
|
||||
window.removeToken = removeToken;
|
||||
|
||||
// Check authentication on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
@@ -237,7 +237,7 @@ async function handleAddToWatchlist(animeUrl, providerId) {
|
||||
|
||||
// Trigger download of all episodes immediately
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
const downloadResponse = await fetch(`${API_BASE}/watchlist/${result.id}/download-all`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
+12
-12
@@ -7,7 +7,7 @@
|
||||
* Get user's watchlist
|
||||
*/
|
||||
async function getWatchlist(status = null) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -34,7 +34,7 @@ async function getWatchlist(status = null) {
|
||||
* Add anime to watchlist
|
||||
*/
|
||||
async function addToWatchlist(animeData) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -60,7 +60,7 @@ async function addToWatchlist(animeData) {
|
||||
* Update watchlist item
|
||||
*/
|
||||
async function updateWatchlistItem(itemId, updateData) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -85,7 +85,7 @@ async function updateWatchlistItem(itemId, updateData) {
|
||||
* Delete from watchlist
|
||||
*/
|
||||
async function deleteFromWatchlist(itemId) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -122,7 +122,7 @@ async function resumeWatchlistItem(itemId) {
|
||||
* Check specific anime for new episodes
|
||||
*/
|
||||
async function checkWatchlistItem(itemId) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -145,7 +145,7 @@ async function checkWatchlistItem(itemId) {
|
||||
* Check all watchlist items
|
||||
*/
|
||||
async function checkAllWatchlistItems() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -168,7 +168,7 @@ async function checkAllWatchlistItems() {
|
||||
* Get watchlist settings
|
||||
*/
|
||||
async function getWatchlistSettings() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -190,7 +190,7 @@ async function getWatchlistSettings() {
|
||||
* Update watchlist settings
|
||||
*/
|
||||
async function updateWatchlistSettings(settings) {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -215,7 +215,7 @@ async function updateWatchlistSettings(settings) {
|
||||
* Get watchlist statistics
|
||||
*/
|
||||
async function getWatchlistStats() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -237,7 +237,7 @@ async function getWatchlistStats() {
|
||||
* Get scheduler status
|
||||
*/
|
||||
async function getSchedulerStatus() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -259,7 +259,7 @@ async function getSchedulerStatus() {
|
||||
* Start scheduler
|
||||
*/
|
||||
async function startScheduler() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
@@ -282,7 +282,7 @@ async function startScheduler() {
|
||||
* Stop scheduler
|
||||
*/
|
||||
async function stopScheduler() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user