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.
+
+
+
+ {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;
+ }
}