Docker compose healthchecks target /health on combined-api, dashboard-api and portal, but these endpoints did not exist (returned 404). This caused all dependent services (condition: service_healthy) to fail startup. - combined_api.py: GET /health + /turf/health with DB connectivity check - dashboard_api.py: GET /health + /turf/health with DB connectivity check - portal_server.py: GET /health (lightweight, no DB) QA Finding 1 from HRT-34 review of HRT-33 branch feature/devops-cicd. Co-Authored-By: Paperclip <noreply@paperclip.ing>
183 lines
7.9 KiB
HTML
183 lines
7.9 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 — Turf IA</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
:root {
|
|
--green: #00c853; --green-d: #009624;
|
|
--dark: #0d1117; --dark2: #161b22; --dark3: #21262d;
|
|
--text: #e6edf3; --muted: #8b949e; --border: #30363d;
|
|
--error: #f85149; --radius: 10px;
|
|
}
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--dark); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; }
|
|
a { color: inherit; text-decoration: none; }
|
|
nav { display: flex; align-items: center; justify-content: space-between; padding: 16px 5%; border-bottom: 1px solid var(--border); }
|
|
.nav-logo { font-weight: 700; font-size: 1.1rem; }
|
|
.nav-link { color: var(--muted); font-size: .9rem; }
|
|
.nav-link:hover { color: var(--text); }
|
|
|
|
main { flex: 1; display: flex; align-items: center; justify-content: center; padding: 40px 20px; }
|
|
.auth-card {
|
|
width: 100%; max-width: 420px;
|
|
background: var(--dark2); border: 1px solid var(--border);
|
|
border-radius: 14px; padding: 40px;
|
|
}
|
|
.auth-title { font-size: 1.5rem; font-weight: 800; margin-bottom: 6px; }
|
|
.auth-subtitle { color: var(--muted); font-size: .9rem; margin-bottom: 28px; }
|
|
.form-group { margin-bottom: 18px; }
|
|
label { display: block; font-size: .85rem; font-weight: 600; color: var(--muted); margin-bottom: 6px; }
|
|
input {
|
|
width: 100%; padding: 11px 14px; background: var(--dark3);
|
|
border: 1px solid var(--border); border-radius: var(--radius);
|
|
color: var(--text); font-size: .95rem; outline: none; transition: border-color .2s;
|
|
}
|
|
input:focus { border-color: var(--green); }
|
|
input.error { border-color: var(--error); }
|
|
.field-error { color: var(--error); font-size: .8rem; margin-top: 4px; display: none; }
|
|
.field-error.show { display: block; }
|
|
.forgot { float: right; font-size: .82rem; color: var(--muted); }
|
|
.forgot:hover { color: var(--text); }
|
|
.btn {
|
|
width: 100%; padding: 12px; border: none; border-radius: var(--radius);
|
|
font-size: 1rem; font-weight: 700; cursor: pointer; transition: all .2s;
|
|
margin-top: 8px;
|
|
}
|
|
.btn-primary { background: var(--green); color: #000; }
|
|
.btn-primary:hover { background: var(--green-d); }
|
|
.btn-primary:disabled { opacity: .6; cursor: not-allowed; }
|
|
.divider { display: flex; align-items: center; gap: 12px; margin: 20px 0; color: var(--muted); font-size: .82rem; }
|
|
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
|
.auth-footer { text-align: center; margin-top: 20px; color: var(--muted); font-size: .9rem; }
|
|
.auth-footer a { color: var(--green); font-weight: 600; }
|
|
.alert { padding: 12px 16px; border-radius: var(--radius); font-size: .88rem; margin-bottom: 18px; display: none; }
|
|
.alert.show { display: block; }
|
|
.alert-error { background: rgba(248,81,73,.12); border: 1px solid rgba(248,81,73,.3); color: #f85149; }
|
|
.loader { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(0,0,0,.3); border-top-color: #000; border-radius: 50%; animation: spin .7s linear infinite; vertical-align: middle; margin-right: 6px; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
footer { text-align: center; padding: 20px; color: var(--muted); font-size: .8rem; border-top: 1px solid var(--border); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav>
|
|
<a href="/" class="nav-logo">🏇 Turf IA</a>
|
|
<a href="/register" class="nav-link">Pas encore de compte ? S'inscrire →</a>
|
|
</nav>
|
|
<main>
|
|
<div class="auth-card">
|
|
<h1 class="auth-title">Bon retour !</h1>
|
|
<p class="auth-subtitle">Connectez-vous à votre compte Turf IA.</p>
|
|
|
|
<div class="alert alert-error" id="alert-error"></div>
|
|
|
|
<form id="login-form" novalidate>
|
|
<div class="form-group">
|
|
<label for="email">Adresse email</label>
|
|
<input type="email" id="email" name="email" placeholder="vous@exemple.fr" autocomplete="email" required>
|
|
<div class="field-error" id="email-error">Email invalide.</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="password">
|
|
Mot de passe
|
|
<a href="/forgot-password" class="forgot">Mot de passe oublié ?</a>
|
|
</label>
|
|
<input type="password" id="password" name="password" placeholder="••••••••" autocomplete="current-password" required>
|
|
<div class="field-error" id="password-error">Mot de passe requis.</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" id="submit-btn">Se connecter</button>
|
|
</form>
|
|
|
|
<div class="divider">ou</div>
|
|
<div class="auth-footer">
|
|
Pas encore de compte ? <a href="/register">Créer un compte gratuit</a>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<footer>© 2026 Turf IA — H3R7 Tech. Jouez de façon responsable. 18+</footer>
|
|
|
|
<script>
|
|
const API = '/api/v1';
|
|
|
|
const form = document.getElementById('login-form');
|
|
const emailInput = document.getElementById('email');
|
|
const passInput = document.getElementById('password');
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
const alertBox = document.getElementById('alert-error');
|
|
|
|
function showError(msg) {
|
|
alertBox.textContent = msg;
|
|
alertBox.classList.add('show');
|
|
}
|
|
function hideError() { alertBox.classList.remove('show'); }
|
|
|
|
function setLoading(on) {
|
|
submitBtn.disabled = on;
|
|
submitBtn.innerHTML = on ? '<span class="loader"></span>Connexion…' : 'Se connecter';
|
|
}
|
|
|
|
function validateField(input, errId, condition, msg) {
|
|
const err = document.getElementById(errId);
|
|
if (condition) {
|
|
input.classList.add('error');
|
|
err.textContent = msg;
|
|
err.classList.add('show');
|
|
return false;
|
|
}
|
|
input.classList.remove('error');
|
|
err.classList.remove('show');
|
|
return true;
|
|
}
|
|
|
|
form.addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
hideError();
|
|
|
|
const email = emailInput.value.trim();
|
|
const pass = passInput.value;
|
|
|
|
const v1 = validateField(emailInput, 'email-error', !email || !/^[^@]+@[^@]+\.[^@]+$/.test(email), 'Adresse email invalide.');
|
|
const v2 = validateField(passInput, 'password-error', !pass, 'Mot de passe requis.');
|
|
if (!v1 || !v2) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch(`${API}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password: pass })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
showError(data.error || 'Identifiants incorrects. Veuillez réessayer.');
|
|
} else {
|
|
localStorage.setItem('turf_token', data.token);
|
|
localStorage.setItem('turf_user', JSON.stringify(data.user));
|
|
const next = new URLSearchParams(location.search).get('next') || '/dashboard';
|
|
location.href = next;
|
|
}
|
|
} catch(_) {
|
|
showError('Erreur de connexion. Vérifiez votre réseau.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
// Remove errors on typing
|
|
[emailInput, passInput].forEach(el => el.addEventListener('input', () => {
|
|
el.classList.remove('error');
|
|
document.getElementById(el.id + '-error')?.classList.remove('show');
|
|
hideError();
|
|
}));
|
|
|
|
// If already logged in, redirect
|
|
(function() {
|
|
const token = localStorage.getItem('turf_token');
|
|
if (token) location.href = '/dashboard';
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|