Compare commits

..

31 Commits
1.5.6 ... 1.7.0

Author SHA1 Message Date
45d4ac30bd Merge pull request 'add interested event' (#66) from feature/interested-events into master
Reviewed-on: #66
2025-08-20 20:33:34 +00:00
5f24657956 add interested event 2025-08-20 22:27:30 +02:00
46b1f80b53 Merge pull request 'feature/interested-events' (#65) from feature/interested-events into master
Reviewed-on: #65
2025-08-18 21:59:48 +00:00
e5510cc390 add list interested 2025-08-18 23:59:05 +02:00
1470d8976e fix save event interested 2025-08-18 23:57:05 +02:00
5e462861e2 Merge pull request 'fix event interested' (#64) from feature/interested-events into master
Reviewed-on: #64
2025-08-18 21:48:32 +00:00
bf47256519 fix event interested 2025-08-18 23:46:56 +02:00
f99b927574 Merge pull request 'add endpoint toogle interested + add data interested for events' (#63) from feature/interested-events into master
Reviewed-on: #63
2025-08-17 13:22:50 +00:00
4a4be39ad3 add endpoint toogle interested + add data interested for events 2025-08-17 14:47:17 +02:00
96aeb56195 Merge pull request 'add ticket and link' (#62) from feature/add-link-ticket into master
Reviewed-on: #62
2025-08-15 20:34:04 +00:00
79a24d23af add ticket and link 2025-08-15 22:18:47 +02:00
1dd2fed95f Merge pull request 'sort by date desc' (#61) from feature/add-sort-date into master
Reviewed-on: #61
2025-06-20 22:02:23 +00:00
b7ae697ea6 sort by date desc 2025-06-20 23:58:14 +02:00
03036b2d3b Merge pull request 'fix skip' (#60) from feature/pagination into master
Reviewed-on: #60
2025-03-16 19:52:03 +00:00
e07a74384f fix skip 2025-03-14 23:05:55 +01:00
32b6fdacb6 Merge pull request 'fix form' (#59) from feature/persist-token into master
Reviewed-on: #59
2025-03-06 22:32:19 +00:00
2baaf3c126 fix form 2025-03-06 23:31:37 +01:00
86a3d05f7e Merge pull request 'fix form remember_me' (#58) from feature/persist-token into master
Reviewed-on: #58
2025-03-06 22:23:42 +00:00
9fac430654 fix form remember_me 2025-03-06 23:22:38 +01:00
f56eb9db92 Merge pull request 'fix if' (#57) from feature/persist-token into master
Reviewed-on: #57
2025-03-06 22:13:04 +00:00
15062c029f fix if 2025-03-06 23:12:28 +01:00
6c51c7469b Merge pull request 'add persist token' (#56) from feature/persist-token into master
Reviewed-on: #56
2025-03-06 21:46:00 +00:00
952b0211ba add persist token 2025-03-06 22:42:26 +01:00
ece35338da Merge pull request 'rollback' (#55) from feature/oauth into master
Reviewed-on: #55
2025-03-06 21:01:56 +00:00
221bd1e244 rollback 2025-03-06 22:00:52 +01:00
de60dee3eb Merge pull request 'feature/oauth' (#54) from feature/oauth into master
Reviewed-on: #54
2025-03-01 20:49:13 +00:00
4669774cc3 add env facebook 2025-03-01 21:45:10 +01:00
a094b56d44 add google client id 2025-03-01 14:02:11 +01:00
3e514acb19 add oauth wip 2025-02-24 20:45:52 +01:00
a34ba04f78 Merge pull request 'fix password update' (#53) from feature/passwordForgot into master
Reviewed-on: #53
2025-02-15 11:09:12 +00:00
8f3f2d0f98 fix password update 2025-02-15 12:07:58 +01:00
5 changed files with 109 additions and 23 deletions

View File

@@ -12,11 +12,20 @@ from ..dependencies import database, cookie
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = cookie.OAuth2PasswordBearerWithCookie(tokenUrl="token") oauth2_scheme = cookie.OAuth2PasswordBearerWithCookie(tokenUrl="token")
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password, hashed_password): def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password) return pwd_context.verify(plain_password, hashed_password)

View File

@@ -1,24 +1,34 @@
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from pydantic_mongo import AbstractRepository, ObjectIdField from pydantic_mongo import AbstractRepository, ObjectIdField
from datetime import datetime, date from datetime import datetime, date
from bson import ObjectId
from typing import List, Optional
class Event(BaseModel): class Event(BaseModel):
id: ObjectIdField = None id: ObjectIdField = None
name: str name: str
place: str place: str
description: str description: str
imgUrl: str | None = None imgUrl: Optional[str] = None
status: int = 0 status: int = 0
latitude: float = 0.0 latitude: float = 0.0
longitude: float = 0.0 longitude: float = 0.0
organizers: list[str] = [] link: Optional[str] = None
tags: list[str] = [] ticket: Optional[str] = None
start_date: datetime | None = None organizers: List[str] = []
end_date: datetime | None = None tags: List[str] = []
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
created_at: datetime = datetime.today() created_at: datetime = datetime.today()
updated_at: datetime | None = None updated_at: Optional[datetime] = None
deleted_at: datetime | None = None deleted_at: Optional[datetime] = None
disabled_at: datetime | None = None disabled_at: Optional[datetime] = None
interested_users: List[ObjectId] = []
model_config = {
"arbitrary_types_allowed": True
}
class EventOut(BaseModel): class EventOut(BaseModel):
id: ObjectIdField = None id: ObjectIdField = None
@@ -26,11 +36,17 @@ class EventOut(BaseModel):
place: str place: str
description: str description: str
imgUrl: str | None = None imgUrl: str | None = None
link: str | None = None
ticket: str | None = None
status: int = 0 status: int = 0
start_date: datetime | None = None start_date: datetime | None = None
end_date: datetime | None = None end_date: datetime | None = None
tags: list[str] = [] tags: list[str] = []
class EventOutWithInterested(EventOut):
interested: bool = False
interested_count: int = 0
class EventIn(BaseModel): class EventIn(BaseModel):
name: str name: str
place: str place: str
@@ -39,6 +55,8 @@ class EventIn(BaseModel):
status: int = 0 status: int = 0
organizers: list[str] = [] organizers: list[str] = []
tags: list[str] = [] tags: list[str] = []
link: str | None = None
ticket: str | None = None
start_date: datetime | None = None start_date: datetime | None = None
end_date: datetime | None = None end_date: datetime | None = None
latitude: float = 0.0 latitude: float = 0.0

View File

@@ -66,6 +66,7 @@ def build_text_filter(item):
@router.get("/events", tags=["events"], response_model=list[events.EventOut]) @router.get("/events", tags=["events"], response_model=list[events.EventOut])
async def read_events( async def read_events(
authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))],
user: users.User = Depends(users_token.get_current_user), # 👈 récupère lutilisateur courant
skip: int = 0, skip: int = 0,
limit: int = 20, limit: int = 20,
id_event: str | None = None, id_event: str | None = None,
@@ -84,8 +85,7 @@ async def read_events(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="`skip` should be >= 0 and `limit` should be > 0 and greater than `skip`.", detail="`skip` should be >= 0 and `limit` should be > 0 and greater than `skip`.",
) )
limit = limit + skip skip = limit * skip
# Initialize filters # Initialize filters
filters = [] filters = []
@@ -127,8 +127,10 @@ async def read_events(
# Fetch and return results # Fetch and return results
event_repository = events.EventRepository(database=database.database) event_repository = events.EventRepository(database=database.database)
list_events = [] list_events = []
for event_index in event_repository.find_by(object_search, limit=limit, skip=skip): for event_index in event_repository.find_by(object_search, limit=limit, skip=skip, sort=[("start_date", 1)]):
event = events.EventOut( interested_users = event_index.interested_users
is_interested = ObjectId(user.id) in interested_users
event = events.EventOutWithInterested(
id=event_index.id, id=event_index.id,
tags=event_index.tags, tags=event_index.tags,
imgUrl=event_index.imgUrl, imgUrl=event_index.imgUrl,
@@ -138,6 +140,10 @@ async def read_events(
status=event_index.status, status=event_index.status,
start_date=event_index.start_date, start_date=event_index.start_date,
end_date=event_index.end_date, end_date=event_index.end_date,
ticket=event_index.ticket,
link=event_index.link,
interested=is_interested,
interested_count=len(interested_users)
) )
list_events.append(event) list_events.append(event)
@@ -147,6 +153,7 @@ async def read_events(
@router.get("/events/search", tags=["events"], response_model=list[events.EventOut]) @router.get("/events/search", tags=["events"], response_model=list[events.EventOut])
async def search_events( async def search_events(
authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))],
user: users.User = Depends(users_token.get_current_user), # 👈 récupère lutilisateur courant
skip: int = 0, skip: int = 0,
limit: int = 20, limit: int = 20,
item: Union[str, None] = None, item: Union[str, None] = None,
@@ -166,7 +173,7 @@ async def search_events(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="`skip` should be >= 0 and `limit` should be > 0 and greater than `skip`.", detail="`skip` should be >= 0 and `limit` should be > 0 and greater than `skip`.",
) )
limit = limit + skip skip = limit * skip
# Initialize filters # Initialize filters
filters = [{"status": {"$eq": status}}] filters = [{"status": {"$eq": status}}]
@@ -201,8 +208,10 @@ async def search_events(
# Fetch and return results # Fetch and return results
event_repository = events.EventRepository(database=database.database) event_repository = events.EventRepository(database=database.database)
list_events = [] list_events = []
for event_index in event_repository.find_by(object_search, limit=limit, skip=skip): for event_index in event_repository.find_by(object_search, limit=limit, skip=skip, sort=[("start_date", 1)]):
event = events.EventOut( interested_users = event_index.interested_users
is_interested = ObjectId(user.id) in interested_users
event = events.EventOutWithInterested(
id=event_index.id, id=event_index.id,
tags=event_index.tags, tags=event_index.tags,
imgUrl=event_index.imgUrl, imgUrl=event_index.imgUrl,
@@ -212,6 +221,10 @@ async def search_events(
status=event_index.status, status=event_index.status,
start_date=event_index.start_date, start_date=event_index.start_date,
end_date=event_index.end_date, end_date=event_index.end_date,
link=event_index.link,
ticket=event_index.ticket,
interested=is_interested,
interested_count=len(interested_users)
) )
list_events.append(event) list_events.append(event)
@@ -225,7 +238,7 @@ async def read_users_me(current_user: Annotated[users.User, Depends(users_token.
listOrganizers = [] listOrganizers = []
for event_index in event_repository.find_by({"organizers":{"$eq": current_user.username}}, limit=limit, skip=skip): for event_index in event_repository.find_by({"organizers":{"$eq": current_user.username}}, limit=limit, skip=skip):
event = events.EventOut(id=event_index.id, name=event_index.name, tags=event_index.tags, imgUrl=event_index.imgUrl, description=event_index.description, place=event_index.place, status=event_index.status, start_date=event_index.start_date, end_date=event_index.end_date) event = events.EventOut(id=event_index.id, name=event_index.name, ticket=event_index.ticket, link=event_index.link, tags=event_index.tags, imgUrl=event_index.imgUrl, description=event_index.description, place=event_index.place, status=event_index.status, start_date=event_index.start_date, end_date=event_index.end_date)
listOrganizers.append(event) listOrganizers.append(event)
content = {"organizers":listOrganizers} content = {"organizers":listOrganizers}
@@ -315,6 +328,8 @@ async def update_events_me(item_id: str, current_user: Annotated[users.User, Dep
event.end_date = eventSingle.end_date event.end_date = eventSingle.end_date
event.latitude = eventSingle.latitude event.latitude = eventSingle.latitude
event.longitude = eventSingle.longitude event.longitude = eventSingle.longitude
event.link = eventSingle.link
event.ticket = eventSingle.ticket
event.updated_at = datetime.today() event.updated_at = datetime.today()
event.imgUrl = eventSingle.imgUrl event.imgUrl = eventSingle.imgUrl
event_repository.save(event) event_repository.save(event)
@@ -350,6 +365,8 @@ async def update_events(authorize: Annotated[bool, Depends(permissions_checker.P
event.latitude = eventSingle.latitude event.latitude = eventSingle.latitude
event.longitude = eventSingle.longitude event.longitude = eventSingle.longitude
event.imgUrl = eventSingle.imgUrl event.imgUrl = eventSingle.imgUrl
event.link = eventSingle.link
event.ticket = eventSingle.ticket
event.tags = eventSingle.tags event.tags = eventSingle.tags
for tag_name in eventSingle.tags: for tag_name in eventSingle.tags:
tag = tags_repository.find_one_by({"name": {'$eq': tag_name}}) tag = tags_repository.find_one_by({"name": {'$eq': tag_name}})
@@ -359,7 +376,7 @@ async def update_events(authorize: Annotated[bool, Depends(permissions_checker.P
event.status = 1 event.status = 1
event.created_at = datetime.today() event.created_at = datetime.today()
event_repository.save(event) event_repository.save(event)
content = {"message": "event is created"} content = {"message": "event created"}
response = JSONResponse(content=content, status_code=status.HTTP_201_CREATED) response = JSONResponse(content=content, status_code=status.HTTP_201_CREATED)
return response return response
@@ -386,6 +403,8 @@ async def update_events_id(item_id: str, authorize: Annotated[bool, Depends(perm
event.start_date = eventSingle.start_date event.start_date = eventSingle.start_date
event.end_date = eventSingle.end_date event.end_date = eventSingle.end_date
event.organizers = eventSingle.organizers event.organizers = eventSingle.organizers
event.link = eventSingle.link
event.ticket = eventSingle.ticket
event.tags = eventSingle.tags event.tags = eventSingle.tags
for tag_name in eventSingle: for tag_name in eventSingle:
tag = tags_repository.find_one_by({"name": {'$eq': tag_name}}) tag = tags_repository.find_one_by({"name": {'$eq': tag_name}})
@@ -433,4 +452,27 @@ async def patch_events_id(item_id : str, authorize: Annotated[bool, Depends(perm
event_repository.save(event) event_repository.save(event)
content = {"message": "event is enabled"} content = {"message": "event is enabled"}
response = JSONResponse(content=content) response = JSONResponse(content=content)
return response return response
@router.post("/events/{event_id}/interest", tags=["events"])
async def toggle_interest(event_id: str, user: users.User = Depends(users_token.get_current_user)):
event_repository = events.EventRepository(database=database.database)
event = event_repository.find_one_by_id(ObjectId(event_id))
if not event:
raise HTTPException(status_code=404, detail="Event not found")
interested_users = event.interested_users
user_obj_id = ObjectId(user.id)
if user_obj_id in interested_users:
# Retirer lutilisateur
interested_users.remove(user_obj_id)
else:
# Ajouter lutilisateur
interested_users.append(user_obj_id)
event.interested_users = interested_users
event_repository.save(event)
return {"event_id": event_id, "interested": user_obj_id in interested_users, "interested_count": len(interested_users)}

View File

@@ -95,12 +95,25 @@ async def reset_password(request: Request, key: str | None = None, email: str |
@router.post("/password/update", tags=["password"]) @router.post("/password/update", tags=["password"])
async def update_password(request: Request, email: str = Form(...), key: str = Form(...), new_password: str = Form(...)): # Vérification du token dans Redis async def update_password(request: Request, email: str = Form(...), key: str = Form(...), new_password: str = Form(...)): # Vérification du token dans Redis
# Récupérer la clé hachée depuis Redis
key_hashed = database.connect_redis.get(email) key_hashed = database.connect_redis.get(email)
if key_hashed is None or key_hashed.decode() != key: if key_hashed is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Key is invalid or expired" detail="Invalid or expired reset key"
)
# Redis stocke les valeurs en `bytes`, donc il faut décoder si nécessaire
if isinstance(key_hashed, bytes):
key_hashed = key_hashed.decode()
# Vérifier que la clé en clair correspond au hash stocké
if not bcrypt.checkpw(key.encode(), key_hashed.encode()):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid reset key"
) )
# Recherche de l'utilisateur dans la base de données # Recherche de l'utilisateur dans la base de données

View File

@@ -1,7 +1,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Annotated from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status, APIRouter from fastapi import Depends, FastAPI, HTTPException, status, APIRouter, Form
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from ..dependencies import users_token, permissions_checker from ..dependencies import users_token, permissions_checker
@@ -13,15 +13,19 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30
@router.post("/token", tags=["token"]) @router.post("/token", tags=["token"])
async def login_for_access_token( async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
remember_me: bool = Form(False)):
user = users_token.authenticate_user(form_data.username, form_data.password) user = users_token.authenticate_user(form_data.username, form_data.password)
expires_access_token_time = ACCESS_TOKEN_EXPIRE_MINUTES
if remember_me:
expires_access_token_time=120
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=expires_access_token_time)
access_token = users_token.create_access_token( access_token = users_token.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires data={"sub": user.username}, expires_delta=access_token_expires
) )