From bd7c67fc93a8c420c6ec876063544b5b65fb8148 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Sat, 25 Apr 2026 20:25:47 +0200 Subject: [PATCH] login --- MEMORY.md | 2 + frontend/src/App.jsx | 135 ++++++++++++++++++++++++++++++++++++++-- frontend/src/styles.css | 42 ++++++++++++- 3 files changed, 174 insertions(+), 5 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index 0e862bd..d5b2d5d 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -17,6 +17,8 @@ Ce projet est un POC de professeur virtuel pour enfants, avec: - Le backend a un socle d'authentification avec comptes `users`, roles `student`, `teacher`, `maintenance`. - Le compte admin/prof initial est seede depuis `.env` via `ADMIN_USERNAME` et `ADMIN_PASSWORD`. - Le login pose un cookie HTTP-only `professeur_top_session` et renvoie aussi un token bearer. +- Le frontend affiche maintenant une page de connexion avant l'application et verifie la session via `/auth/me`. +- Apres connexion admin/prof, l'interface actuelle reste accessible avec un bouton de deconnexion. - Le vrai `docker-compose.yml` de production n'est pas dans ce repo. Il est situe un niveau au-dessus sur le serveur. - La conf nginx reelle route: - `/api/` vers `tutor-backend:8000` diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 91b0685..bcc6211 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,13 +30,19 @@ async function parseApiResponse(res) { return data } -async function apiFetch(path, options) { - const res = await fetch(`${API_BASE}${path}`, options) +async function apiFetch(path, options = {}) { + const res = await fetch(`${API_BASE}${path}`, { + credentials: 'same-origin', + ...options, + }) return parseApiResponse(res) } async function fetchAudio(path, options) { - const res = await fetch(`${API_BASE}${path}`, options) + const res = await fetch(`${API_BASE}${path}`, { + credentials: 'same-origin', + ...options, + }) if (!res.ok) { let detail = `Erreur API (${res.status})` const contentType = res.headers.get('content-type') || '' @@ -99,7 +105,72 @@ function getSupportedRecordingMimeType() { return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || '' } -export default function App() { +function LoginPage({ onLogin }) { + const [credentials, setCredentials] = useState({ username: '', password: '' }) + const [status, setStatus] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + async function submitLogin(e) { + e.preventDefault() + if (!credentials.username.trim() || !credentials.password) return + + setIsSubmitting(true) + setStatus('') + try { + const data = await apiFetch('/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: credentials.username.trim(), + password: credentials.password, + }), + }) + onLogin(data.user) + } catch (error) { + setStatus(error.message || 'Connexion impossible.') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ +
+

Professeur TOP

+

Connexion eleve, professeur ou maintenance.

+
+ +
+ + setCredentials({ ...credentials, username: event.target.value }) + } + /> + + setCredentials({ ...credentials, password: event.target.value }) + } + /> + +
+ {status &&

{status}

} +
+
+ ) +} + +function TutorApp({ currentUser, onLogout }) { const [students, setStudents] = useState([]) const [selectedStudentId, setSelectedStudentId] = useState('') const [form, setForm] = useState({ first_name: '', age: 8, grade: 'CM1' }) @@ -725,6 +796,12 @@ export default function App() { : 'Choisis ou crée un élève pour commencer.'}

+
+ {currentUser.username} · {currentUser.role} + +
@@ -795,3 +872,53 @@ export default function App() { ) } + +export default function App() { + const [currentUser, setCurrentUser] = useState(null) + const [isCheckingSession, setIsCheckingSession] = useState(true) + + useEffect(() => { + let isMounted = true + + async function loadSession() { + try { + const user = await apiFetch('/auth/me') + if (isMounted) setCurrentUser(user) + } catch { + if (isMounted) setCurrentUser(null) + } finally { + if (isMounted) setIsCheckingSession(false) + } + } + + loadSession() + return () => { + isMounted = false + } + }, []) + + async function logout() { + try { + await apiFetch('/auth/logout', { method: 'POST' }) + } finally { + setCurrentUser(null) + } + } + + if (isCheckingSession) { + return ( +
+
+ +

Verification de la session...

+
+
+ ) + } + + if (!currentUser) { + return + } + + return +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 87ec25f..5dec03e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -19,6 +19,10 @@ button { cursor: pointer; } button:disabled { opacity: 0.5; cursor: not-allowed; } +.secondary-button { + background: #e2e8f0; + color: #14213d; +} .layout { display: grid; @@ -36,7 +40,15 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } .sidebar, .main { display: flex; flex-direction: column; gap: 1rem; } .stack { display: flex; flex-direction: column; gap: 0.75rem; } .small-gap { gap: 0.5rem; } -.hero { display: flex; align-items: center; gap: 1rem; } +.hero { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } +.session-user { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.75rem; + color: #64748b; + font-size: 0.95rem; +} .messages { flex: 1; min-height: 360px; @@ -113,6 +125,28 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } display: block; } .avatar-caption { font-weight: 700; } +.login-page { + min-height: 100vh; + display: grid; + place-items: center; + padding: 1rem; +} +.login-panel { + width: min(420px, 100%); + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} +.login-copy { + text-align: center; +} +.login-copy h1 { + margin: 0; +} +.login-form { + width: 100%; +} @keyframes pulse { from { transform: scale(1); } @@ -122,4 +156,10 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } @media (max-width: 920px) { .layout { grid-template-columns: 1fr; } .composer { grid-template-columns: 1fr; } + .hero { align-items: flex-start; } + .session-user { + width: 100%; + margin-left: 0; + justify-content: space-between; + } }