Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 63 additions & 7 deletions backend/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -50,38 +61,83 @@ 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)

Comment on lines +67 to +69
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L’association de carte via scanned_card_id repose sur validate_card_context(latest_card, scanned_card_id)latest_card est un état global partagé. En environnement multi-clients, un scan récent d’un autre utilisateur peut permettre une association erronée (condition de course). Pour fiabiliser, stocker le contexte de scan par client/session (token côté frontend, binding à socket id, ou autre mécanisme) plutôt qu’un unique latest_card global.

Copilot uses AI. Check for mistakes.
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
}


@router.post("/submit")
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)
Comment on lines +107 to +111
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

La vérification existing_email renvoie un message explicite quand un email existe déjà. Ça facilite l’énumération d’emails (et donc la collecte d’infos) via l’endpoint d’inscription. Envisager de renvoyer un message plus générique (ou de toujours répondre 200/202) et/ou d’ajouter des protections (rate limiting, CAPTCHA) selon le contexte de déploiement.

Copilot uses AI. Check for mistakes.
.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]}"
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dans le cas NO_CARD_PLACEHOLDER, vous générez un normalized_card_id temporaire mais ne réinitialisez pas latest_card. Si une carte a été scannée juste avant, le contexte (id/ts) reste potentiellement valide et pourrait être réutilisé par inadvertance par d’autres endpoints protégés par validate_card_context. Pensez à invalider/vider latest_card aussi dans cette branche.

Suggested change
normalized_card_id = f"{TEMP_CARD_PREFIX}{uuid4().hex[:12]}"
normalized_card_id = f"{TEMP_CARD_PREFIX}{uuid4().hex[:12]}"
latest_card["id"] = None
latest_card["ts"] = None

Copilot uses AI. Check for mistakes.
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,
"password": data.password,
"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")
Expand Down
31 changes: 28 additions & 3 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,47 @@
}

.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 {
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%;
Expand Down
64 changes: 51 additions & 13 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Comment on lines +12 to 13
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO_CARD_PLACEHOLDER est défini ici, dans Inscription.tsx et côté backend (backend/routes/users.py). Comme c’est une valeur de protocole frontend/backend, cette duplication peut dériver silencieusement. Centraliser la constante (ex: fichier constants, config, ou valeur renvoyée par l’API) pour garantir la cohérence.

Suggested change
const NO_CARD_PLACEHOLDER = '000000';

Copilot uses AI. Check for mistakes.
function App() {
const [scannedCardId, setScannedCardId] = useState<string | null>(null);
Expand All @@ -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);
Expand All @@ -58,8 +61,42 @@ function App() {
);

case 'inscription':
return scannedCardId ?
<> <div className="background" /> <Inscription card_id={scannedCardId} /> </>: null;
return (
<>
<div className="background" />
<Inscription card_id={scannedCardId || NO_CARD_PLACEHOLDER} />
</>
);

case 'cardNotFound':
return (
<>
<div className="background" />
<div className="login-container">
<h2>Carte inconnue</h2>
<p>Cette carte n&apos;est pas encore associée.</p>
<p>Avez-vous déjà un compte ?</p>
<div className="auth-choice-actions">
<Bouton onClick={() => setAppState('linkLogin')} label="Oui, j&apos;ai déjà un compte" />
<Bouton onClick={() => setAppState('inscription')} component_type="secondary" label="Non, créer un compte" />
</div>
</div>
</>
);
case 'linkLogin':
return (
<>
<div className="background" />
<Connexion
scannedCardId={scannedCardId}
onLoginSuccess={(cardId: string) => {
setScannedCardId(cardId);
setAppState('calendar');
}}
onBack={() => setAppState('cardNotFound')}
/>
</>
);

case 'calendar':
return (
Expand All @@ -73,15 +110,16 @@ function App() {
return (
<>
<div className="background" />
<div className="login-container">
<h2>Bienvenue au CreaLab</h2>
<p>Veuillez scanner votre carte pour continuer.</p>
<Connexion
onLoginSuccess={(cardId: string) => {
setScannedCardId(cardId);
setAppState('calendar');
}}
/>
<div className="login-container">
<h2>Bienvenue au CreaLab</h2>
<Connexion
onLoginSuccess={(cardId: string) => {
setScannedCardId(cardId);
setAppState('calendar');
}}
/>
<p>Vous n&apos;êtes pas encore inscrit ?</p>
<Bouton onClick={() => setAppState('inscription')} label="S'inscrire" />
</div>
</>
);
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/components/Connexion/Connexion.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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;
Expand Down
23 changes: 20 additions & 3 deletions frontend/src/components/Connexion/Connexion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand Down Expand Up @@ -39,6 +41,7 @@ const Connexion = ({ onLoginSuccess }: ConnexionProps) => {
body: JSON.stringify({
email: String(email),
password: hashedPassword,
scanned_card_id: scannedCardId || null,
}),
});

Expand All @@ -54,15 +57,20 @@ 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);
}
};

return (
<div className="connexion_container">
<h2>Connexion manuelle</h2>
<h2>{scannedCardId ? "Connexion et association de carte" : "Connexion manuelle"}</h2>
{scannedCardId && <p className="connexion_info">Carte scannée : {scannedCardId}</p>}
<form className="connexion_form" onSubmit={handleSubmit}>
<FormEmail
label="Email"
Expand All @@ -88,6 +96,15 @@ const Connexion = ({ onLoginSuccess }: ConnexionProps) => {
label={isLoading ? "Connexion..." : "Se connecter"}
disabled={isLoading}
/>
{onBack && (
<Bouton
type="button"
component_type="secondary"
label="Retour"
onClick={onBack}
disabled={isLoading}
/>
)}
</form>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/Inscription/Inscription.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/components/Inscription/Inscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface InscriptionInterface {
card_id: string;
};

const NO_CARD_PLACEHOLDER = "000000";

const Inscription = ({card_id}: InscriptionInterface) => {
const { getApiUrl, getHeaders } = useApi();
Expand Down Expand Up @@ -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 (
<div className="inscription_container">
<h2>Formulaire d&apos;Inscription</h2>
{card_id === NO_CARD_PLACEHOLDER && (
<p className="inscription_info">
Aucun scan de carte detecte. Un identifiant provisoire sera utilise jusqu&apos;a l&apos;association d&apos;une carte.
</p>
Comment on lines +98 to +100
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Texte UI ajouté avec plusieurs accents manquants (ex: « detecte », « utilise », « jusqu'a »). Comme d’autres écrans utilisent les accents, corriger la phrase pour une meilleure qualité de contenu et cohérence.

Copilot uses AI. Check for mistakes.
)}
<form className="inscription_form" onSubmit={handleSubmit}>
<FormText
label="Prénom"
Expand Down
Loading