feat: Add complete user authentication system with JWT and mandatory login
Implemented a comprehensive authentication system requiring all users to be logged in to access the web interface. Features include: Backend: - JWT-based authentication with 7-day token expiration - bcrypt password hashing with 72-byte limit handling - User management with JSON file storage (config/users.json) - Pydantic models for validation (UserCreate, UserLogin, User, Token) - Authentication endpoints: register, login, me, logout - Protected route dependency with HTTPBearer security Frontend: - Login/register page with dual-tab interface (/login) - Client-side authentication check with automatic redirect - All content hidden by default, shown only after auth validation - User info display with logout button - Main content and tabs hidden when not authenticated - Auto-redirect to /login if token missing or invalid Security: - Password truncation to 72 bytes (bcrypt limitation) - Token verification on each page load - Automatic logout and redirect on token expiry - Username-to-SHA256 user ID generation Dependencies: - passlib[bcrypt]==1.7.4 - python-jose[cryptography]==3.3.0 - bcrypt<4.0 Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
+10
-9
@@ -9,15 +9,16 @@
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/static/js/api.js?v=1.5" defer></script>
|
||||
<script src="/static/js/utils.js?v=1.5" defer></script>
|
||||
<script src="/static/js/downloads.js?v=1.5" defer></script>
|
||||
<script src="/static/js/anime.js?v=1.5" defer></script>
|
||||
<script src="/static/js/anime-details.js?v=1.5" defer></script>
|
||||
<script src="/static/js/series-search.js?v=1.5" defer></script>
|
||||
<script src="/static/js/recommendations.js?v=1.5" defer></script>
|
||||
<script src="/static/js/tabs.js?v=1.5" defer></script>
|
||||
<script src="/static/js/main.js?v=1.5" defer></script>
|
||||
<script src="/static/js/auth.js?v=1.9" defer></script>
|
||||
<script src="/static/js/api.js?v=1.9" defer></script>
|
||||
<script src="/static/js/utils.js?v=1.9" defer></script>
|
||||
<script src="/static/js/downloads.js?v=1.9" defer></script>
|
||||
<script src="/static/js/anime.js?v=1.9" defer></script>
|
||||
<script src="/static/js/anime-details.js?v=1.9" defer></script>
|
||||
<script src="/static/js/series-search.js?v=1.9" defer></script>
|
||||
<script src="/static/js/recommendations.js?v=1.9" defer></script>
|
||||
<script src="/static/js/tabs.js?v=1.9" defer></script>
|
||||
<script src="/static/js/main.js?v=1.9" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
<h1>⚡ Ohm Stream Downloader</h1>
|
||||
<p class="subtitle">Téléchargez vos vidéos, animes et séries depuis vos hébergeurs préférés</p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs">
|
||||
<!-- User info and logout button -->
|
||||
<div id="userInfo" style="display: none; margin-bottom: 15px; padding: 10px; background: rgba(0, 217, 255, 0.1); border-radius: 8px; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="color: #00d9ff;">👤</span>
|
||||
<span style="color: #fff; font-size: 14px;">Connecté en tant que <strong id="currentUser" style="color: #00d9ff;">-</strong></span>
|
||||
</div>
|
||||
<button class="btn-secondary btn-small" onclick="handleLogout()" style="padding: 5px 15px; font-size: 12px;">
|
||||
🚪 Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login prompt (shown when not logged in) -->
|
||||
<div id="loginPrompt" style="display: none; margin-bottom: 15px; padding: 15px; background: rgba(0, 217, 255, 0.1); border: 1px solid rgba(0, 217, 255, 0.3); border-radius: 8px; text-align: center;">
|
||||
<p style="color: #00d9ff; margin: 0 0 10px 0;">👋 Bienvenue! <a href="/login" style="color: #00d9ff; text-decoration: underline;">Connectez-vous</a> pour télécharger des vidéos</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabs - Hidden by default, shown only when authenticated -->
|
||||
<div class="tabs" style="visibility: hidden;">
|
||||
<button class="tab active" data-tab-type="home" onclick="switchTab('home')">
|
||||
<svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
{% block content %}
|
||||
{% include "components/header.html" %}
|
||||
|
||||
<!-- Main content - Hidden by default, shown only when authenticated -->
|
||||
<div id="main-content" style="display: none;">
|
||||
|
||||
{% include "components/home_section.html" %}
|
||||
|
||||
<!-- Nouveaux onglets -->
|
||||
@@ -110,4 +113,8 @@
|
||||
</div>
|
||||
|
||||
{% include "components/downloads_section.html" %}
|
||||
|
||||
</div>
|
||||
<!-- End of main-content -->
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - Ohm Stream Downloader</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.auth-container {
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, rgba(26, 26, 46, 0.95), rgba(22, 33, 62, 0.95));
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 217, 255, 0.3);
|
||||
}
|
||||
.auth-title {
|
||||
text-align: center;
|
||||
color: #00d9ff;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
}
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 2px solid rgba(0, 217, 255, 0.2);
|
||||
}
|
||||
.auth-tab {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.auth-tab.active {
|
||||
color: #00d9ff;
|
||||
border-bottom: 2px solid #00d9ff;
|
||||
}
|
||||
.auth-tab:hover {
|
||||
color: #00d9ff;
|
||||
}
|
||||
.auth-form {
|
||||
display: none;
|
||||
}
|
||||
.auth-form.active {
|
||||
display: block;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #00d9ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: rgba(0, 217, 255, 0.1);
|
||||
border: 1px solid rgba(0, 217, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #00d9ff;
|
||||
box-shadow: 0 0 10px rgba(0, 217, 255, 0.3);
|
||||
}
|
||||
.form-group input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
.auth-error {
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
border: 1px solid #ff6b6b;
|
||||
color: #ff6b6b;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
.auth-error.show {
|
||||
display: block;
|
||||
}
|
||||
.auth-success {
|
||||
background: rgba(0, 217, 255, 0.2);
|
||||
border: 1px solid #00d9ff;
|
||||
color: #00d9ff;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
.auth-success.show {
|
||||
display: block;
|
||||
}
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.back-link a {
|
||||
color: #00d9ff;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
.back-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<h1 class="auth-title">🎬 Ohm Stream</h1>
|
||||
|
||||
<div class="auth-tabs">
|
||||
<div class="auth-tab active" onclick="switchTab('login')">Connexion</div>
|
||||
<div class="auth-tab" onclick="switchTab('register')">Inscription</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-error" id="authError"></div>
|
||||
<div class="auth-success" id="authSuccess"></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form class="auth-form active" id="loginForm" onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">Nom d'utilisateur</label>
|
||||
<input
|
||||
type="text"
|
||||
id="loginUsername"
|
||||
placeholder="Entrez votre nom d'utilisateur"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Mot de passe</label>
|
||||
<input
|
||||
type="password"
|
||||
id="loginPassword"
|
||||
placeholder="Entrez votre mot de passe"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary btn-block">Se connecter</button>
|
||||
</form>
|
||||
|
||||
<!-- Register Form -->
|
||||
<form class="auth-form" id="registerForm" onsubmit="handleRegister(event)">
|
||||
<div class="form-group">
|
||||
<label for="registerUsername">Nom d'utilisateur</label>
|
||||
<input
|
||||
type="text"
|
||||
id="registerUsername"
|
||||
placeholder="Choisissez un nom d'utilisateur"
|
||||
minlength="3"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerEmail">Email (optionnel)</label>
|
||||
<input
|
||||
type="email"
|
||||
id="registerEmail"
|
||||
placeholder="votre@email.com"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerFullName">Nom complet (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="registerFullName"
|
||||
placeholder="Votre nom complet"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerPassword">Mot de passe</label>
|
||||
<input
|
||||
type="password"
|
||||
id="registerPassword"
|
||||
placeholder="Au moins 6 caractères"
|
||||
minlength="6"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerPasswordConfirm">Confirmer le mot de passe</label>
|
||||
<input
|
||||
type="password"
|
||||
id="registerPasswordConfirm"
|
||||
placeholder="Confirmez votre mot de passe"
|
||||
minlength="6"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary btn-block">S'inscrire</button>
|
||||
</form>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/web">← Retour à l'accueil</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = window.location.protocol + '//' + window.location.host;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
hideMessages();
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('authError');
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.classList.add('show');
|
||||
document.getElementById('authSuccess').classList.remove('show');
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const successDiv = document.getElementById('authSuccess');
|
||||
successDiv.textContent = message;
|
||||
successDiv.classList.add('show');
|
||||
document.getElementById('authError').classList.remove('show');
|
||||
}
|
||||
|
||||
function hideMessages() {
|
||||
document.getElementById('authError').classList.remove('show');
|
||||
document.getElementById('authSuccess').classList.remove('show');
|
||||
}
|
||||
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
hideMessages();
|
||||
|
||||
const username = document.getElementById('loginUsername').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Store token in localStorage
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
localStorage.setItem('user', JSON.stringify(data.user));
|
||||
|
||||
showSuccess('Connexion réussie! Redirection...');
|
||||
|
||||
// Redirect to home page after 1 second
|
||||
setTimeout(() => {
|
||||
window.location.href = '/web';
|
||||
}, 1000);
|
||||
} else {
|
||||
showError(data.detail || 'Erreur lors de la connexion');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
showError('Erreur de connexion au serveur');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister(event) {
|
||||
event.preventDefault();
|
||||
hideMessages();
|
||||
|
||||
const username = document.getElementById('registerUsername').value;
|
||||
const email = document.getElementById('registerEmail').value || null;
|
||||
const full_name = document.getElementById('registerFullName').value || null;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const passwordConfirm = document.getElementById('registerPasswordConfirm').value;
|
||||
|
||||
// Validate passwords match
|
||||
if (password !== passwordConfirm) {
|
||||
showError('Les mots de passe ne correspondent pas');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password, email, full_name })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showSuccess('Inscription réussie! Vous pouvez maintenant vous connecter.');
|
||||
|
||||
// Switch to login tab after 1.5 seconds
|
||||
setTimeout(() => {
|
||||
switchTab('login');
|
||||
document.getElementById('loginUsername').value = username;
|
||||
}, 1500);
|
||||
} else {
|
||||
showError(data.detail || 'Erreur lors de l\'inscription');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Register error:', error);
|
||||
showError('Erreur de connexion au serveur');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user