ef72e221be
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>
334 lines
11 KiB
HTML
334 lines
11 KiB
HTML
<!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>
|