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
14 changes: 9 additions & 5 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=["*"],
Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion backend/routes/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
87 changes: 73 additions & 14 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 @@ -13,24 +15,36 @@

supabase = None
latest_card = None
FRONTEND_URL = 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):
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
Expand All @@ -47,44 +61,89 @@ 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
}


@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_card_context(latest_card, data.card_id)
validate_origin(request, FRONTEND_URLS)

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,
"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")
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({
Expand All @@ -98,7 +157,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()
Expand Down
23 changes: 18 additions & 5 deletions backend/utils/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
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';

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
Loading
Loading