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
24 changes: 24 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Example environment variables (copy to .env and fill values)
SUPABASE_URL=
SUPABASE_KEY=
SECRET_KEY=change_this_secret
BACKEND_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
FRONTEND_URL_DEPLOYED=

# SMTP (for email sending)
SMTP_SERVER=smtp.example.com
SMTP_PORT=465
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
SMTP_FROM_NAME=CreaLab
ADMIN_EMAIL=admin@crealab.com

# Google Calendar (optional)
GOOGLE_CALENDAR_SYNC_ENABLED=false
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REFRESH_TOKEN=
GOOGLE_CALENDAR_ID=primary
GOOGLE_CALENDAR_TIMEZONE=Europe/Paris
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
/.env
/.env.local
/.env.*.local
node_modules/
frontend/node_modules/
backend/__pycache__/
**/__pycache__/
*.pyc
frontend/build/
dist/
.venv/
.idea/
.vscode/
*.log
# Node modules
node_modules/
npm-debug.log*
Expand Down
4 changes: 3 additions & 1 deletion backend/models/event.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional


class EventCreate(BaseModel):
id: str
title: str
user: str
email: Optional[str] = None
start: datetime
startStr: str
end: datetime
endStr: str
duration: str
color: str
id_card: str
id_card: Optional[str] = None


class Event(EventCreate):
Expand Down
5 changes: 3 additions & 2 deletions backend/models/profile.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from pydantic import BaseModel
from typing import Optional
from .user import UserRole


class ProfileData(BaseModel):
card_id: str
card_id: Optional[str] = None
first_name: str
last_name: str
email: str
password: str
password: Optional[str] = None
role: UserRole
admin: bool = False
6 changes: 6 additions & 0 deletions backend/routes/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,15 @@ async def get_card(card_data: CardScan):

@router.get("/check-card/{card_id}")
def check_existing_card(card_id: str):
# Try to find by card_id first
result = supabase.table("CreaLab_visitors").select("*").eq("id_card", card_id).execute()
exists = len(result.data) > 0
if exists:
return {"exists": True, "data": result.data}
else:
# Fallback: also try to find by email (card_id could be an email)
result = supabase.table("CreaLab_visitors").select("*").eq("email", card_id).execute()
exists = len(result.data) > 0
if exists:
return {"exists": True, "data": result.data}
return {"exists": False}
37 changes: 25 additions & 12 deletions backend/routes/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,20 @@ def get_user_info_by_card_id(card_id: str) -> tuple:

def send_notification_email(event_data: dict, action: str) -> bool:
try:
card_id = event_data.get("id_card")
if not card_id:
logging.warning("No card_id found for event, cannot send notification email")
return False

user_email, user_name = get_user_info_by_card_id(card_id)
# Try to get user email from event data first
user_email = event_data.get("email")
user_name = event_data.get("user", "Utilisateur")

# If email not in event data, try to get it from card_id (fallback)
if not user_email:
logging.warning(f"No email found for card {card_id}, cannot send notification email")
card_id = event_data.get("id_card")
if card_id:
user_email, name = get_user_info_by_card_id(card_id)
if name:
user_name = name

if not user_email:
logging.warning("No email found for event, cannot send notification email")
return False

if action == "accept":
Expand All @@ -107,11 +113,18 @@ def sync_event_to_google_calendar(event_data: dict) -> None:
try:
event_payload = dict(event_data)

card_id = event_data.get("id_card")
if card_id:
creator_email, _ = get_user_info_by_card_id(card_id)
if creator_email:
event_payload["creator_email"] = creator_email
# Try to get creator email from event data first
creator_email = event_data.get("email")

# If email not in event data, try to get it from card_id (fallback)
if not creator_email:
card_id = event_data.get("id_card")
if card_id:
creator_email, _ = get_user_info_by_card_id(card_id)

# Add creator email to event payload if available
if creator_email:
event_payload["creator_email"] = creator_email

result = sync_validated_event_to_admin_calendar(event_payload)
if result.get("synced"):
Expand Down
38 changes: 19 additions & 19 deletions backend/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def login_user(request: Request, data: LoginData):
card_id = user.get("id_card")
scanned_card_id = data.scanned_card_id.strip() if data.scanned_card_id else None

# If a card is scanned, associate it with the account (optional step to make login faster)
if scanned_card_id:
validate_card_context(latest_card, scanned_card_id)

Expand All @@ -83,16 +84,10 @@ def login_user(request: Request, data: LoginData):

supabase.table("CreaLab_visitors").update({"id_card": scanned_card_id}).eq("email", data.email).execute()
card_id = scanned_card_id
latest_card["id"] = card_id
latest_card["ts"] = datetime.utcnow()

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()

# Allow login even without a valid card - card scanning is optional and used for faster identification
return {
"authenticated": True,
"card_id": card_id
Expand All @@ -114,7 +109,7 @@ def submit_data(request: Request, data: ProfileData):
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()
normalized_card_id = data.card_id.strip() if data.card_id else NO_CARD_PLACEHOLDER
if normalized_card_id == NO_CARD_PLACEHOLDER:
normalized_card_id = f"{TEMP_CARD_PREFIX}{uuid4().hex[:12]}"
else:
Expand All @@ -125,7 +120,7 @@ def submit_data(request: Request, data: ProfileData):
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"
detail="Adresse email invalide: domaines autorises @devinci.fr ou @edu.devinci.fr"
)

