Add /api/auth/status endpoint for JavaScript compatibility

This commit is contained in:
Lukas Pupka-Lipinski 2025-10-05 23:42:59 +02:00
parent 2c8c9a788c
commit e3b752a2a7
2 changed files with 122 additions and 100 deletions

View File

@ -343,6 +343,31 @@ async def logout(current_user: Dict = Depends(get_current_user)) -> Dict[str, An
"message": "Logged out successfully. Please remove the token from client storage." "message": "Logged out successfully. Please remove the token from client storage."
} }
@app.get("/api/auth/status", response_model=Dict[str, Any], tags=["Authentication"])
async def auth_status(request: Request) -> Dict[str, Any]:
"""
Check authentication status and configuration.
This endpoint checks if master password is configured and if user is authenticated.
"""
has_master_password = bool(settings.master_password_hash or settings.master_password)
# Check if user has valid token
authenticated = False
try:
auth_header = request.headers.get("authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ")[1]
payload = verify_jwt_token(token)
authenticated = payload is not None
except Exception:
authenticated = False
return {
"has_master_password": has_master_password,
"authenticated": authenticated
}
# Health check endpoint # Health check endpoint
@app.get("/health", response_model=HealthResponse, tags=["System"]) @app.get("/health", response_model=HealthResponse, tags=["System"])
async def health_check() -> HealthResponse: async def health_check() -> HealthResponse:

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="light"> <html lang="en" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -15,7 +16,7 @@
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%); background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%);
padding: 1rem; padding: 1rem;
} }
.setup-card { .setup-card {
background: var(--color-surface); background: var(--color-surface);
border-radius: 16px; border-radius: 16px;
@ -25,50 +26,50 @@
max-width: 500px; max-width: 500px;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
} }
.setup-header { .setup-header {
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.setup-header .logo { .setup-header .logo {
font-size: 3rem; font-size: 3rem;
color: var(--color-primary); color: var(--color-primary);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.setup-header h1 { .setup-header h1 {
margin: 0; margin: 0;
color: var(--color-text); color: var(--color-text);
font-size: 1.8rem; font-size: 1.8rem;
font-weight: 600; font-weight: 600;
} }
.setup-header p { .setup-header p {
margin: 1rem 0 0 0; margin: 1rem 0 0 0;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 1rem; font-size: 1rem;
line-height: 1.5; line-height: 1.5;
} }
.setup-form { .setup-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
} }
.form-label { .form-label {
font-weight: 500; font-weight: 500;
color: var(--color-text); color: var(--color-text);
font-size: 0.9rem; font-size: 0.9rem;
} }
.form-input { .form-input {
width: 100%; width: 100%;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
@ -80,21 +81,21 @@
transition: all 0.2s ease; transition: all 0.2s ease;
box-sizing: border-box; box-sizing: border-box;
} }
.form-input:focus { .form-input:focus {
outline: none; outline: none;
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1); box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);
} }
.password-input-group { .password-input-group {
position: relative; position: relative;
} }
.password-input { .password-input {
padding-right: 3rem; padding-right: 3rem;
} }
.password-toggle { .password-toggle {
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;
@ -108,17 +109,17 @@
border-radius: 4px; border-radius: 4px;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.password-toggle:hover { .password-toggle:hover {
color: var(--color-primary); color: var(--color-primary);
} }
.password-strength { .password-strength {
display: flex; display: flex;
gap: 0.25rem; gap: 0.25rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.strength-bar { .strength-bar {
flex: 1; flex: 1;
height: 4px; height: 4px;
@ -126,24 +127,35 @@
border-radius: 2px; border-radius: 2px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
.strength-bar.active.weak { background: var(--color-error); } .strength-bar.active.weak {
.strength-bar.active.fair { background: var(--color-warning); } background: var(--color-error);
.strength-bar.active.good { background: var(--color-info); } }
.strength-bar.active.strong { background: var(--color-success); }
.strength-bar.active.fair {
background: var(--color-warning);
}
.strength-bar.active.good {
background: var(--color-info);
}
.strength-bar.active.strong {
background: var(--color-success);
}
.strength-text { .strength-text {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.form-help { .form-help {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
line-height: 1.4; line-height: 1.4;
} }
.setup-button { .setup-button {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
@ -160,20 +172,20 @@
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 0.5rem;
} }
.setup-button:hover:not(:disabled) { .setup-button:hover:not(:disabled) {
background: var(--color-primary-dark); background: var(--color-primary-dark);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3); box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3);
} }
.setup-button:disabled { .setup-button:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
box-shadow: none; box-shadow: none;
} }
.error-message { .error-message {
background: var(--color-error-light); background: var(--color-error-light);
color: var(--color-error); color: var(--color-error);
@ -182,7 +194,7 @@
border: 1px solid var(--color-error); border: 1px solid var(--color-error);
font-size: 0.9rem; font-size: 0.9rem;
} }
.success-message { .success-message {
background: var(--color-success-light); background: var(--color-success-light);
color: var(--color-success); color: var(--color-success);
@ -191,7 +203,7 @@
border: 1px solid var(--color-success); border: 1px solid var(--color-success);
font-size: 0.9rem; font-size: 0.9rem;
} }
.security-tips { .security-tips {
margin-top: 1.5rem; margin-top: 1.5rem;
padding: 1rem; padding: 1rem;
@ -201,19 +213,19 @@
font-size: 0.85rem; font-size: 0.85rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
.security-tips h4 { .security-tips h4 {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
color: var(--color-info); color: var(--color-info);
font-size: 0.9rem; font-size: 0.9rem;
} }
.security-tips ul { .security-tips ul {
margin: 0; margin: 0;
padding-left: 1.2rem; padding-left: 1.2rem;
line-height: 1.4; line-height: 1.4;
} }
.theme-toggle { .theme-toggle {
position: absolute; position: absolute;
top: 1rem; top: 1rem;
@ -231,12 +243,12 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.theme-toggle:hover { .theme-toggle:hover {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
transform: scale(1.1); transform: scale(1.1);
} }
.loading-spinner { .loading-spinner {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@ -245,7 +257,7 @@
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
@ -253,12 +265,13 @@
} }
</style> </style>
</head> </head>
<body> <body>
<div class="setup-container"> <div class="setup-container">
<button class="theme-toggle" id="theme-toggle" title="Toggle theme"> <button class="theme-toggle" id="theme-toggle" title="Toggle theme">
<i class="fas fa-moon"></i> <i class="fas fa-moon"></i>
</button> </button>
<div class="setup-card"> <div class="setup-card">
<div class="setup-header"> <div class="setup-header">
<div class="logo"> <div class="logo">
@ -267,34 +280,22 @@
<h1>Welcome to AniWorld Manager</h1> <h1>Welcome to AniWorld Manager</h1>
<p>Let's set up your master password to secure your anime collection.</p> <p>Let's set up your master password to secure your anime collection.</p>
</div> </div>
<form class="setup-form" id="setup-form"> <form class="setup-form" id="setup-form">
<div class="form-group"> <div class="form-group">
<label for="directory" class="form-label">Anime Directory</label> <label for="directory" class="form-label">Anime Directory</label>
<input <input type="text" id="directory" name="directory" class="form-input" placeholder="C:\Anime"
type="text" value="{{ current_directory }}" required>
id="directory"
name="directory"
class="form-input"
placeholder="C:\Anime"
value="{{ current_directory }}"
required>
<div class="form-help"> <div class="form-help">
The directory where your anime series are stored. This can be changed later in settings. The directory where your anime series are stored. This can be changed later in settings.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password" class="form-label">Master Password</label> <label for="password" class="form-label">Master Password</label>
<div class="password-input-group"> <div class="password-input-group">
<input <input type="password" id="password" name="password" class="form-input password-input"
type="password" placeholder="Create a strong password" required minlength="8">
id="password"
name="password"
class="form-input password-input"
placeholder="Create a strong password"
required
minlength="8">
<button type="button" class="password-toggle" id="password-toggle" tabindex="-1"> <button type="button" class="password-toggle" id="password-toggle" tabindex="-1">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
@ -307,32 +308,27 @@
</div> </div>
<div class="strength-text" id="strength-text">Password strength will be shown here</div> <div class="strength-text" id="strength-text">Password strength will be shown here</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="confirm-password" class="form-label">Confirm Password</label> <label for="confirm-password" class="form-label">Confirm Password</label>
<div class="password-input-group"> <div class="password-input-group">
<input <input type="password" id="confirm-password" name="confirm-password"
type="password" class="form-input password-input" placeholder="Confirm your password" required
id="confirm-password"
name="confirm-password"
class="form-input password-input"
placeholder="Confirm your password"
required
minlength="8"> minlength="8">
<button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1"> <button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
</div> </div>
</div> </div>
<div id="message-container"></div> <div id="message-container"></div>
<button type="submit" class="setup-button" id="setup-button"> <button type="submit" class="setup-button" id="setup-button">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
<span>Complete Setup</span> <span>Complete Setup</span>
</button> </button>
</form> </form>
<div class="security-tips"> <div class="security-tips">
<h4><i class="fas fa-shield-alt"></i> Security Tips</h4> <h4><i class="fas fa-shield-alt"></i> Security Tips</h4>
<ul> <ul>
@ -349,25 +345,25 @@
// Theme toggle functionality // Theme toggle functionality
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
const htmlElement = document.documentElement; const htmlElement = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'light'; const savedTheme = localStorage.getItem('theme') || 'light';
htmlElement.setAttribute('data-theme', savedTheme); htmlElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme); updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', () => { themeToggle.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-theme'); const currentTheme = htmlElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
htmlElement.setAttribute('data-theme', newTheme); htmlElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme); localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme); updateThemeIcon(newTheme);
}); });
function updateThemeIcon(theme) { function updateThemeIcon(theme) {
const icon = themeToggle.querySelector('i'); const icon = themeToggle.querySelector('i');
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
} }
// Password visibility toggles // Password visibility toggles
document.querySelectorAll('.password-toggle').forEach(toggle => { document.querySelectorAll('.password-toggle').forEach(toggle => {
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
@ -375,50 +371,50 @@
const type = input.getAttribute('type'); const type = input.getAttribute('type');
const newType = type === 'password' ? 'text' : 'password'; const newType = type === 'password' ? 'text' : 'password';
const icon = toggle.querySelector('i'); const icon = toggle.querySelector('i');
input.setAttribute('type', newType); input.setAttribute('type', newType);
icon.className = newType === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash'; icon.className = newType === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash';
}); });
}); });
// Password strength checker // Password strength checker
const passwordInput = document.getElementById('password'); const passwordInput = document.getElementById('password');
const strengthBars = document.querySelectorAll('.strength-bar'); const strengthBars = document.querySelectorAll('.strength-bar');
const strengthText = document.getElementById('strength-text'); const strengthText = document.getElementById('strength-text');
passwordInput.addEventListener('input', () => { passwordInput.addEventListener('input', () => {
const password = passwordInput.value; const password = passwordInput.value;
const strength = calculatePasswordStrength(password); const strength = calculatePasswordStrength(password);
updatePasswordStrength(strength); updatePasswordStrength(strength);
}); });
function calculatePasswordStrength(password) { function calculatePasswordStrength(password) {
let score = 0; let score = 0;
let feedback = []; let feedback = [];
// Length check // Length check
if (password.length >= 8) score++; if (password.length >= 8) score++;
if (password.length >= 12) score++; if (password.length >= 12) score++;
// Character variety // Character variety
if (/[a-z]/.test(password)) score++; if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++; if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++; if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++; if (/[^A-Za-z0-9]/.test(password)) score++;
// Penalties // Penalties
if (password.length < 8) { if (password.length < 8) {
feedback.push('Too short'); feedback.push('Too short');
score = Math.max(0, score - 2); score = Math.max(0, score - 2);
} }
if (!/[A-Z]/.test(password)) feedback.push('Add uppercase'); if (!/[A-Z]/.test(password)) feedback.push('Add uppercase');
if (!/[0-9]/.test(password)) feedback.push('Add numbers'); if (!/[0-9]/.test(password)) feedback.push('Add numbers');
if (!/[^A-Za-z0-9]/.test(password)) feedback.push('Add symbols'); if (!/[^A-Za-z0-9]/.test(password)) feedback.push('Add symbols');
const strengthLevels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong']; const strengthLevels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
const strengthLevel = Math.min(Math.floor(score / 1.2), 5); const strengthLevel = Math.min(Math.floor(score / 1.2), 5);
return { return {
score: Math.min(score, 6), score: Math.min(score, 6),
level: strengthLevel, level: strengthLevel,
@ -426,18 +422,18 @@
feedback feedback
}; };
} }
function updatePasswordStrength(strength) { function updatePasswordStrength(strength) {
const colors = ['weak', 'weak', 'fair', 'good', 'strong', 'strong']; const colors = ['weak', 'weak', 'fair', 'good', 'strong', 'strong'];
const color = colors[strength.level]; const color = colors[strength.level];
strengthBars.forEach((bar, index) => { strengthBars.forEach((bar, index) => {
bar.className = 'strength-bar'; bar.className = 'strength-bar';
if (index < strength.score) { if (index < strength.score) {
bar.classList.add('active', color); bar.classList.add('active', color);
} }
}); });
if (passwordInput.value) { if (passwordInput.value) {
let text = `Password strength: ${strength.text}`; let text = `Password strength: ${strength.text}`;
if (strength.feedback.length > 0) { if (strength.feedback.length > 0) {
@ -450,22 +446,22 @@
strengthText.style.color = 'var(--color-text-secondary)'; strengthText.style.color = 'var(--color-text-secondary)';
} }
} }
// Form submission // Form submission
const setupForm = document.getElementById('setup-form'); const setupForm = document.getElementById('setup-form');
const setupButton = document.getElementById('setup-button'); const setupButton = document.getElementById('setup-button');
const messageContainer = document.getElementById('message-container'); const messageContainer = document.getElementById('message-container');
const confirmPasswordInput = document.getElementById('confirm-password'); const confirmPasswordInput = document.getElementById('confirm-password');
const directoryInput = document.getElementById('directory'); const directoryInput = document.getElementById('directory');
// Real-time password confirmation // Real-time password confirmation
confirmPasswordInput.addEventListener('input', validatePasswordMatch); confirmPasswordInput.addEventListener('input', validatePasswordMatch);
passwordInput.addEventListener('input', validatePasswordMatch); passwordInput.addEventListener('input', validatePasswordMatch);
function validatePasswordMatch() { function validatePasswordMatch() {
const password = passwordInput.value; const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value; const confirmPassword = confirmPasswordInput.value;
if (confirmPassword && password !== confirmPassword) { if (confirmPassword && password !== confirmPassword) {
confirmPasswordInput.setCustomValidity('Passwords do not match'); confirmPasswordInput.setCustomValidity('Passwords do not match');
confirmPasswordInput.style.borderColor = 'var(--color-error)'; confirmPasswordInput.style.borderColor = 'var(--color-error)';
@ -474,32 +470,32 @@
confirmPasswordInput.style.borderColor = 'var(--color-border)'; confirmPasswordInput.style.borderColor = 'var(--color-border)';
} }
} }
setupForm.addEventListener('submit', async (e) => { setupForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const password = passwordInput.value; const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value; const confirmPassword = confirmPasswordInput.value;
const directory = directoryInput.value.trim(); const directory = directoryInput.value.trim();
if (password !== confirmPassword) { if (password !== confirmPassword) {
showMessage('Passwords do not match', 'error'); showMessage('Passwords do not match', 'error');
return; return;
} }
const strength = calculatePasswordStrength(password); const strength = calculatePasswordStrength(password);
if (strength.level < 2) { if (strength.level < 2) {
showMessage('Password is too weak. Please use a stronger password.', 'error'); showMessage('Password is too weak. Please use a stronger password.', 'error');
return; return;
} }
if (!directory) { if (!directory) {
showMessage('Please enter a valid anime directory', 'error'); showMessage('Please enter a valid anime directory', 'error');
return; return;
} }
setLoading(true); setLoading(true);
try { try {
const response = await fetch('/api/auth/setup', { const response = await fetch('/api/auth/setup', {
method: 'POST', method: 'POST',
@ -511,9 +507,9 @@
directory directory
}) })
}); });
const data = await response.json(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
showMessage('Setup completed successfully! Redirecting...', 'success'); showMessage('Setup completed successfully! Redirecting...', 'success');
setTimeout(() => { setTimeout(() => {
@ -529,7 +525,7 @@
setLoading(false); setLoading(false);
} }
}); });
function showMessage(message, type) { function showMessage(message, type) {
messageContainer.innerHTML = ` messageContainer.innerHTML = `
<div class="${type}-message"> <div class="${type}-message">
@ -537,12 +533,12 @@
</div> </div>
`; `;
} }
function setLoading(loading) { function setLoading(loading) {
setupButton.disabled = loading; setupButton.disabled = loading;
const buttonText = setupButton.querySelector('span'); const buttonText = setupButton.querySelector('span');
const buttonIcon = setupButton.querySelector('i'); const buttonIcon = setupButton.querySelector('i');
if (loading) { if (loading) {
buttonIcon.className = 'loading-spinner'; buttonIcon.className = 'loading-spinner';
buttonText.textContent = 'Setting up...'; buttonText.textContent = 'Setting up...';
@ -551,7 +547,7 @@
buttonText.textContent = 'Complete Setup'; buttonText.textContent = 'Complete Setup';
} }
} }
// Clear message on input // Clear message on input
[passwordInput, confirmPasswordInput, directoryInput].forEach(input => { [passwordInput, confirmPasswordInput, directoryInput].forEach(input => {
input.addEventListener('input', () => { input.addEventListener('input', () => {
@ -560,4 +556,5 @@
}); });
</script> </script>
</body> </body>
</html> </html>