From 2131e95f9edf74ef6612bfe87b20c75736e39bba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:13:52 +0000 Subject: [PATCH 1/5] chore(release): 2.0.0 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad62149..eeb23a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crealabvisitors", - "version": "1.1.1", + "version": "2.0.0", "description": "Une petite application de gestion des visiteurs pour l'espace CreaLab. Suit les visites, stocke les métadonnées de base et fournit une interface/API simple pour consulter les journaux des visiteurs.", "homepage": "https://github.com/IIM-CDI/CreaLabVisitors#readme", "bugs": { From e3d76330f1f20e97092ed035fdcf2072bcb59357 Mon Sep 17 00:00:00 2001 From: Thomas Candille Date: Mon, 20 Apr 2026 12:04:27 +0200 Subject: [PATCH 2/5] feat: enhance CORS support by allowing multiple frontend URLs --- backend/main.py | 14 +++++++++----- backend/routes/events.py | 5 ++++- backend/routes/users.py | 17 ++++++++++------- backend/utils/auth.py | 23 ++++++++++++++++++----- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/backend/main.py b/backend/main.py index 43f2338..0e84ba6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -21,10 +21,14 @@ ) FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:3000") +FRONTEND_URL_DEPLOYED = os.getenv("FRONTEND_URL_DEPLOYED") +ALLOWED_FRONTEND_ORIGINS = [FRONTEND_URL] +if FRONTEND_URL_DEPLOYED and FRONTEND_URL_DEPLOYED not in ALLOWED_FRONTEND_ORIGINS: + ALLOWED_FRONTEND_ORIGINS.append(FRONTEND_URL_DEPLOYED) fastapi_app.add_middleware( CORSMiddleware, - allow_origins=[FRONTEND_URL], + allow_origins=ALLOWED_FRONTEND_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -33,11 +37,11 @@ latest_card = {"id": None, "ts": None, "role": None} SECRET_KEY = os.getenv("SECRET_KEY", "change_this_secret") -sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=[FRONTEND_URL]) +sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=ALLOWED_FRONTEND_ORIGINS) -init_card_routes(supabase, latest_card, sio, FRONTEND_URL) -init_user_routes(supabase, latest_card, FRONTEND_URL) -init_event_routes(supabase, sio, SECRET_KEY, FRONTEND_URL) +init_card_routes(supabase, latest_card, sio, ALLOWED_FRONTEND_ORIGINS) +init_user_routes(supabase, latest_card, ALLOWED_FRONTEND_ORIGINS) +init_event_routes(supabase, sio, SECRET_KEY, ALLOWED_FRONTEND_ORIGINS) fastapi_app.include_router(health_router) fastapi_app.include_router(cards_router) diff --git a/backend/routes/events.py b/backend/routes/events.py index f551a47..f00191f 100644 --- a/backend/routes/events.py +++ b/backend/routes/events.py @@ -24,7 +24,10 @@ def init_event_routes(db, socket_io=None, secret_key=None, frontend_url=None): supabase = db sio = socket_io EMAIL_TOKEN_SECRET = secret_key + "_email_salt" if secret_key else os.getenv("SECRET_KEY", "change_this_secret") + "_email_salt" - FRONTEND_URL = frontend_url or os.getenv("FRONTEND_URL") + if isinstance(frontend_url, (list, tuple)): + FRONTEND_URL = os.getenv("FRONTEND_URL_DEPLOYED") or (frontend_url[0] if frontend_url else None) + else: + FRONTEND_URL = frontend_url or os.getenv("FRONTEND_URL_DEPLOYED") or os.getenv("FRONTEND_URL") BACKEND_URL = os.getenv("BACKEND_URL") def generate_email_token(event_id: str, action: str, admin_email: str = "admin@crealab.com", expires_days: int = 7) -> str: diff --git a/backend/routes/users.py b/backend/routes/users.py index 9060a04..8394cde 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -13,7 +13,7 @@ supabase = None latest_card = None -FRONTEND_URL = None +FRONTEND_URLS = None class LoginData(BaseModel): @@ -22,15 +22,18 @@ class LoginData(BaseModel): def init_user_routes(db, card_data, frontend_url=None): - global supabase, latest_card, FRONTEND_URL + global supabase, latest_card, FRONTEND_URLS supabase = db latest_card = card_data - FRONTEND_URL = frontend_url or os.getenv("FRONTEND_URL") + FRONTEND_URLS = frontend_url or [ + os.getenv("FRONTEND_URL"), + os.getenv("FRONTEND_URL_DEPLOYED") + ] @router.post("/login") def login_user(request: Request, data: LoginData): - validate_origin(request, FRONTEND_URL) + validate_origin(request, FRONTEND_URLS) result = ( supabase @@ -59,7 +62,7 @@ def login_user(request: Request, data: LoginData): @router.post("/submit") def submit_data(request: Request, data: ProfileData): logging.info("Submitting profile for card: %s", data.card_id) - validate_origin(request, FRONTEND_URL) + validate_origin(request, FRONTEND_URLS) validate_card_context(latest_card, data.card_id) if not is_school_email(data.email): raise HTTPException( @@ -84,7 +87,7 @@ def submit_data(request: Request, data: ProfileData): @router.post("/update-profile") def update_profile(request: Request, data: ProfileData): logging.info("Updating profile for card: %s", data.card_id) - validate_origin(request, FRONTEND_URL) + validate_origin(request, FRONTEND_URLS) validate_card_context(latest_card, data.card_id) supabase.table("CreaLab_visitors").update({ @@ -98,7 +101,7 @@ def update_profile(request: Request, data: ProfileData): @router.get("/get-profile/{card_id}") def get_profile(request: Request, card_id: str): - validate_origin(request, FRONTEND_URL) + validate_origin(request, FRONTEND_URLS) validate_card_context(latest_card, card_id) result = supabase.table("CreaLab_visitors").select("*").eq("id_card", card_id).execute() diff --git a/backend/utils/auth.py b/backend/utils/auth.py index f0c5a2a..a2151bf 100644 --- a/backend/utils/auth.py +++ b/backend/utils/auth.py @@ -4,12 +4,25 @@ ALLOWED_SCHOOL_EMAIL_DOMAINS = ("@devinci.fr", "@edu.vinci.fr") -def validate_origin(request, frontend_url=None): - origin = request.headers.get("origin") or request.client.host - if frontend_url and frontend_url not in origin and origin != "http://localhost": - from fastapi import HTTPException, status +def _normalize_origin(value: str) -> str: + return value.rstrip("/") - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Origine non autorisée") + +def validate_origin(request, frontend_urls=None): + origin = request.headers.get("origin") + if not origin: + return + + if frontend_urls and isinstance(frontend_urls, str): + frontend_urls = [frontend_urls] + + if frontend_urls: + normalized_origin = _normalize_origin(origin) + allowed_origins = {_normalize_origin(url) for url in frontend_urls if url} + if normalized_origin not in allowed_origins and normalized_origin != "http://localhost": + from fastapi import HTTPException, status + + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Origine non autorisée") def validate_card_context(latest_card, card_id: str, max_age_seconds: int = 300): From 2617ec0c23e32419ee35575f1364d1c32a60f966 Mon Sep 17 00:00:00 2001 From: Thomas Candille Date: Fri, 24 Apr 2026 11:57:09 +0200 Subject: [PATCH 3/5] feat: implement card association and error handling in login and inscription processes --- backend/routes/users.py | 70 +++++++++++++++++-- frontend/src/App.css | 20 ++++++ frontend/src/App.tsx | 65 +++++++++++++---- .../src/components/Connexion/Connexion.css | 6 ++ .../src/components/Connexion/Connexion.tsx | 23 +++++- .../components/Inscription/Inscription.css | 7 ++ .../components/Inscription/Inscription.tsx | 12 +++- 7 files changed, 179 insertions(+), 24 deletions(-) diff --git a/backend/routes/users.py b/backend/routes/users.py index 8394cde..11fa679 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -4,6 +4,8 @@ import os from datetime import datetime from pydantic import BaseModel +from typing import Optional +from uuid import uuid4 from dotenv import load_dotenv from utils.auth import is_school_email, validate_card_context, validate_origin @@ -14,11 +16,20 @@ supabase = None latest_card = None FRONTEND_URLS = None +NO_CARD_PLACEHOLDER = "000000" +TEMP_CARD_PREFIX = "TMP-000000-" class LoginData(BaseModel): email: str password: str + scanned_card_id: Optional[str] = None + + +def _is_temporary_card(card_id: Optional[str]) -> bool: + if not card_id: + return True + return card_id == NO_CARD_PLACEHOLDER or card_id.startswith(TEMP_CARD_PREFIX) def init_user_routes(db, card_data, frontend_url=None): @@ -50,12 +61,41 @@ def login_user(request: Request, data: LoginData): if user.get("password") != data.password: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Identifiants invalides") - latest_card["id"] = user.get("id_card") + card_id = user.get("id_card") + scanned_card_id = data.scanned_card_id.strip() if data.scanned_card_id else None + + if scanned_card_id: + validate_card_context(latest_card, scanned_card_id) + + existing_card = ( + supabase + .table("CreaLab_visitors") + .select("email") + .eq("id_card", scanned_card_id) + .execute() + ) + + if existing_card.data and existing_card.data[0].get("email") != data.email: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Cette carte est déjà associée à un autre compte" + ) + + supabase.table("CreaLab_visitors").update({"id_card": scanned_card_id}).eq("email", data.email).execute() + card_id = scanned_card_id + + if _is_temporary_card(card_id): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Scannez une carte pour finaliser la connexion" + ) + + latest_card["id"] = card_id latest_card["ts"] = datetime.utcnow() return { "authenticated": True, - "card_id": user.get("id_card") + "card_id": card_id } @@ -63,17 +103,33 @@ def login_user(request: Request, data: LoginData): def submit_data(request: Request, data: ProfileData): logging.info("Submitting profile for card: %s", data.card_id) validate_origin(request, FRONTEND_URLS) - validate_card_context(latest_card, data.card_id) + + existing_email = ( + supabase + .table("CreaLab_visitors") + .select("email") + .eq("email", data.email) + .execute() + ) + if existing_email.data: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Un compte existe déjà avec cet email") + + normalized_card_id = data.card_id.strip() + if normalized_card_id == NO_CARD_PLACEHOLDER: + normalized_card_id = f"{TEMP_CARD_PREFIX}{uuid4().hex[:12]}" + else: + validate_card_context(latest_card, normalized_card_id) + latest_card["id"] = normalized_card_id + latest_card["ts"] = None + if not is_school_email(data.email): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Adresse email invalide: domaines autorises @devinci.fr ou @edu.vinci.fr" ) - latest_card["id"] = data.card_id - latest_card["ts"] = None supabase.table("CreaLab_visitors").insert({ - "id_card": data.card_id, + "id_card": normalized_card_id, "first_name": data.first_name, "last_name": data.last_name, "email": data.email, @@ -81,7 +137,7 @@ def submit_data(request: Request, data: ProfileData): "role": data.role.value, "admin": False }).execute() - return {"message": f"Carte {data.card_id} reçue avec succès"} + return {"message": f"Carte {normalized_card_id} reçue avec succès"} @router.post("/update-profile") diff --git a/frontend/src/App.css b/frontend/src/App.css index 58883d3..2eb6b03 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -41,6 +41,26 @@ font-size: 16px; color: #666; } + +.auth-choice-actions { + display: flex; + flex-direction: column; + gap: 12px; + align-items: center; + margin-top: 16px; +} + +.auth-choice-actions .bouton { + width: 100%; +} + +@media (max-width: 768px) { + .login-container, + .waiting-container { + width: calc(100vw - 48px); + } +} + .calendar-with-connexion { display: flex; height: 100%; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c1f3d85..3c9d002 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,8 +5,11 @@ import Calendar from './pages/Calendar/Calendar'; import Inscription from './components/Inscription/Inscription'; import Connexion from './components/Connexion/Connexion'; import { setCardScanCallback } from './services/cardScanListener'; +import Bouton from './components/Bouton/Bouton'; -type AppState = 'waiting' | 'login' | 'inscription' | 'calendar'; +type AppState = 'waiting' | 'login' | 'inscription' | 'calendar' | 'cardNotFound' | 'linkLogin'; + +const NO_CARD_PLACEHOLDER = '000000'; function App() { const [scannedCardId, setScannedCardId] = useState(null); @@ -31,7 +34,7 @@ function App() { if (data.exists) { setAppState('calendar'); } else { - setAppState('inscription'); + setAppState('cardNotFound'); } } catch (error) { console.error('Error checking existing card:', error); @@ -58,8 +61,43 @@ function App() { ); case 'inscription': - return scannedCardId ? - <>
: null; + return ( + <> +
+ + + ); + + case 'cardNotFound': + return ( + <> +
+
+

Carte inconnue

+

Cette carte n'est pas encore associée.

+

Avez-vous déjà un compte ?

+
+ setAppState('linkLogin')} label="Oui, j'ai déjà un compte" /> + setAppState('inscription')} component_type="secondary" label="Non, créer un compte" /> +
+
+ + ); + + case 'linkLogin': + return ( + <> +
+ { + setScannedCardId(cardId); + setAppState('calendar'); + }} + onBack={() => setAppState('cardNotFound')} + /> + + ); case 'calendar': return ( @@ -73,15 +111,16 @@ function App() { return ( <>
-
-

Bienvenue au CreaLab

-

Veuillez scanner votre carte pour continuer.

- { - setScannedCardId(cardId); - setAppState('calendar'); - }} - /> +
+

Bienvenue au CreaLab

+ { + setScannedCardId(cardId); + setAppState('calendar'); + }} + /> +

Vous n'êtes pas encore inscrit ?

+ setAppState('inscription')} label="S'inscrire" />
); diff --git a/frontend/src/components/Connexion/Connexion.css b/frontend/src/components/Connexion/Connexion.css index 6e22b22..200d609 100644 --- a/frontend/src/components/Connexion/Connexion.css +++ b/frontend/src/components/Connexion/Connexion.css @@ -36,6 +36,12 @@ grid-column-end: 3; } +.connexion_info { + margin: 0 0 16px; + color: #4b5563; + font-size: 0.95rem; +} + .connexion_error { grid-column-start: 1; grid-column-end: 3; diff --git a/frontend/src/components/Connexion/Connexion.tsx b/frontend/src/components/Connexion/Connexion.tsx index c2e064f..a32ce55 100644 --- a/frontend/src/components/Connexion/Connexion.tsx +++ b/frontend/src/components/Connexion/Connexion.tsx @@ -8,9 +8,11 @@ import { hashPassword } from "../../utils/auth"; interface ConnexionProps { onLoginSuccess: (cardId: string) => void; + scannedCardId?: string | null; + onBack?: () => void; } -const Connexion = ({ onLoginSuccess }: ConnexionProps) => { +const Connexion = ({ onLoginSuccess, scannedCardId, onBack }: ConnexionProps) => { const { getApiUrl, getHeaders } = useApi(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -39,6 +41,7 @@ const Connexion = ({ onLoginSuccess }: ConnexionProps) => { body: JSON.stringify({ email: String(email), password: hashedPassword, + scanned_card_id: scannedCardId || null, }), }); @@ -54,7 +57,11 @@ const Connexion = ({ onLoginSuccess }: ConnexionProps) => { onLoginSuccess(data.card_id); } catch (error) { console.error("Error during login:", error); - setErrorMessage("Connexion échouee. Verifiez vos identifiants."); + if (error instanceof Error && error.message) { + setErrorMessage(error.message); + } else { + setErrorMessage("Connexion échouee. Verifiez vos identifiants."); + } } finally { setIsLoading(false); } @@ -62,7 +69,8 @@ const Connexion = ({ onLoginSuccess }: ConnexionProps) => { return (
-

Connexion manuelle

+

{scannedCardId ? "Connexion et association de carte" : "Connexion manuelle"}

+ {scannedCardId &&

Carte scannée : {scannedCardId}

}
{ label={isLoading ? "Connexion..." : "Se connecter"} disabled={isLoading} /> + {onBack && ( + + )}
); diff --git a/frontend/src/components/Inscription/Inscription.css b/frontend/src/components/Inscription/Inscription.css index a4b93e0..935d737 100644 --- a/frontend/src/components/Inscription/Inscription.css +++ b/frontend/src/components/Inscription/Inscription.css @@ -20,6 +20,13 @@ gap: 20px; } +.inscription_info { + margin: 0 0 16px; + color: #4b5563; + font-size: 0.95rem; + text-align: center; +} + .inscription_form label { display: flex; flex-direction: column; diff --git a/frontend/src/components/Inscription/Inscription.tsx b/frontend/src/components/Inscription/Inscription.tsx index 247bb64..e30acb2 100644 --- a/frontend/src/components/Inscription/Inscription.tsx +++ b/frontend/src/components/Inscription/Inscription.tsx @@ -11,6 +11,7 @@ interface InscriptionInterface { card_id: string; }; +const NO_CARD_PLACEHOLDER = "000000"; const Inscription = ({card_id}: InscriptionInterface) => { const { getApiUrl, getHeaders } = useApi(); @@ -82,13 +83,22 @@ const Inscription = ({card_id}: InscriptionInterface) => { return; } - setFormError("L'inscription a echoue. Veuillez reessayer."); + if (error instanceof Error && error.message) { + setFormError(error.message); + } else { + setFormError("L'inscription a echoue. Veuillez reessayer."); + } } } return (

Formulaire d'Inscription

+ {card_id === NO_CARD_PLACEHOLDER && ( +

+ Aucun scan de carte detecte. Un identifiant provisoire sera utilise jusqu'a l'association d'une carte. +

+ )}
Date: Fri, 24 Apr 2026 12:13:50 +0200 Subject: [PATCH 4/5] style: improve layout and typography in login and connexion components --- frontend/src/App.css | 11 ++++++++--- frontend/src/components/Connexion/Connexion.css | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 2eb6b03..42e2ac9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -25,21 +25,26 @@ } .login-container, .waiting-container { + display: flex; + flex-direction: column; text-align: center; width: 40%; background-color: #fff; padding: 20px; border-radius: 8px; + gap: 16px; } .login-container h2, .waiting-container h2 { - margin-bottom: 20px; + font-size: 2em; color: #333; } .login-container p, .waiting-container p { - font-size: 16px; - color: #666; + font-size: 1.25em; + font-weight: 500; + margin-top: 12px; + color: #333; } .auth-choice-actions { diff --git a/frontend/src/components/Connexion/Connexion.css b/frontend/src/components/Connexion/Connexion.css index 200d609..ac01914 100644 --- a/frontend/src/components/Connexion/Connexion.css +++ b/frontend/src/components/Connexion/Connexion.css @@ -5,12 +5,12 @@ justify-content: center; height: fit-content; background: #fff; - padding: 32px; border-radius: 12px; h2 { - font-size: 1.5em; - margin-bottom: 20px; + font-size: 1.25em; + font-weight: 500; + margin-block: 20px; } } From 51d74bb769836b2dae31c258e55a829ae87be64c Mon Sep 17 00:00:00 2001 From: Thomas Candille Date: Fri, 24 Apr 2026 12:20:15 +0200 Subject: [PATCH 5/5] fix: correct HTML entity for apostrophe in inscription prompt --- frontend/src/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3c9d002..cbbd069 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -83,7 +83,6 @@ function App() {
); - case 'linkLogin': return ( <> @@ -119,7 +118,7 @@ function App() { setAppState('calendar'); }} /> -

Vous n'êtes pas encore inscrit ?

+

Vous n'êtes pas encore inscrit ?

setAppState('inscription')} label="S'inscrire" />