diff --git a/api/v1/auth.py b/api/v1/auth.py index 531e268..5f36c4c 100644 --- a/api/v1/auth.py +++ b/api/v1/auth.py @@ -3,7 +3,7 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from fastapi import APIRouter, Depends, HTTPException, status, Body from fastapi.responses import JSONResponse -from fastapi.security import OAuth2PasswordRequestForm +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy import update, select from services.auth_service import AuthService from models.schemas import Token, UserCreate, UserResponse @@ -17,6 +17,7 @@ from utils.logging import logger from utils.security import verify_password, get_password_hash, pwd_context router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") @router.post("/signup", response_model=UserResponse, status_code=201, summary="User Signup") async def signup(user: UserCreate, db=Depends(get_db)): @@ -42,8 +43,8 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( return {"access_token": access_token, "token_type": "bearer"} @router.get("/me", response_model=UserResponse, summary="Get current user") -async def read_users_me(current_user: UserResponse = Depends(AuthService.get_current_user)): - return current_user +async def read_users_me(token:str = Depends(oauth2_scheme) , db=Depends(get_db)): + return await AuthService.get_current_user(token, db) @router.post("/reset-password") async def reset_password(token: str = Body(...), new_password: str = Body(...), db: AsyncSession = Depends(get_db)): diff --git a/api/v1/person_reports.py b/api/v1/person_reports.py index 0c596ff..1d10718 100644 --- a/api/v1/person_reports.py +++ b/api/v1/person_reports.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile +from fastapi.security import OAuth2PasswordBearer from services.person_report_service import PersonReportService from models.schemas import PersonReportCreate, PersonReportUpdate, PersonReportResponse, UserResponse from config.database import get_db @@ -6,10 +7,11 @@ from typing import Optional from services.auth_service import AuthService router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") @router.post("/", response_model=PersonReportResponse, status_code=201) -async def create_report(report: PersonReportCreate, db=Depends(get_db)): - return await PersonReportService.create_report(report, db) +async def create_report(report: PersonReportCreate, image_file: UploadFile = File(None), db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await PersonReportService.create_report(report, db, image_file, token) @router.put("/{report_id}", response_model=PersonReportResponse) async def update_report(report_id: int, report: PersonReportUpdate, db=Depends(get_db)): diff --git a/api/v1/points_of_interest.py b/api/v1/points_of_interest.py index 98e1cd8..f24672f 100644 --- a/api/v1/points_of_interest.py +++ b/api/v1/points_of_interest.py @@ -1,14 +1,15 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer from services.points_of_interest_service import PointsOfInterestService -from models.schemas import PointOfInterest +from models.schemas import CreatePointOfInterest, UpdatePointOfInterest from config.database import get_db router = APIRouter() - +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") @router.post("/", status_code=201) -async def create_point(point: PointOfInterest, db=Depends(get_db)): - return await PointsOfInterestService.create_point_of_interest(point, db) +async def create_point(point: CreatePointOfInterest, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await PointsOfInterestService.create_point_of_interest(point, db, token) @router.get("/{point_id}") @@ -22,10 +23,10 @@ async def get_all_points(db=Depends(get_db)): @router.put("/{point_id}") -async def update_point(point_id: int, data: dict, db=Depends(get_db)): - return await PointsOfInterestService.update_point_of_interest(point_id, data, db) +async def update_point(point_id: int, point: UpdatePointOfInterest, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await PointsOfInterestService.update_point_of_interest(point_id, point, db, token) @router.delete("/{point_id}") -async def delete_point(point_id: int, db=Depends(get_db)): - return await PointsOfInterestService.delete_point_of_interest(point_id, db) +async def delete_point(point_id: int, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await PointsOfInterestService.delete_point_of_interest(point_id, db, token) diff --git a/api/v1/shelters.py b/api/v1/shelters.py index 2f8d16f..fea9436 100644 --- a/api/v1/shelters.py +++ b/api/v1/shelters.py @@ -1,14 +1,15 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer from services.shelter_service import ShelterService -from models.schemas import Shelter +from models.schemas import CreateShelter, UpdateShelter from config.database import get_db router = APIRouter() - +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") @router.post("/", status_code=201) -async def create_shelter(shelter: Shelter, db=Depends(get_db)): - return await ShelterService.create_shelter(shelter, db) +async def create_shelter(shelter: CreateShelter, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await ShelterService.create_shelter(shelter, db, token) @router.get("/{shelter_id}") @@ -22,10 +23,10 @@ async def get_all_shelters(db=Depends(get_db)): @router.put("/{shelter_id}") -async def update_shelter(shelter_id: int, data: dict, db=Depends(get_db)): - return await ShelterService.update_shelter(shelter_id, data, db) +async def update_shelter(shelter_id: int, shelter: UpdateShelter, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await ShelterService.update_shelter(shelter_id, shelter, db, token) @router.delete("/{shelter_id}") -async def delete_shelter(shelter_id: int, db=Depends(get_db)): - return await ShelterService.delete_shelter(shelter_id, db) +async def delete_shelter(shelter_id: int, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await ShelterService.delete_shelter(shelter_id, db, token) diff --git a/api/v1/technical_issues.py b/api/v1/technical_issues.py index 0799496..9d1417b 100644 --- a/api/v1/technical_issues.py +++ b/api/v1/technical_issues.py @@ -1,26 +1,29 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from services.auth_service import AuthService from services.technical_issue_service import TechnicalIssueService -from models.schemas import TechnicalIssue, UpdateTechnicalIssue +from models.schemas import CreateTechnicalIssue, UpdateTechnicalIssue from config.database import get_db router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") @router.post("/", status_code=201) -async def create_issue(issue: TechnicalIssue, db=Depends(get_db)): - return await TechnicalIssueService.create_issue(issue, db) +async def create_issue(issue: CreateTechnicalIssue, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await TechnicalIssueService.create_issue(issue, db, token) @router.get("/{issue_id}") -async def get_issue(issue_id: int, db=Depends(get_db)): - return await TechnicalIssueService.get_issue(issue_id, db) +async def get_issue(issue_id: int, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await TechnicalIssueService.get_issue(issue_id, db, token) @router.get("/") -async def get_all_issues(db=Depends(get_db)): - return await TechnicalIssueService.get_all_issues(db) +async def get_all_issues(db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await TechnicalIssueService.get_all_issues(db, token) @router.put("/{issue_id}") -async def update_issue(issue_id: int, issue_data: UpdateTechnicalIssue, db=Depends(get_db)): - return await TechnicalIssueService.update_issue(issue_id, issue_data, db) +async def update_issue(issue_id: int, issue_data: UpdateTechnicalIssue, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await TechnicalIssueService.update_issue(issue_id, issue_data, db, token) @router.delete("/{issue_id}") -async def delete_issue(issue_id: int, db=Depends(get_db)): - return await TechnicalIssueService.delete_issue(issue_id, db) +async def delete_issue(issue_id: int, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await TechnicalIssueService.delete_issue(issue_id, db, token) diff --git a/api/v1/uploads.py b/api/v1/uploads.py index 5126e8a..2a53b43 100644 --- a/api/v1/uploads.py +++ b/api/v1/uploads.py @@ -1,28 +1,16 @@ # app/api/v1/upload.py -from fastapi import APIRouter, UploadFile, File, HTTPException +from fastapi import APIRouter, Depends, UploadFile, File, HTTPException +from fastapi.security import OAuth2PasswordBearer +from config.database import get_db +from services.auth_service import AuthService from services.upload_service import UploadService -import uuid +from utils.logging import logger router = APIRouter() - -# Instancier UploadService avec le nom de ton bucket S3 -upload_service = UploadService(bucket_name="ton-nom-de-bucket") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") @router.post("/") -async def upload_file(file: UploadFile = File(...)): - # Générer un nom de fichier unique - file_extension = file.filename.split(".")[-1] # Récupérer l'extension du fichier - unique_filename = f"{uuid.uuid4()}.{file_extension}" # Générer un nom unique - - try: - # Lire le contenu du fichier de manière asynchron - file_content = await file.read() - - # Uploader le fichier sur S3 - file_url = upload_service.upload_file(file_content, unique_filename) - - # Retourner l'URL du fichier uploadé - return {"file_url": file_url} - except Exception as e: - # Gérer les erreurs - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file +async def upload_file(file: UploadFile, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + user = await AuthService.get_current_user(token, db) + logger.info(f"User {user} is uploading a file") + return await UploadService.upload_image_to_s3(file, user["id"]) \ No newline at end of file diff --git a/api/v1/users.py b/api/v1/users.py index 6e6e429..b067810 100644 --- a/api/v1/users.py +++ b/api/v1/users.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer from sqlalchemy.ext.asyncio import AsyncSession from services.user_service import UserService from models.schemas import UserUpdateRole, UserBlockBan, UserResponse @@ -7,27 +8,28 @@ from services.auth_service import AuthService from typing import Optional router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") @router.get("/", response_model=list[UserResponse]) -async def list_users(status: Optional[str] = None, db=Depends(get_db)): - return await UserService.list_users(status, db) +async def list_users(status: Optional[str] = None, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await UserService.list_users(token, status, db) -@router.patch("/role", status_code=200, dependencies=[Depends(AuthService.admin_required)]) -async def change_role(user_update: UserUpdateRole, db: AsyncSession = Depends(get_db)): - return await UserService.change_user_role(user_update, db) +@router.patch("/role", status_code=200) +async def change_role(user_update: UserUpdateRole, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme)): + return await UserService.change_user_role(user_update, db, token) -@router.post("/block", status_code=200, dependencies=[Depends(AuthService.admin_required)]) -async def block_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db)): - return await UserService.block_user(user_action, db) +@router.post("/block", status_code=200) +async def block_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme)): + return await UserService.block_user(user_action, db, token) -@router.post("/ban", status_code=200, dependencies=[Depends(AuthService.admin_required)]) -async def ban_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db)): - return await UserService.ban_user(user_action, db) +@router.post("/ban", status_code=200) +async def ban_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme)): + return await UserService.ban_user(user_action, db, token) -@router.post("/unblock", status_code=200, dependencies=[Depends(AuthService.admin_required)]) -async def unblock_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db)): - return await UserService.unblock_user(user_action, db) +@router.post("/unblock", status_code=200) +async def unblock_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme)): + return await UserService.unblock_user(user_action, db, token) -@router.post("/unban", status_code=200, dependencies=[Depends(AuthService.admin_required)]) -async def unban_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db)): - return await UserService.unban_user(user_action, db) \ No newline at end of file +@router.post("/unban", status_code=200) +async def unban_user(user_action: UserBlockBan, db: AsyncSession = Depends(get_db), token: str = Depends(oauth2_scheme)): + return await UserService.unban_user(user_action, db, token) \ No newline at end of file diff --git a/models/db.py b/models/db.py index 749e3ac..f6f4ec3 100644 --- a/models/db.py +++ b/models/db.py @@ -1,5 +1,5 @@ from sqlalchemy import Table, Column, Integer, String, DateTime, Boolean, MetaData, ForeignKey, Text -from datetime import datetime +from datetime import datetime, timezone metadata = MetaData() @@ -100,6 +100,8 @@ shelters_table = Table( Column("description", Text), Column("status", String(50), default="available"), Column("contact_person", String(255)), + Column('contact_email', String(255)), + Column('contact_phone', String(20)), Column("gps_coordinates", String(100), nullable=False), Column("added_by", Integer, ForeignKey("users.id"), nullable=False) ) @@ -115,6 +117,6 @@ person_reports_table = Table( Column("gps_coordinates", String(100)), Column("photo_url", String(255)), Column("reporter_email", String(255), ForeignKey("users.email"), nullable=False), - Column("created_at", DateTime, default=datetime.utcnow), - Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + Column("created_at", DateTime, default=datetime.now(timezone.utc)), + Column("updated_at", DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) ) \ No newline at end of file diff --git a/models/schemas.py b/models/schemas.py index e189b0d..41886e2 100644 --- a/models/schemas.py +++ b/models/schemas.py @@ -122,9 +122,12 @@ class TechnicalIssue(BaseModel): description: str status: str +class CreateTechnicalIssue(BaseModel): + description: str + status: str + class UpdateTechnicalIssue(BaseModel): status: Optional[str] = None - description: Optional[str] = None class PersonReportBase(BaseModel): full_name: str @@ -135,8 +138,12 @@ class PersonReportBase(BaseModel): photo_url: Optional[str] = None reporter_email: str -class PersonReportCreate(PersonReportBase): - pass +class PersonReportCreate(BaseModel): + full_name: str + date_of_birth: datetime + status: str + location: Optional[str] = None + gps_coordinates: Optional[str] = None class PersonReportUpdate(BaseModel): status: Optional[str] = None @@ -159,6 +166,19 @@ class PointOfInterest(BaseModel): gps_coordinates: str added_by: int # ID de l'utilisateur qui a ajouté ce point +class CreatePointOfInterest(BaseModel): + label: str + description: Optional[str] = None + icon: Optional[str] = None # URL de l'icône + organization: Optional[str] = None + gps_coordinates: str + +class UpdatePointOfInterest(BaseModel): + description: Optional[str] = None + icon: Optional[str] = None # URL de l'icône + organization: Optional[str] = None + gps_coordinates: Optional[str] = None + class Shelter(BaseModel): label: str description: Optional[str] = None @@ -167,6 +187,24 @@ class Shelter(BaseModel): gps_coordinates: str added_by: int # ID de l'utilisateur qui a ajouté cet abri +class CreateShelter(BaseModel): + label: str + description: Optional[str] = None + status: str # "available", "full", "closed" + contact_person: Optional[str] = None + contact_email: Optional[str] = None + contact_phone: Optional[str] = None + gps_coordinates: str + +class UpdateShelter(BaseModel): + label: Optional[str] = None + description: Optional[str] = None + status: Optional[str] = None # "available", "full", "closed" + contact_person: Optional[str] = None + contact_email: Optional[str] = None + contact_phone: Optional[str] = None + gps_coordinates: Optional[str] = None + class PersonReportResponse(PersonReportBase): id: int created_at: datetime diff --git a/services/auth_service.py b/services/auth_service.py index d9a7c92..2c03d05 100644 --- a/services/auth_service.py +++ b/services/auth_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Optional from jose import JWTError, jwt from passlib.context import CryptContext @@ -19,9 +19,10 @@ logger.info("Test log message") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # Configuration pour OAuth2 -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") + class AuthService: + @staticmethod def verify_password(plain_password: str, hashed_password: str) -> bool: passEncr = pwd_context.hash(plain_password) @@ -58,14 +59,14 @@ class AuthService: def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) to_encode.update({"exp": expire}) return jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm) @staticmethod - async def get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_db)): + async def get_current_user(token: str, db): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", @@ -147,7 +148,7 @@ class AuthService: @staticmethod - async def admin_required(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): + async def admin_required(token: str, db): credentials_exception = HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You do not have permission to perform this action.", @@ -177,7 +178,7 @@ class AuthService: @staticmethod - async def update_user(user_id: int, updates: dict, token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): + async def update_user(user_id: int, updates: dict, token: str, db): """ Met à jour les informations d'un utilisateur. - Un utilisateur peut mettre à jour ses propres informations. @@ -240,7 +241,35 @@ class AuthService: return UserResponse(**updated_user) @staticmethod - async def get_user_by_email(email: str, db: AsyncSession): + async def get_user_by_email(email: str, db): query = select(users_table).where(users_table.c.email == email) result = await db.execute(query) - return result.mappings().first() \ No newline at end of file + return result.mappings().first() + + @staticmethod + async def check_permissions(token: str, db, required_permissions: list): + credentials_exception = HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have permission to perform this action.", + ) + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) + email: str = payload.get("sub") + if email is None: + raise credentials_exception + token_data = TokenData(email=email) + except JWTError: + logger.error("could not decode token", extra={"token": token}) + raise credentials_exception + + query = users_table.select().where(users_table.c.email == token_data.email) + result = await db.execute(query) + user = result.mappings().first() + if user is None: + raise credentials_exception + + if user["role"] not in required_permissions: + logger.error("permission denied", extra={"email": token_data.email, "required_permissions": required_permissions}) + raise credentials_exception + + logger.info("permission granted", extra={"email": token_data.email, "role": user["role"]}) \ No newline at end of file diff --git a/services/need_request_service.py b/services/need_request_service.py index b165429..22e72e3 100644 --- a/services/need_request_service.py +++ b/services/need_request_service.py @@ -100,9 +100,11 @@ class NeedRequestService: # D�codage du token JWT payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) email: str = payload.get("sub") + logger.info("email from payload.get(sub) " +email) if email is None: raise credentials_exception token_data = TokenData(email=email) + logger.info("token_data from TokenData(email=email) " +token_data) except JWTError: raise credentials_exception @@ -111,7 +113,7 @@ class NeedRequestService: result = await db.execute(user_query) user = result.mappings().fetchone() - logger.info("user loooooooooooooooooooo: " +user) + logger.info("user from database: " +user) if user is None: raise credentials_exception diff --git a/services/person_report_service.py b/services/person_report_service.py index 57c27d6..df8e0e2 100644 --- a/services/person_report_service.py +++ b/services/person_report_service.py @@ -7,13 +7,21 @@ from typing import Optional from fastapi import Depends from fastapi.security import OAuth2PasswordBearer +from services.auth_service import AuthService +from services.s3_service import UploadService + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") class PersonReportService: @staticmethod - async def create_report(report: PersonReportCreate, db): - query = person_reports_table.insert().values(**report.model_dump()) + async def create_report(report: PersonReportCreate, db, image_file, token): + user = await AuthService.get_current_user(token, db) + image_url = await UploadService.upload_image_to_s3(image_file, user.email) if image_file else None + + query = person_reports_table.insert().values(**report.model_dump(), + photo_url=image_url, + reporter_email=user["email"]) try: result = await db.execute(query) await db.commit() @@ -53,7 +61,7 @@ class PersonReportService: if status: query = query.where(person_reports_table.c.status == status) result = await db.execute(query) - reports = result.fetchall() + reports = result.mappings().all() return [PersonReportResponse(**report) for report in reports] @staticmethod diff --git a/services/points_of_interest_service.py b/services/points_of_interest_service.py index a749a39..b8e684c 100644 --- a/services/points_of_interest_service.py +++ b/services/points_of_interest_service.py @@ -1,20 +1,23 @@ from sqlalchemy import insert, select, update, delete from fastapi import HTTPException from models.db import points_of_interest_table -from models.schemas import PointOfInterest +from models.schemas import CreatePointOfInterest, PointOfInterest, UpdatePointOfInterest from sqlalchemy.ext.asyncio import AsyncSession +from services.auth_service import AuthService + class PointsOfInterestService: @staticmethod - async def create_point_of_interest(point: PointOfInterest, db: AsyncSession): + async def create_point_of_interest(point: CreatePointOfInterest, db, token): + user = await AuthService.get_current_user(token, db) query = insert(points_of_interest_table).values( label=point.label, description=point.description, icon=point.icon, organization=point.organization, gps_coordinates=point.gps_coordinates, - added_by=point.added_by, + added_by=user["id"], ) try: result = await db.execute(query) @@ -26,26 +29,33 @@ class PointsOfInterestService: raise HTTPException(status_code=500, detail=f"Could not create point of interest: {str(e)}") @staticmethod - async def get_point_of_interest(point_id: int, db: AsyncSession): + async def get_point_of_interest(point_id: int, db): query = select(points_of_interest_table).where(points_of_interest_table.c.id == point_id) result = await db.execute(query) - point = result.fetchone() + point = result.mappings().fetchone() if not point: raise HTTPException(status_code=404, detail="Point of interest not found") return dict(point) @staticmethod - async def get_all_points_of_interest(db: AsyncSession): + async def get_all_points_of_interest(db): query = select(points_of_interest_table) result = await db.execute(query) - return [dict(row) for row in result.fetchall()] + points = result.mappings().all() + return [dict(point) for point in points] @staticmethod - async def update_point_of_interest(point_id: int, data: dict, db: AsyncSession): + async def update_point_of_interest(point_id: int, point: UpdatePointOfInterest, db, token): + user = await AuthService.get_current_user(token, db) + + update_values = point.model_dump(exclude_unset=True) + if not update_values: + raise HTTPException(status_code=400, detail="No fields provided for update") + query = ( update(points_of_interest_table) .where(points_of_interest_table.c.id == point_id) - .values(**data) + .values(**update_values, added_by=user["id"]) ) try: result = await db.execute(query) @@ -57,8 +67,10 @@ class PointsOfInterestService: await db.rollback() raise HTTPException(status_code=500, detail=f"Could not update point of interest: {str(e)}") + @staticmethod - async def delete_point_of_interest(point_id: int, db: AsyncSession): + async def delete_point_of_interest(point_id: int, db, token): + await AuthService.admin_required(token, db) query = delete(points_of_interest_table).where(points_of_interest_table.c.id == point_id) try: result = await db.execute(query) diff --git a/services/report_service.py b/services/report_service.py index 84766ef..1fd17a6 100644 --- a/services/report_service.py +++ b/services/report_service.py @@ -7,8 +7,6 @@ from fastapi import Depends, HTTPException, status from services.auth_service import AuthService -# Configuration pour OAuth2 -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") class ReportService: @staticmethod @@ -29,7 +27,7 @@ class ReportService: raise HTTPException(status_code=500, detail=f"Could not create user report: {str(e)}") @staticmethod - async def get_report_by_id(report_id: int, db, token: str = Depends(oauth2_scheme)): + async def get_report_by_id(report_id: int, db, token: str): await AuthService.admin_required(token, db) query = select(user_reports_table).where(user_reports_table.c.id == report_id) result = await db.execute(query) @@ -39,7 +37,7 @@ class ReportService: return dict(report) @staticmethod - async def get_all_reports(db, token: str = Depends(oauth2_scheme)): + async def get_all_reports(db, token: str): await AuthService.admin_required(token, db) query = select(user_reports_table) @@ -48,7 +46,7 @@ class ReportService: return [dict(report) for report in reports] @staticmethod - async def update_report(report_id: int, report_update: UserReportUpdate, db, token: str = Depends(oauth2_scheme)): + async def update_report(report_id: int, report_update: UserReportUpdate, db, token: str): await AuthService.admin_required(token, db) query = ( update(user_reports_table) diff --git a/services/role_service.py b/services/role_service.py index 68092bf..348d3aa 100644 --- a/services/role_service.py +++ b/services/role_service.py @@ -9,9 +9,6 @@ from jose import jwt, JWTError from sqlalchemy.ext.asyncio import AsyncSession from services.auth_service import AuthService -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") - - class RoleService: @staticmethod async def get_all_roles(db: AsyncSession): @@ -37,7 +34,7 @@ class RoleService: return roles_with_permissions @staticmethod - async def create_role(name: str, db, token: str): + async def create_role(name: str, db: AsyncSession, token: str): """ Crée un nouveau rôle avec les permissions spécifiées (réservé aux administrateurs). """ @@ -126,7 +123,7 @@ class RoleService: return permissions_result @staticmethod - async def get_role(role_id: int, db): + async def get_role(role_id: int, db: AsyncSession): """ Récupère les détails d'un rôle spécifique, y compris ses permissions associées. """ diff --git a/services/shelter_service.py b/services/shelter_service.py index 65af1db..15d02dd 100644 --- a/services/shelter_service.py +++ b/services/shelter_service.py @@ -1,51 +1,64 @@ from sqlalchemy import insert, select, update, delete from fastapi import HTTPException from models.db import shelters_table -from models.schemas import Shelter +from models.schemas import Shelter, UpdateShelter from sqlalchemy.ext.asyncio import AsyncSession +from services.auth_service import AuthService + class ShelterService: + + permissions = ["admin", "moderator"] + @staticmethod - async def create_shelter(shelter: Shelter, db: AsyncSession): + async def create_shelter(shelter: Shelter, db, token): + user = await AuthService.get_current_user(token, db) query = insert(shelters_table).values( label=shelter.label, description=shelter.description, status=shelter.status, contact_person=shelter.contact_person, gps_coordinates=shelter.gps_coordinates, - added_by=shelter.added_by, + added_by=user["id"], ) try: result = await db.execute(query) await db.commit() shelter_id = result.inserted_primary_key[0] - return {"id": shelter_id, **shelter.dict()} + return {"id": shelter_id, **shelter.model_dump()} except Exception as e: await db.rollback() raise HTTPException(status_code=500, detail=f"Could not create shelter: {str(e)}") @staticmethod - async def get_shelter(shelter_id: int, db: AsyncSession): + async def get_shelter(shelter_id: int, db): query = select(shelters_table).where(shelters_table.c.id == shelter_id) result = await db.execute(query) - shelter = result.fetchone() + shelter = result.mappings().fetchone() if not shelter: raise HTTPException(status_code=404, detail="Shelter not found") return dict(shelter) @staticmethod - async def get_all_shelters(db: AsyncSession): + async def get_all_shelters(db): query = select(shelters_table) result = await db.execute(query) - return [dict(row) for row in result.fetchall()] + shelters = result.mappings().fetchall() + return [dict(shelter) for shelter in shelters] @staticmethod - async def update_shelter(shelter_id: int, data: dict, db: AsyncSession): + async def update_shelter(shelter_id: int, shelter: UpdateShelter, db, token): + user = await AuthService.get_current_user(token, db) + update_values = shelter.model_dump(exclude_unset=True) + if not update_values: + raise HTTPException(status_code=400, detail="No fields provided for update") + query = ( update(shelters_table) .where(shelters_table.c.id == shelter_id) - .values(**data) + .values(**update_values, + added_by=user["id"]) ) try: result = await db.execute(query) @@ -58,7 +71,9 @@ class ShelterService: raise HTTPException(status_code=500, detail=f"Could not update shelter: {str(e)}") @staticmethod - async def delete_shelter(shelter_id: int, db: AsyncSession): + async def delete_shelter(shelter_id: int, db, token): + #await AuthService.check_permissions(token, db, ShelterService.permissions) + await AuthService.admin_required(token, db) query = delete(shelters_table).where(shelters_table.c.id == shelter_id) try: result = await db.execute(query) diff --git a/services/technical_issue_service.py b/services/technical_issue_service.py index eb22068..f2dbf7b 100644 --- a/services/technical_issue_service.py +++ b/services/technical_issue_service.py @@ -1,14 +1,16 @@ from sqlalchemy import insert, select, update, delete from fastapi import HTTPException from models.db import technical_issues_table -from models.schemas import TechnicalIssue, UpdateTechnicalIssue +from models.schemas import CreateTechnicalIssue, UpdateTechnicalIssue +from services.auth_service import AuthService class TechnicalIssueService: @staticmethod - async def create_issue(issue: TechnicalIssue, db): + async def create_issue(issue: CreateTechnicalIssue, db, token: str): + user = await AuthService.get_current_user(token, db); query = insert(technical_issues_table).values( - user_id=issue.user_id, + user_id=user["id"], description=issue.description, status=issue.status ) @@ -16,32 +18,36 @@ class TechnicalIssueService: result = await db.execute(query) await db.commit() issue_id = result.inserted_primary_key[0] - return {"id": issue_id, **issue.dict()} + return {"id": issue_id, **issue.model_dump()} except Exception as e: await db.rollback() raise HTTPException(status_code=500, detail=f"Could not create issue: {str(e)}") @staticmethod - async def get_issue(issue_id: int, db): + async def get_issue(issue_id: int, db, token: str): + await AuthService.admin_required(token, db) query = select(technical_issues_table).where(technical_issues_table.c.id == issue_id) result = await db.execute(query) - issue = result.fetchone() + issue = result.mappings().fetchone() if not issue: raise HTTPException(status_code=404, detail="Technical issue not found") return dict(issue) @staticmethod - async def get_all_issues(db): + async def get_all_issues(db, token: str): + await AuthService.admin_required(token, db) query = select(technical_issues_table) result = await db.execute(query) - return [dict(row) for row in result.fetchall()] + technical_issues = result.mappings().all() + return [dict(technical_issue) for technical_issue in technical_issues] @staticmethod - async def update_issue(issue_id: int, issue_data: UpdateTechnicalIssue, db): + async def update_issue(issue_id: int, issue_data: UpdateTechnicalIssue, db, token: str): + await AuthService.admin_required(token, db) query = ( update(technical_issues_table) .where(technical_issues_table.c.id == issue_id) - .values(**issue_data.dict(exclude_unset=True)) + .values(**issue_data.model_dump(exclude_unset=True)) ) try: result = await db.execute(query) @@ -54,7 +60,8 @@ class TechnicalIssueService: raise HTTPException(status_code=500, detail=f"Could not update issue: {str(e)}") @staticmethod - async def delete_issue(issue_id: int, db): + async def delete_issue(issue_id: int, db, token: str): + await AuthService.admin_required(token, db) query = delete(technical_issues_table).where(technical_issues_table.c.id == issue_id) try: result = await db.execute(query) diff --git a/services/upload_service.py b/services/upload_service.py index 6d8db2f..1018e7a 100644 --- a/services/upload_service.py +++ b/services/upload_service.py @@ -1,17 +1,71 @@ # app/services/upload_service.py +from typing import Self +import uuid import boto3 -from botocore.exceptions import NoCredentialsError +from botocore.exceptions import NoCredentialsError, BotoCoreError, ClientError +from fastapi import HTTPException, UploadFile class UploadService: - def __init__(self, bucket_name): - self.s3_client = boto3.client('s3') - self.bucket_name = bucket_name - def upload_file(self, file, file_name): + async def upload_image_to_s3(file: UploadFile, account_id: str) -> dict: + + # Configuration globale + EXTENSIONS_AUTHORIZED = { + "docx": "doc", + "pdf": "doc", + "png": "image", + "jpg": "image", + "jpeg": "image", + "mp4": "video", + } + FILE_PATH_S3 = { + "image": "images/", + "video": "videos/", + "doc": "docs/", + } + + S3_CONFIG = { + "bucket": "sywmtnsg", + "endpoint_url": "https://ht2-storage.n0c.com:5443", + "region_name": "ht2-storage", + "aws_access_key_id": "X89EU8NHL54CIKGMZP8Q", + "aws_secret_access_key": "71RsicRiiSgXDcAZLM4vCpEZESMJ4iA9sOKp0UQy", + } + + # Validate file extension + extension = file.filename.split(".")[-1].lower() + if extension not in EXTENSIONS_AUTHORIZED: + raise HTTPException(status_code=400, detail="Unsupported file extension.") + + # Generate unique file name + new_filename = f"{uuid.uuid4()}.{extension}" + file_type = EXTENSIONS_AUTHORIZED[extension] + storage_path = f"public/{FILE_PATH_S3[file_type]}{account_id}/{new_filename}" + + # Initialize S3 client try: - self.s3_client.upload_fileobj(file, self.bucket_name, file_name) - return f"https://{self.bucket_name}.s3.amazonaws.com/{file_name}" - except NoCredentialsError: - raise Exception("AWS credentials not found") - except Exception as e: - raise e \ No newline at end of file + s3_client = boto3.client( + "s3", + endpoint_url=S3_CONFIG["endpoint_url"], + region_name=S3_CONFIG["region_name"], + aws_access_key_id=S3_CONFIG["aws_access_key_id"], + aws_secret_access_key=S3_CONFIG["aws_secret_access_key"], + ) + + # Upload file + s3_client.upload_fileobj( + file.file, + S3_CONFIG["bucket"], + storage_path, + ExtraArgs={"ACL": "public-read"}, + ) + except (BotoCoreError, ClientError) as e: + raise HTTPException(status_code=500, detail=f"Failed to upload file: {str(e)}") + + return { + "success": True, + "path_with_name": storage_path.replace("public", ""), + "filename": new_filename, + "original_filename": file.filename, + "filetype": extension, + } \ No newline at end of file diff --git a/services/user_service.py b/services/user_service.py index a674a03..21d2cdc 100644 --- a/services/user_service.py +++ b/services/user_service.py @@ -4,6 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from models.schemas import UserCreate, UserResponse, UserUpdateRole, UserBlockBan from config.database import get_db from models.db import users_table +from services.auth_service import AuthService from utils.security import get_password_hash from typing import Optional @@ -11,7 +12,9 @@ class UserService: @staticmethod - async def list_users(status: Optional[str] = None, db=Depends(get_db)): + async def list_users(token: str, status: Optional[str] = None, db=Depends(get_db)): + await AuthService.check_permissions(token, ["admin"], db) + query = select(users_table) if status: query = query.where(users_table.c.status == status) @@ -20,7 +23,7 @@ class UserService: return [UserResponse(**user) for user in users] @staticmethod - async def create_user(user: UserCreate, db: AsyncSession): + async def create_user(user: UserCreate, db): hashed_password = get_password_hash(user.password) query = users_table.insert().values( email=user.email, @@ -42,7 +45,9 @@ class UserService: raise HTTPException(status_code=500, detail=f"Error creating user: {str(e)}") @staticmethod - async def change_user_role(user_update: UserUpdateRole, db: AsyncSession): + async def change_user_role(user_update: UserUpdateRole, db, token: str): + await AuthService.check_permissions(token, ["admin"], db) + query = ( update(users_table) .where(users_table.c.email == user_update.email) @@ -55,7 +60,8 @@ class UserService: return {"message": f"Role updated to {user_update.new_role}"} @staticmethod - async def block_user(user_action: UserBlockBan, db: AsyncSession): + async def block_user(user_action: UserBlockBan, db, token: str): + await AuthService.admin_required(token, db) query = ( update(users_table) .where(users_table.c.email == user_action.email) @@ -68,7 +74,9 @@ class UserService: return {"message": f"User {user_action.email} blocked"} @staticmethod - async def ban_user(user_action: UserBlockBan, db: AsyncSession): + async def ban_user(user_action: UserBlockBan, db, token: str): + await AuthService.admin_required(token, db) + query = ( update(users_table) .where(users_table.c.email == user_action.email) @@ -81,7 +89,8 @@ class UserService: return {"message": f"User {user_action.email} banned"} @staticmethod - async def unblock_user(user_action: UserBlockBan, db: AsyncSession): + async def unblock_user(user_action: UserBlockBan, db, token: str): + await AuthService.admin_required(token, db) query = ( update(users_table) .where(users_table.c.email == user_action.email) @@ -94,7 +103,8 @@ class UserService: return {"message": f"User {user_action.email} unblocked"} @staticmethod - async def unban_user(user_action: UserBlockBan, db: AsyncSession): + async def unban_user(user_action: UserBlockBan, db, token: str): + await AuthService.admin_required(token, db) query = ( update(users_table) .where(users_table.c.email == user_action.email)