supabase.table("CreaLab_visitors").insert({
Expand All @@ -142,26 +137,31 @@ 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)
logging.info("Updating profile for email: %s", data.email)
validate_origin(request, FRONTEND_URLS)
validate_card_context(latest_card, data.card_id)


# Use email as primary identifier for updating profile
# This works whether user logged in with card or without
supabase.table("CreaLab_visitors").update({
"first_name": data.first_name,
"last_name": data.last_name,
"email": data.email,
"role": data.role.value
}).eq("id_card", data.card_id).execute()
return {"message": f"Profil pour la carte {data.card_id} mis à jour avec succès"}
}).eq("email", data.email).execute()

return {"message": f"Profil pour {data.email} mis à jour avec succès"}


@router.get("/get-profile/{card_id}")
def get_profile(request: Request, card_id: str):
validate_origin(request, FRONTEND_URLS)
validate_card_context(latest_card, card_id)


# Try to get profile by card_id first (for backward compatibility)
result = supabase.table("CreaLab_visitors").select("*").eq("id_card", card_id).execute()
if len(result.data) > 0:
return {"found": True, "data": result.data[0]}
else:
# Fallback: also search by email (card_id could be an email in some cases)
result = supabase.table("CreaLab_visitors").select("*").eq("email", card_id).execute()
if len(result.data) > 0:
return {"found": True, "data": result.data[0]}
return {"found": False}
2 changes: 1 addition & 1 deletion backend/utils/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime


ALLOWED_SCHOOL_EMAIL_DOMAINS = ("@devinci.fr", "@edu.vinci.fr")
ALLOWED_SCHOOL_EMAIL_DOMAINS = ("@devinci.fr", "@edu.devinci.fr")


def _normalize_origin(value: str) -> str:
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
display: flex;
justify-content: center;
align-items: center;
filter: blur(3px);
filter: blur(2px);
}

.inscription-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}

.login-container, .waiting-container {
Expand All @@ -32,7 +39,7 @@
background-color: #fff;
padding: 20px;
border-radius: 8px;
gap: 16px;
gap : 16px;
}

.login-container h2, .waiting-container h2 {
Expand All @@ -43,7 +50,7 @@
.login-container p, .waiting-container p {
font-size: 1.25em;
font-weight: 500;
margin-top: 12px;
margin-bottom: 12px;
color: #333;
}

Expand Down
9 changes: 6 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Calendar from './pages/Calendar/Calendar';
import Inscription from './components/Inscription/Inscription';
import Connexion from './components/Connexion/Connexion';
import { setCardScanCallback } from './services/cardScanListener';
import { getApiUrl } from './services/api';
import Bouton from './components/Bouton/Bouton';

type AppState = 'waiting' | 'login' | 'inscription' | 'calendar' | 'cardNotFound' | 'linkLogin';
Expand All @@ -28,7 +29,7 @@ function App() {
useEffect(() => {
const checkExistingCard = async (id: string) => {
try {
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8000';
const apiUrl = getApiUrl();
const response = await fetch(`${apiUrl}/check-card/${id}`);
const data = await response.json();
if (data.exists) {
Expand Down Expand Up @@ -118,8 +119,10 @@ function App() {
setAppState('calendar');
}}
/>
<p>Vous n&apos;êtes pas encore inscrit ?</p>
<Bouton onClick={() => setAppState('inscription')} label="S'inscrire" />
<div className='inscription-container'>
<p>Vous n&apos;êtes pas encore inscrit ?</p>
<Bouton onClick={() => setAppState('inscription')} label="S'inscrire" />
</div>
</div>
</>
);
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/Connexion/Connexion.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
width: min(640px, 100%);
width: 100%;
}

.connexion_form .form_email {
Expand Down Expand Up @@ -56,7 +56,6 @@

@media (max-width: 768px) {
.connexion_container {
width: calc(100vw - 48px);
padding: 24px;
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Connexion/Connexion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const Connexion = ({ onLoginSuccess, scannedCardId, onBack }: ConnexionProps) =>

const data = await response.json();

if (!response.ok || !data?.authenticated || !data?.card_id) {
if (!response.ok || !data?.authenticated) {
throw new Error(data?.detail || "Identifiants invalides");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ const CreateCustomEvent = ({ openModal, setOpenModal, currentUser, onEventSave }
duration: String(end.getTime() - start.getTime()),
color,
user: currentUser.first_name + " " + currentUser.last_name,
id_card: currentUser.id_card || "",
email: currentUser.email,
id_card: currentUser.id_card,
accepted: false,
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Inscription/Inscription.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
.inscription_form {
display: grid;
grid-template-columns: 1fr 1fr;
width: 100%;
gap: 20px;
}

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/Inscription/Inscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const Inscription = ({card_id}: InscriptionInterface) => {
setFormError("");

if (!isSchoolEmail(email)) {
setEmailError("L'email doit se terminer par @devinci.fr ou @edu.vinci.fr.");
setEmailError("L'email doit se terminer par @devinci.fr ou @edu.devinci.fr.");
return;
}

Expand Down Expand Up @@ -78,8 +78,8 @@ const Inscription = ({card_id}: InscriptionInterface) => {
return;
}

if (error instanceof Error && error.message.includes("@devinci.fr ou @edu.vinci.fr")) {
setEmailError("L'email doit se terminer par @devinci.fr ou @edu.vinci.fr.");
if (error instanceof Error && error.message.includes("@devinci.fr ou @edu.devinci.fr")) {
setEmailError("L'email doit se terminer par @devinci.fr ou @edu.devinci.fr.");
return;
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/useCalendarApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const useCalendarApi = () => {
borderColor: displayColor,
extendedProps: {
user: event.user,
email: event.email,
duration: event.duration,
id_card: event.id_card,
accepted: event.accepted
Expand Down
Loading
Loading