Files
ohm_streaming/templates/login.html
T
root ef72e221be 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>
2026-01-29 17:25:50 +00:00

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>