From b3ef8d4a19db35ac818a07606b81e0a42e1d5f78 Mon Sep 17 00:00:00 2001 From: Anaz Date: Thu, 9 Jan 2025 12:47:47 +0400 Subject: [PATCH] First commit from visualcode --- api/v1/auth.py | 27 ++++ api/v1/messages.py | 10 ++ api/v1/need_requests.py | 65 +++++++++ api/v1/person_reports.py | 23 +++ api/v1/points_of_interest.py | 31 ++++ api/v1/reports.py | 1 + api/v1/roles.py | 33 +++++ api/v1/shelters.py | 31 ++++ api/v1/technical_issues.py | 26 ++++ api/v1/uploads.py | 28 ++++ api/v1/users.py | 33 +++++ config/database.py | 138 ++++++++++++++++++ config/settings.py | 24 +++ models/db.py | 139 ++++++++++++++++++ models/schemas.py | 133 +++++++++++++++++ old.env | 5 + services/auth_service.py | 193 +++++++++++++++++++++++++ services/celery_service.py | 25 ++++ services/message_service.py | 21 +++ services/need_request_service.py | 127 ++++++++++++++++ services/person_report_service.py | 57 ++++++++ services/points_of_interest_service.py | 71 +++++++++ services/report_service.py | 1 + services/role_service.py | 104 +++++++++++++ services/s3_service.py | 22 +++ services/shelter_service.py | 71 +++++++++ services/technical_issue_service.py | 67 +++++++++ services/upload_service.py | 17 +++ services/user_service.py | 107 ++++++++++++++ tests/integration/test_api.py | 0 tests/unit/test_auth.py | 0 tests/unit/test_user.py | 0 tmp/restart.txt | Bin 1024 -> 0 bytes utils/helpers.py | Bin 1024 -> 0 bytes utils/logging.py | Bin 1024 -> 0 bytes 35 files changed, 1630 insertions(+) create mode 100644 api/v1/auth.py create mode 100644 api/v1/messages.py create mode 100644 api/v1/need_requests.py create mode 100644 api/v1/person_reports.py create mode 100644 api/v1/points_of_interest.py create mode 100644 api/v1/reports.py create mode 100644 api/v1/roles.py create mode 100644 api/v1/shelters.py create mode 100644 api/v1/technical_issues.py create mode 100644 api/v1/uploads.py create mode 100644 api/v1/users.py create mode 100644 config/database.py create mode 100644 config/settings.py create mode 100644 models/db.py create mode 100644 models/schemas.py create mode 100644 old.env create mode 100644 services/auth_service.py create mode 100644 services/celery_service.py create mode 100644 services/message_service.py create mode 100644 services/need_request_service.py create mode 100644 services/person_report_service.py create mode 100644 services/points_of_interest_service.py create mode 100644 services/report_service.py create mode 100644 services/role_service.py create mode 100644 services/s3_service.py create mode 100644 services/shelter_service.py create mode 100644 services/technical_issue_service.py create mode 100644 services/upload_service.py create mode 100644 services/user_service.py create mode 100644 tests/integration/test_api.py create mode 100644 tests/unit/test_auth.py create mode 100644 tests/unit/test_user.py diff --git a/api/v1/auth.py b/api/v1/auth.py new file mode 100644 index 0000000..cbc22d9 --- /dev/null +++ b/api/v1/auth.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from services.auth_service import AuthService +from models.schemas import Token, UserCreate, UserResponse +from config.database import get_db + +router = APIRouter() + +@router.post("/signup", response_model=UserResponse, status_code=201) +async def signup(user: UserCreate, db=Depends(get_db)): + return await AuthService.create_user(user, db) + +@router.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db=Depends(get_db)): + user = await AuthService.authenticate_user(form_data.username, form_data.password, db) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token = AuthService.create_access_token(data={"sub": user["email"]}) + return {"access_token": access_token, "token_type": "bearer"} + +@router.get("/me", response_model=UserResponse) +async def read_users_me(current_user: UserResponse = Depends(AuthService.get_current_user)): + return current_user \ No newline at end of file diff --git a/api/v1/messages.py b/api/v1/messages.py new file mode 100644 index 0000000..269c5de --- /dev/null +++ b/api/v1/messages.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter, Depends, HTTPException +from services.message_service import MessageService +from models.schemas import TechnicalIssue +from config.database import get_db + +router = APIRouter() + +@router.post("/", status_code=201) +async def report_issue(issue: TechnicalIssue, db=Depends(get_db)): + return await MessageService.create_issue(issue, db) \ No newline at end of file diff --git a/api/v1/need_requests.py b/api/v1/need_requests.py new file mode 100644 index 0000000..179aec1 --- /dev/null +++ b/api/v1/need_requests.py @@ -0,0 +1,65 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from services.need_request_service import NeedRequestService +from models.schemas import NeedRequestCreate, NeedRequestUpdate +from config.database import get_db +from services.auth_service import AuthService + +router = APIRouter() + + +@router.post("/", status_code=status.HTTP_201_CREATED) +async def request_need(need: NeedRequestCreate, db=Depends(get_db)): + + return await NeedRequestService.create_need(need, db) + + +@router.get("/{need_id}", status_code=status.HTTP_200_OK) +async def get_need(need_id: int, db=Depends(get_db)): + + need = await NeedRequestService.get_need_by_id(need_id, db) + if not need: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Need request not found") + return need + + +@router.put("/{need_id}", status_code=status.HTTP_200_OK) +async def update_need( + need_id: int, + need_update: NeedRequestUpdate, + db=Depends(get_db), + current_user=Depends(AuthService.get_current_user), +): + + need = await NeedRequestService.get_need_by_id(need_id, db) + if not need: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Need request not found") + + # Vérifie si l'utilisateur est l'auteur ou un administrateur + if need.requester_email != current_user.email and not await admin_required(db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have permission to update this need request", + ) + + return await NeedRequestService.update_need(need_id, need_update, db) + + +@router.delete("/{need_id}", status_code=status.HTTP_200_OK) +async def delete_need( + need_id: int, + db=Depends(get_db), + current_user=Depends(AuthService.get_current_user), +): + + need = await NeedRequestService.get_need_by_id(need_id, db) + if not need: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Need request not found") + + # Vérifie si l'utilisateur est l'auteur ou un administrateur + if need.requester_email != current_user.email and not await admin_required(db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have permission to delete this need request", + ) + + return await NeedRequestService.delete_need(need_id, db) diff --git a/api/v1/person_reports.py b/api/v1/person_reports.py new file mode 100644 index 0000000..c0e5c9f --- /dev/null +++ b/api/v1/person_reports.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, Depends, HTTPException +from services.person_report_service import PersonReportService +from models.schemas import PersonReportCreate, PersonReportUpdate, PersonReportResponse +from config.database import get_db +from typing import Optional + +router = APIRouter() + +@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) + +@router.put("/{report_id}", response_model=PersonReportResponse) +async def update_report(report_id: int, report: PersonReportUpdate, db=Depends(get_db)): + return await PersonReportService.update_report(report_id, report, db) + +@router.get("/{report_id}", response_model=PersonReportResponse) +async def get_report(report_id: int, db=Depends(get_db)): + return await PersonReportService.get_report(report_id, db) + +@router.get("/", response_model=list[PersonReportResponse]) +async def list_reports(status: Optional[str] = None, db=Depends(get_db)): + return await PersonReportService.list_reports(status, db) \ No newline at end of file diff --git a/api/v1/points_of_interest.py b/api/v1/points_of_interest.py new file mode 100644 index 0000000..98e1cd8 --- /dev/null +++ b/api/v1/points_of_interest.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends, HTTPException +from services.points_of_interest_service import PointsOfInterestService +from models.schemas import PointOfInterest +from config.database import get_db + +router = APIRouter() + + +@router.post("/", status_code=201) +async def create_point(point: PointOfInterest, db=Depends(get_db)): + return await PointsOfInterestService.create_point_of_interest(point, db) + + +@router.get("/{point_id}") +async def get_point(point_id: int, db=Depends(get_db)): + return await PointsOfInterestService.get_point_of_interest(point_id, db) + + +@router.get("/") +async def get_all_points(db=Depends(get_db)): + return await PointsOfInterestService.get_all_points_of_interest(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) + + +@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) diff --git a/api/v1/reports.py b/api/v1/reports.py new file mode 100644 index 0000000..35aae14 --- /dev/null +++ b/api/v1/reports.py @@ -0,0 +1 @@ +from fastapi import APIRouter, Depends, HTTPException, status from services.report_service import ReportService from models.schemas import UserReport, UserReportUpdate from config.database import get_db from services.auth_service import AuthService admin_required = AuthService.admin_required router = APIRouter() @router.post("/", status_code=status.HTTP_201_CREATED) async def report_user(report: UserReport, db=Depends(get_db)): return await ReportService.create_report(report, db) @router.get("/{report_id}", status_code=status.HTTP_200_OK) async def get_report(report_id: int, db=Depends(get_db), current_user=Depends(admin_required)): return await ReportService.get_report_by_id(report_id, db) @router.get("/", status_code=status.HTTP_200_OK) async def get_all_reports(db=Depends(get_db), current_user=Depends(admin_required)): return await ReportService.get_all_reports(db) @router.put("/{report_id}", status_code=status.HTTP_200_OK) async def update_report(report_id: int, report_update: UserReportUpdate, db=Depends(get_db), current_user=Depends(admin_required)): return await ReportService.update_report(report_id, report_update, db) @router.delete("/{report_id}", status_code=status.HTTP_200_OK) async def delete_report(report_id: int, db=Depends(get_db), current_user=Depends(admin_required)): return await ReportService.delete_report(report_id, db) \ No newline at end of file diff --git a/api/v1/roles.py b/api/v1/roles.py new file mode 100644 index 0000000..d9a5220 --- /dev/null +++ b/api/v1/roles.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends +from services.role_service import RoleService +from models.schemas import Role +from config.database import get_db +from fastapi.security import OAuth2PasswordBearer + +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + + +@router.post("/", status_code=201) +async def create_role(role: Role, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await RoleService.create_role(role, db, token) + + +@router.get("/{role_id}") +async def get_role(role_id: int, db=Depends(get_db)): + return await RoleService.get_role(role_id, db) + + +@router.get("/") +async def get_all_roles(db=Depends(get_db)): + return await RoleService.get_all_roles(db) + + +@router.put("/{role_id}") +async def update_role(role_id: int, data: dict, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await RoleService.update_role(role_id, data, db, token) + + +@router.delete("/{role_id}") +async def delete_role(role_id: int, db=Depends(get_db), token: str = Depends(oauth2_scheme)): + return await RoleService.delete_role(role_id, db, token) diff --git a/api/v1/shelters.py b/api/v1/shelters.py new file mode 100644 index 0000000..2f8d16f --- /dev/null +++ b/api/v1/shelters.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends, HTTPException +from services.shelter_service import ShelterService +from models.schemas import Shelter +from config.database import get_db + +router = APIRouter() + + +@router.post("/", status_code=201) +async def create_shelter(shelter: Shelter, db=Depends(get_db)): + return await ShelterService.create_shelter(shelter, db) + + +@router.get("/{shelter_id}") +async def get_shelter(shelter_id: int, db=Depends(get_db)): + return await ShelterService.get_shelter(shelter_id, db) + + +@router.get("/") +async def get_all_shelters(db=Depends(get_db)): + return await ShelterService.get_all_shelters(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) + + +@router.delete("/{shelter_id}") +async def delete_shelter(shelter_id: int, db=Depends(get_db)): + return await ShelterService.delete_shelter(shelter_id, db) diff --git a/api/v1/technical_issues.py b/api/v1/technical_issues.py new file mode 100644 index 0000000..0799496 --- /dev/null +++ b/api/v1/technical_issues.py @@ -0,0 +1,26 @@ +from fastapi import APIRouter, Depends, HTTPException +from services.technical_issue_service import TechnicalIssueService +from models.schemas import TechnicalIssue, UpdateTechnicalIssue +from config.database import get_db + +router = APIRouter() + +@router.post("/", status_code=201) +async def create_issue(issue: TechnicalIssue, db=Depends(get_db)): + return await TechnicalIssueService.create_issue(issue, db) + +@router.get("/{issue_id}") +async def get_issue(issue_id: int, db=Depends(get_db)): + return await TechnicalIssueService.get_issue(issue_id, db) + +@router.get("/") +async def get_all_issues(db=Depends(get_db)): + return await TechnicalIssueService.get_all_issues(db) + +@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) + +@router.delete("/{issue_id}") +async def delete_issue(issue_id: int, db=Depends(get_db)): + return await TechnicalIssueService.delete_issue(issue_id, db) diff --git a/api/v1/uploads.py b/api/v1/uploads.py new file mode 100644 index 0000000..5126e8a --- /dev/null +++ b/api/v1/uploads.py @@ -0,0 +1,28 @@ +# app/api/v1/upload.py +from fastapi import APIRouter, UploadFile, File, HTTPException +from services.upload_service import UploadService +import uuid + +router = APIRouter() + +# Instancier UploadService avec le nom de ton bucket S3 +upload_service = UploadService(bucket_name="ton-nom-de-bucket") + +@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 diff --git a/api/v1/users.py b/api/v1/users.py new file mode 100644 index 0000000..6e6e429 --- /dev/null +++ b/api/v1/users.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from services.user_service import UserService +from models.schemas import UserUpdateRole, UserBlockBan, UserResponse +from config.database import get_db +from services.auth_service import AuthService +from typing import Optional + +router = APIRouter() + +@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) + +@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.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("/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("/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("/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 diff --git a/config/database.py b/config/database.py new file mode 100644 index 0000000..ebd82d5 --- /dev/null +++ b/config/database.py @@ -0,0 +1,138 @@ +from datetime import datetime +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from sqlalchemy import Table, Column, Integer, String, DateTime, Boolean, MetaData, ForeignKey, Text +from config.settings import settings +import logging + +# Configuration du logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# MĂ©tadonnĂ©es pour les tables +metadata = MetaData() + +# Table des utilisateurs +users_table = Table( + "users", + metadata, + Column("id", Integer, primary_key=True), + Column("email", String(255), unique=True, nullable=False), # VARCHAR(255) + Column("full_name", String(255), nullable=False), # VARCHAR(255) + Column("phone", String(20), nullable=False), # VARCHAR(20) + Column("date_of_birth", DateTime, nullable=False), + Column("organization", String(255)), # VARCHAR(255) + Column("hashed_password", String(255), nullable=False), # VARCHAR(255) + Column("role", String(50), default="user"), # VARCHAR(50) + Column("is_blocked", Boolean, default=False), + Column("is_deleted", Boolean, default=False) +) + +# Table des rĂ´les +roles_table = Table( + "roles", + metadata, + Column("id", Integer, primary_key=True), + Column("name", String(50), unique=True, nullable=False), # VARCHAR(50) + Column("permissions", Text) # Stocke les permissions sous forme de chaĂ®ne sĂ©parĂ©e par des virgules +) + +# Table des demandes de besoin +need_requests_table = Table( + "need_requests", + metadata, + Column("id", Integer, primary_key=True), + Column("category", String(50), nullable=False), # Ex: "water", "food", "medical assistance" + Column("description", Text, nullable=False), + Column("adults", Integer, nullable=False), + Column("children", Integer, nullable=False), + Column("vulnerable", Integer, nullable=False), + Column("location", String(255), nullable=False), # VARCHAR(255) + Column("gps_coordinates", String(100)), # VARCHAR(100) + Column("requester_email", String(255), ForeignKey("users.email"), nullable=False), # VARCHAR(255) + Column("assigned_to", Integer, ForeignKey("users.id")), # Utilisateur qui a pris en charge la demande + Column("status", String(50), default="pending") # Ex: "pending", "in_progress", "completed" +) + +# Table des signalements d'utilisateurs +user_reports_table = Table( + "user_reports", + metadata, + Column("id", Integer, primary_key=True), + Column("reporter_id", Integer, ForeignKey("users.id"), nullable=False), + Column("reported_user_id", Integer, ForeignKey("users.id"), nullable=False), + Column("reason", Text, nullable=False), + Column("status", String(50), default="pending") # Ex: "pending", "resolved" +) + +# Table des problèmes techniques +technical_issues_table = Table( + "technical_issues", + metadata, + Column("id", Integer, primary_key=True), + Column("user_id", Integer, ForeignKey("users.id"), nullable=False), + Column("description", Text, nullable=False), + Column("status", String(50), default="open") # Ex: "open", "in_progress", "resolved" +) + +# Table des points d'intĂ©rĂŞt +points_of_interest_table = Table( + "points_of_interest", + metadata, + Column("id", Integer, primary_key=True), + Column("label", String(255), nullable=False), # VARCHAR(255) + Column("description", Text), + Column("icon", String(255)), # URL de l'icĂ´ne (VARCHAR(255)) + Column("organization", String(255)), # VARCHAR(255) + Column("gps_coordinates", String(100), nullable=False), # VARCHAR(100) + Column("added_by", Integer, ForeignKey("users.id"), nullable=False) +) + +# Table des abris +shelters_table = Table( + "shelters", + metadata, + Column("id", Integer, primary_key=True), + Column("label", String(255), nullable=False), # VARCHAR(255) + Column("description", Text), + Column("status", String(50), default="available"), # Ex: "available", "full", "closed" + Column("contact_person", String(255)), # VARCHAR(255) + Column("gps_coordinates", String(100), nullable=False), # VARCHAR(100) + Column("added_by", Integer, ForeignKey("users.id"), nullable=False) +) + +# Table des dĂ©clarations de personnes +person_reports_table = Table( + "person_reports", + metadata, + Column("id", Integer, primary_key=True), + Column("full_name", String(255), nullable=False), # VARCHAR(255) + Column("date_of_birth", DateTime, nullable=False), + Column("status", String(50), nullable=False), # Ex: "missing", "safe", "deceased" + Column("location", String(255)), # VARCHAR(255) + Column("gps_coordinates", String(100)), # VARCHAR(100) + Column("photo_url", String(255)), # URL de la photo (VARCHAR(255)) + Column("reporter_email", String(255), ForeignKey("users.email"), nullable=False), # VARCHAR(255) + Column("created_at", DateTime, default=datetime.utcnow), # Date de crĂ©ation + Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Date de mise Ă  jour +) + +# Connexion Ă  la base de donnĂ©es +DATABASE_URL = settings.database_url +engine = create_async_engine(DATABASE_URL, echo=True) +AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + +# Fonction pour initialiser la base de donnĂ©es +async def init_db(): + try: + async with engine.begin() as conn: + await conn.run_sync(metadata.create_all) + logger.info("Database tables created successfully.") + except Exception as e: + logger.error(f"Error creating database tables: {str(e)}") + raise + +# Fonction pour obtenir une session de base de donnĂ©es +async def get_db(): + async with AsyncSessionLocal() as session: + yield session \ No newline at end of file diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..e8c2f96 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,24 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + database_url: str = "mysql+aiomysql://sywmtnsg_admin:EEy_>2JJS0@localhost:6033/sywmtnsg_dm_management" + secret_key: str = "LAGs7G8Sis9aQHcipROxpjYRxFZKjr4wNm-_O0pBTkjNYv1rgPUR87VcNswH_VYGpIrsyGdqnNa3vcVSH0f5Tg" + algorithm: str = "HS256" + access_token_expire_minutes: int = 30 + aws_access_key_id: str = "" + aws_secret_access_key: str = "" + aws_bucket_name: str = "" + celery_broker_url: str = "" + log_level: str = "INFO" + email_host: str = "" + email_port: int = 587 + email_username: str = "" + email_password: str = "" + gdpr_deletion_delay_days: int = 7 + testing: bool = False + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + +settings = Settings() \ No newline at end of file diff --git a/models/db.py b/models/db.py new file mode 100644 index 0000000..6dd10a4 --- /dev/null +++ b/models/db.py @@ -0,0 +1,139 @@ +from datetime import datetime +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from sqlalchemy import Table, Column, Integer, String, DateTime, Boolean, MetaData, ForeignKey, Text +from config.settings import settings +import logging + +# Configuration du logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# MĂ©tadonnĂ©es pour les tables +metadata = MetaData() + +# Table des utilisateurs +users_table = Table( + "users", + metadata, + Column("id", Integer, primary_key=True), + Column("email", String(255), unique=True, nullable=False), # VARCHAR(255) + Column("full_name", String(255), nullable=False), # VARCHAR(255) + Column("phone", String(20), nullable=False), # VARCHAR(20) + Column("date_of_birth", DateTime, nullable=False), + Column("organization", String(255)), # VARCHAR(255) + Column("hashed_password", String(255), nullable=False), # VARCHAR(255) + Column("role", String(50), default="user"), # VARCHAR(50) + Column("is_blocked", Boolean, default=False), + Column("is_deleted", Boolean, default=False) +) + +# Table des rĂ´les +roles_table = Table( + "roles", + metadata, + Column("id", Integer, primary_key=True), + Column("name", String(50), unique=True, nullable=False), # VARCHAR(50) + Column("permissions", Text) # Stocke les permissions sous forme de chaĂ®ne sĂ©parĂ©e par des virgules +) + +# Table des demandes de besoin +need_requests_table = Table( + "need_requests", + metadata, + Column("id", Integer, primary_key=True), + Column("category", String(50), nullable=False), # Ex: "water", "food", "medical assistance" + Column("description", Text, nullable=False), + Column("adults", Integer, nullable=False), + Column("children", Integer, nullable=False), + Column("vulnerable", Integer, nullable=False), + Column("location", String(255), nullable=False), # VARCHAR(255) + Column("gps_coordinates", String(100)), # VARCHAR(100) + Column("requester_email", String(255), ForeignKey("users.email"), nullable=False), # VARCHAR(255) + Column("assigned_to", Integer, ForeignKey("users.id")), # Utilisateur qui a pris en charge la demande + Column("status", String(50), default="pending"), # Ex: "pending", "in_progress", "completed" + Column("deleted", DateTime, default=None) # Marque la suppression logique +) + +# Table des signalements d'utilisateurs +user_reports_table = Table( + "user_reports", + metadata, + Column("id", Integer, primary_key=True), + Column("reporter_id", Integer, ForeignKey("users.id"), nullable=False), + Column("reported_user_id", Integer, ForeignKey("users.id"), nullable=False), + Column("reason", Text, nullable=False), + Column("status", String(50), default="pending") # Ex: "pending", "resolved" +) + +# Table des problèmes techniques +technical_issues_table = Table( + "technical_issues", + metadata, + Column("id", Integer, primary_key=True), + Column("user_id", Integer, ForeignKey("users.id"), nullable=False), + Column("description", Text, nullable=False), + Column("status", String(50), default="open") # Ex: "open", "in_progress", "resolved" +) + +# Table des points d'intĂ©rĂŞt +points_of_interest_table = Table( + "points_of_interest", + metadata, + Column("id", Integer, primary_key=True), + Column("label", String(255), nullable=False), # VARCHAR(255) + Column("description", Text), + Column("icon", String(255)), # URL de l'icĂ´ne (VARCHAR(255)) + Column("organization", String(255)), # VARCHAR(255) + Column("gps_coordinates", String(100), nullable=False), # VARCHAR(100) + Column("added_by", Integer, ForeignKey("users.id"), nullable=False) +) + +# Table des abris +shelters_table = Table( + "shelters", + metadata, + Column("id", Integer, primary_key=True), + Column("label", String(255), nullable=False), # VARCHAR(255) + Column("description", Text), + Column("status", String(50), default="available"), # Ex: "available", "full", "closed" + Column("contact_person", String(255)), # VARCHAR(255) + Column("gps_coordinates", String(100), nullable=False), # VARCHAR(100) + Column("added_by", Integer, ForeignKey("users.id"), nullable=False) +) + +# Table des dĂ©clarations de personnes +person_reports_table = Table( + "person_reports", + metadata, + Column("id", Integer, primary_key=True), + Column("full_name", String(255), nullable=False), # VARCHAR(255) + Column("date_of_birth", DateTime, nullable=False), + Column("status", String(50), nullable=False), # Ex: "missing", "safe", "deceased" + Column("location", String(255)), # VARCHAR(255) + Column("gps_coordinates", String(100)), # VARCHAR(100) + Column("photo_url", String(255)), # URL de la photo (VARCHAR(255)) + Column("reporter_email", String(255), ForeignKey("users.email"), nullable=False), # VARCHAR(255) + Column("created_at", DateTime, default=datetime.utcnow), # Date de crĂ©ation + Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Date de mise Ă  jour +) + +# Connexion Ă  la base de donnĂ©es +DATABASE_URL = settings.database_url +engine = create_async_engine(DATABASE_URL, echo=True) +AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + +# Fonction pour initialiser la base de donnĂ©es +async def init_db(): + try: + async with engine.begin() as conn: + await conn.run_sync(metadata.create_all) + logger.info("Database tables created successfully.") + except Exception as e: + logger.error(f"Error creating database tables: {str(e)}") + raise + +# Fonction pour obtenir une session de base de donnĂ©es +async def get_db(): + async with AsyncSessionLocal() as session: + yield session \ No newline at end of file diff --git a/models/schemas.py b/models/schemas.py new file mode 100644 index 0000000..a215354 --- /dev/null +++ b/models/schemas.py @@ -0,0 +1,133 @@ +from pydantic import BaseModel, EmailStr +from datetime import datetime +from typing import List, Optional + +class UserBase(BaseModel): + email: EmailStr + full_name: str + phone: str + date_of_birth: datetime + organization: Optional[str] = None + +class UserCreate(UserBase): + password: str + role: str + +class UserResponse(BaseModel): + email: EmailStr + full_name: str + phone: str + date_of_birth: str + organization: Optional[str] = None + role: str + is_active: bool + is_banned: bool + +class UserUpdateRole(BaseModel): + email: EmailStr + new_role: str + +class UserBlockBan(BaseModel): + email: EmailStr + +class Role(BaseModel): + id: int + name: str + permissions: List[str] + +# Demande de besoin (NeedRequest) +class NeedRequestBase(BaseModel): + category: str + description: str + adults: int + children: int + vulnerable: int + location: str + gps_coordinates: Optional[str] = None + + +class NeedRequestCreate(NeedRequestBase): + requester_email: EmailStr + + +class NeedRequestUpdate(BaseModel): + category: Optional[str] = None + description: Optional[str] = None + adults: Optional[int] = None + children: Optional[int] = None + vulnerable: Optional[int] = None + location: Optional[str] = None + gps_coordinates: Optional[str] = None + status: Optional[str] = None + + +class NeedRequestResponse(NeedRequestBase): + id: int + requester_email: EmailStr + status: str + deleted: Optional[datetime] = None + +class UserReport(BaseModel): + reporter_id: int + reported_user_id: int + reason: str + status: Optional[str] = "pending" + +class UserReportUpdate(BaseModel): + reason: Optional[str] = None + status: Optional[str] = None # Exemples : "pending", "resolved" + +class TechnicalIssue(BaseModel): + user_id: int + description: str + status: str + +class UpdateTechnicalIssue(BaseModel): + status: Optional[str] = None + description: Optional[str] = None + +class PersonReportBase(BaseModel): + full_name: str + date_of_birth: datetime + status: str # "missing", "safe", "deceased" + location: Optional[str] = None + gps_coordinates: Optional[str] = None + photo_url: Optional[str] = None + reporter_email: str + +class PersonReportCreate(PersonReportBase): + pass + +class PersonReportUpdate(BaseModel): + status: Optional[str] = None + location: Optional[str] = None + gps_coordinates: Optional[str] = None + photo_url: Optional[str] = None + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + email: Optional[str] = None + +class PointOfInterest(BaseModel): + label: str + description: Optional[str] = None + icon: Optional[str] = None # URL de l'icĂ´ne + organization: Optional[str] = None + gps_coordinates: str + added_by: int # ID de l'utilisateur qui a ajoutĂ© ce point + +class Shelter(BaseModel): + label: str + description: Optional[str] = None + status: str # "available", "full", "closed" + contact_person: Optional[str] = None + gps_coordinates: str + added_by: int # ID de l'utilisateur qui a ajoutĂ© cet abri + +class PersonReportResponse(PersonReportBase): + id: int + created_at: datetime + updated_at: datetime \ No newline at end of file diff --git a/old.env b/old.env new file mode 100644 index 0000000..cdf6950 --- /dev/null +++ b/old.env @@ -0,0 +1,5 @@ +DB_HOST=localhost +DB_USER=sywmtnsg_admin +DB_PASSWORD=EEy_>2JJS0 +DB_NAME=sywmtnsg_dm_management +SECRET_KEY=LAGs7G8Sis9aQHcipROxpjYRxFZKjr4wNm-_O0pBTkjNYv1rgPUR87VcNswH_VYGpIrsyGdqnNa3vcVSH0f5Tg \ No newline at end of file diff --git a/services/auth_service.py b/services/auth_service.py new file mode 100644 index 0000000..ffef4ae --- /dev/null +++ b/services/auth_service.py @@ -0,0 +1,193 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from config.settings import settings +from models.schemas import TokenData, UserCreate, UserResponse +from config.database import get_db +from models.db import users_table +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession # Import ajoutĂ© ici + +# Configuration pour le hachage des mots de passe +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# Configuration pour OAuth2 +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + +class AuthService: + @staticmethod + def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + @staticmethod + def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + @staticmethod + async def authenticate_user(email: str, password: str, db): + query = select(users_table).where(users_table.c.email == email) + result = await db.execute(query) + user = result.fetchone() + + if not user: + return None + if not AuthService.verify_password(password, user["hashed_password"]): + return None + + return user + + @staticmethod + def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + 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)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + 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: + raise credentials_exception + + query = select(users_table).where(users_table.c.email == token_data.email) + result = await db.execute(query) + user = result.fetchone() + if user is None: + raise credentials_exception + + return UserResponse(**user) + + @staticmethod + async def create_user(user: UserCreate, db): + query = select(users_table).where(users_table.c.email == user.email) + result = await db.execute(query) + existing_user = result.fetchone() + if existing_user: + raise HTTPException(status_code=400, detail="Email already registered") + + hashed_password = AuthService.get_password_hash(user.password) + + query = insert(users_table).values( + email=user.email, + full_name=user.full_name, + phone=user.phone, + date_of_birth=user.date_of_birth, + organization=user.organization, + hashed_password=hashed_password, + role="user" + ) + try: + result = await db.execute(query) + await db.commit() + user_id = result.inserted_primary_key[0] + return await AuthService.get_current_user(db=db) + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not create user: {str(e)}") + + + @staticmethod + async def admin_required(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): + 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: + raise credentials_exception + + query = users_table.select().where(users_table.c.email == token_data.email) + result = await db.execute(query) + user = result.fetchone() + if user is None: + raise credentials_exception + + if user["role"] != "admin": + raise credentials_exception + + return user + + + @staticmethod + async def update_user(user_id: int, updates: dict, token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): + """ + Met Ă  jour les informations d'un utilisateur. + - Un utilisateur peut mettre Ă  jour ses propres informations. + - Un administrateur peut mettre Ă  jour les informations de n'importe quel utilisateur. + """ + # DĂ©codage et vĂ©rification des permissions + 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: + raise credentials_exception + + query = select(users_table).where(users_table.c.email == token_data.email) + result = await db.execute(query) + current_user = result.fetchone() + if current_user is None: + raise credentials_exception + + # VĂ©rifier si l'utilisateur est l'admin ou lui-mĂŞme + if current_user["id"] != user_id and current_user["role"] != "admin": + raise credentials_exception + + # Mise Ă  jour des champs autorisĂ©s + allowed_updates = {"full_name", "phone", "date_of_birth", "organization", "email"} + updates = {key: value for key, value in updates.items() if key in allowed_updates} + + if "email" in updates: # VĂ©rifie si l'email existe dĂ©jĂ  + existing_email_query = select(users_table).where(users_table.c.email == updates["email"]) + result = await db.execute(existing_email_query) + existing_user = result.fetchone() + if existing_user and existing_user["id"] != user_id: + raise HTTPException(status_code=400, detail="Email already registered by another user.") + + query = ( + update(users_table) + .where(users_table.c.id == user_id) + .values(**updates) + .execution_options(synchronize_session="fetch") + ) + + try: + await db.execute(query) + await db.commit() + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not update user: {str(e)}") + + # RĂ©cupĂ©rer les informations mises Ă  jour + updated_user_query = select(users_table).where(users_table.c.id == user_id) + result = await db.execute(updated_user_query) + updated_user = result.fetchone() + + return UserResponse(**updated_user) diff --git a/services/celery_service.py b/services/celery_service.py new file mode 100644 index 0000000..012c4bf --- /dev/null +++ b/services/celery_service.py @@ -0,0 +1,25 @@ +from celery import Celery +from config.settings import settings +from config.database import get_db +from models.db import users_table # Assure-toi d'avoir une table `users_table` dĂ©finie dans `db.py` + +celery_app = Celery("tasks", broker=settings.celery_broker_url) + +@celery_app.task +async def delete_user_data(user_id: int): + async with get_db() as db: + try: + await db.execute( + users_table.update() + .where(users_table.c.id == user_id) + .values( + full_name="xxxxx", + email="xxxxx", + phone="xxxxx", + is_deleted=True + ) + ) + await db.commit() + except Exception as e: + await db.rollback() + raise Exception(f"Could not delete user data: {str(e)}") \ No newline at end of file diff --git a/services/message_service.py b/services/message_service.py new file mode 100644 index 0000000..2169e1e --- /dev/null +++ b/services/message_service.py @@ -0,0 +1,21 @@ +from sqlalchemy import insert +from models.schemas import TechnicalIssue +from config.database import get_db +from models.db import technical_issues_table # Assure-toi d'avoir une table `technical_issues_table` dĂ©finie dans `db.py` + +class MessageService: + @staticmethod + async def create_issue(issue: TechnicalIssue, db): + query = insert(technical_issues_table).values( + user_id=issue.user_id, + description=issue.description, + status="open" # Par dĂ©faut, le statut est "open" + ) + try: + result = await db.execute(query) + await db.commit() + issue_id = result.inserted_primary_key[0] + return {"id": issue_id, **issue.dict()} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not create technical issue: {str(e)}") \ No newline at end of file diff --git a/services/need_request_service.py b/services/need_request_service.py new file mode 100644 index 0000000..dbfd737 --- /dev/null +++ b/services/need_request_service.py @@ -0,0 +1,127 @@ +from sqlalchemy import insert, select, update +from models.schemas import NeedRequestCreate +from config.database import get_db +from models.db import need_requests_table, users_table +from models.schemas import TokenData +from fastapi import HTTPException, Depends, status +from fastapi.security import OAuth2PasswordBearer +from config.settings import settings +from jose import jwt, JWTError +from datetime import datetime + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + + +class NeedRequestService: + @staticmethod + async def create_need(need: NeedRequestCreate, db): + query = insert(need_requests_table).values( + category=need.category, + description=need.description, + adults=need.adults, + children=need.children, + vulnerable=need.vulnerable, + location=need.location, + gps_coordinates=need.gps_coordinates, + requester_email=need.requester_email + ) + try: + result = await db.execute(query) + await db.commit() + need_id = result.inserted_primary_key[0] + return {"id": need_id, **need.dict()} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not create need request: {str(e)}") + + @staticmethod + async def get_need(need_id: int, db): + query = select(need_requests_table).where(need_requests_table.c.id == need_id) + result = await db.execute(query) + need = result.fetchone() + if need is None: + raise HTTPException(status_code=404, detail="Need request not found") + return dict(need) + + @staticmethod + async def get_all_needs(db): + query = select(need_requests_table).where(need_requests_table.c.deleted == None) + result = await db.execute(query) + needs = result.fetchall() + return [dict(need) for need in needs] + + @staticmethod + async def update_need(need_id: int, data: dict, db, token: str): + user = await NeedRequestService.verify_requester_or_admin(need_id, token, db) + if "deleted" in data: # Empęche la mise ŕ jour directe du champ `deleted` + raise HTTPException(status_code=400, detail="Invalid update field") + + query = ( + update(need_requests_table) + .where(need_requests_table.c.id == need_id) + .values(**data) + ) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Need request not found") + await db.commit() + return {"message": "Need request updated successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not update need request: {str(e)}") + + @staticmethod + async def delete_need(need_id: int, db, token: str): + user = await NeedRequestService.verify_requester_or_admin(need_id, token, db) + + query = ( + update(need_requests_table) + .where(need_requests_table.c.id == need_id) + .values(deleted=datetime.utcnow()) + ) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Need request not found") + await db.commit() + return {"message": "Need request deleted successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not delete need request: {str(e)}") + + @staticmethod + async def verify_requester_or_admin(need_id: int, token: str, db): + credentials_exception = HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have permission to perform this action.", + ) + try: + # Décodage du token JWT + 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: + raise credentials_exception + + # Récupčre l'utilisateur depuis la base de données + user_query = select(users_table).where(users_table.c.email == token_data.email) + result = await db.execute(user_query) + user = result.fetchone() + if user is None: + raise credentials_exception + + # Récupčre la demande de besoin + need_query = select(need_requests_table).where(need_requests_table.c.id == need_id) + result = await db.execute(need_query) + need = result.fetchone() + if need is None: + raise HTTPException(status_code=404, detail="Need request not found") + + # Vérifie si l'utilisateur est l'auteur ou un administrateur + if need["requester_email"] != user["email"] and user["role"] != "admin": + raise credentials_exception + + return user diff --git a/services/person_report_service.py b/services/person_report_service.py new file mode 100644 index 0000000..04ffc84 --- /dev/null +++ b/services/person_report_service.py @@ -0,0 +1,57 @@ +from sqlalchemy import select, update +from fastapi import HTTPException +from models.schemas import PersonReportCreate, PersonReportUpdate, PersonReportResponse +from config.database import get_db +from models.db import person_reports_table +from typing import Optional +from fastapi import Depends +from fastapi.security import OAuth2PasswordBearer + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + + +class PersonReportService: + @staticmethod + async def create_report(report: PersonReportCreate, db): + query = person_reports_table.insert().values(**report.dict()) + try: + result = await db.execute(query) + await db.commit() + report_id = result.inserted_primary_key[0] + return await PersonReportService.get_report(report_id, db) + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not create report: {str(e)}") + + @staticmethod + async def update_report(report_id: int, report: PersonReportUpdate, db): + query = ( + person_reports_table.update() + .where(person_reports_table.c.id == report_id) + .values(**report.dict(exclude_unset=True)) + ) + try: + await db.execute(query) + await db.commit() + return await PersonReportService.get_report(report_id, db) + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not update report: {str(e)}") + + @staticmethod + async def get_report(report_id: int, db): + query = select(person_reports_table).where(person_reports_table.c.id == report_id) + result = await db.execute(query) + report = result.fetchone() + if not report: + raise HTTPException(status_code=404, detail="Report not found") + return PersonReportResponse(**report) + + @staticmethod + async def list_reports(status: Optional[str] = None, db=Depends(get_db)): + query = select(person_reports_table) + if status: + query = query.where(person_reports_table.c.status == status) + result = await db.execute(query) + reports = result.fetchall() + return [PersonReportResponse(**report) for report in reports] \ No newline at end of file diff --git a/services/points_of_interest_service.py b/services/points_of_interest_service.py new file mode 100644 index 0000000..a749a39 --- /dev/null +++ b/services/points_of_interest_service.py @@ -0,0 +1,71 @@ +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 sqlalchemy.ext.asyncio import AsyncSession + + +class PointsOfInterestService: + @staticmethod + async def create_point_of_interest(point: PointOfInterest, db: AsyncSession): + 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, + ) + try: + result = await db.execute(query) + await db.commit() + point_id = result.inserted_primary_key[0] + return {"id": point_id, **point.dict()} + except Exception as e: + await db.rollback() + 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): + query = select(points_of_interest_table).where(points_of_interest_table.c.id == point_id) + result = await db.execute(query) + point = result.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): + query = select(points_of_interest_table) + result = await db.execute(query) + return [dict(row) for row in result.fetchall()] + + @staticmethod + async def update_point_of_interest(point_id: int, data: dict, db: AsyncSession): + query = ( + update(points_of_interest_table) + .where(points_of_interest_table.c.id == point_id) + .values(**data) + ) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Point of interest not found") + await db.commit() + return {"message": "Point of interest updated successfully"} + except Exception as e: + 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): + query = delete(points_of_interest_table).where(points_of_interest_table.c.id == point_id) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Point of interest not found") + await db.commit() + return {"message": "Point of interest deleted successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not delete point of interest: {str(e)}") diff --git a/services/report_service.py b/services/report_service.py new file mode 100644 index 0000000..f77f9ec --- /dev/null +++ b/services/report_service.py @@ -0,0 +1 @@ +from sqlalchemy import insert, update, select, delete from models.schemas import UserReport, UserReportUpdate from config.database import get_db from models.db import user_reports_table from fastapi import HTTPException, status class ReportService: @staticmethod async def create_report(report: UserReport, db): query = insert(user_reports_table).values( reporter_id=report.reporter_id, reported_user_id=report.reported_user_id, reason=report.reason, status="pending" # Par dĂ©faut, le statut est "pending" ) try: result = await db.execute(query) await db.commit() report_id = result.inserted_primary_key[0] return {"id": report_id, **report.dict()} except Exception as e: await db.rollback() 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): query = select(user_reports_table).where(user_reports_table.c.id == report_id) result = await db.execute(query) report = result.fetchone() if not report: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report not found") return dict(report) @staticmethod async def get_all_reports(db): query = select(user_reports_table) result = await db.execute(query) reports = result.fetchall() return [dict(report) for report in reports] @staticmethod async def update_report(report_id: int, report_update: UserReportUpdate, db): query = ( update(user_reports_table) .where(user_reports_table.c.id == report_id) .values(**report_update.dict(exclude_unset=True)) .returning(user_reports_table.c.id) ) try: result = await db.execute(query) await db.commit() updated_id = result.fetchone() if not updated_id: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report not found") return {"id": updated_id[0], **report_update.dict()} except Exception as e: await db.rollback() raise HTTPException(status_code=500, detail=f"Could not update report: {str(e)}") @staticmethod async def delete_report(report_id: int, db): query = delete(user_reports_table).where(user_reports_table.c.id == report_id) try: result = await db.execute(query) await db.commit() if result.rowcount == 0: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report not found") return {"detail": "Report deleted successfully"} except Exception as e: await db.rollback() raise HTTPException(status_code=500, detail=f"Could not delete report: {str(e)}") \ No newline at end of file diff --git a/services/role_service.py b/services/role_service.py new file mode 100644 index 0000000..0db6255 --- /dev/null +++ b/services/role_service.py @@ -0,0 +1,104 @@ +from sqlalchemy import insert, select, update, delete +from fastapi import HTTPException, Depends, status +from fastapi.security import OAuth2PasswordBearer +from models.schemas import Role, TokenData +from models.db import roles_table, users_table +from config.database import get_db +from config.settings import settings +from jose import jwt, JWTError +from sqlalchemy.ext.asyncio import AsyncSession + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + + +class RoleService: + @staticmethod + async def create_role(role: Role, db: AsyncSession, token: str): + """ + CrĂ©e un nouveau rĂ´le (rĂ©servĂ© aux administrateurs). + """ + await RoleService.admin_required(token, db) + query = insert(roles_table).values( + name=role.name, + permissions=",".join(role.permissions), # Stocke les permissions sous forme de chaĂ®ne + ) + try: + result = await db.execute(query) + await db.commit() + role_id = result.inserted_primary_key[0] + return {"id": role_id, **role.dict()} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not create role: {str(e)}") + + @staticmethod + async def update_role(role_id: int, data: dict, db: AsyncSession, token: str): + """ + Met Ă  jour un rĂ´le par son ID (rĂ©servĂ© aux administrateurs). + """ + await RoleService.admin_required(token, db) + if "permissions" in data: + data["permissions"] = ",".join(data["permissions"]) # Convertit les permissions en chaĂ®ne + query = ( + update(roles_table) + .where(roles_table.c.id == role_id) + .values(**data) + ) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Role not found") + await db.commit() + return {"message": "Role updated successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not update role: {str(e)}") + + @staticmethod + async def delete_role(role_id: int, db: AsyncSession, token: str): + """ + Supprime un rĂ´le par son ID (rĂ©servĂ© aux administrateurs). + """ + await RoleService.admin_required(token, db) + query = delete(roles_table).where(roles_table.c.id == role_id) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Role not found") + await db.commit() + return {"message": "Role deleted successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not delete role: {str(e)}") + + @staticmethod + async def admin_required(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)): + """ + VĂ©rifie si l'utilisateur actuel est un administrateur. + """ + credentials_exception = HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have permission to perform this action.", + ) + try: + # DĂ©codage du token JWT + 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: + raise credentials_exception + + # RĂ©cupère l'utilisateur depuis la base de donnĂ©es + user_query = select(users_table).where(users_table.c.email == token_data.email) + result = await db.execute(user_query) + user = result.fetchone() + if user is None: + raise credentials_exception + + # VĂ©rifie si l'utilisateur a le rĂ´le d'administrateur + if user["role"] != "admin": + raise credentials_exception + + return user diff --git a/services/s3_service.py b/services/s3_service.py new file mode 100644 index 0000000..658ce20 --- /dev/null +++ b/services/s3_service.py @@ -0,0 +1,22 @@ +from fastapi import UploadFile, HTTPException +import boto3 +from botocore.exceptions import NoCredentialsError +from config.settings import settings + +s3_client = boto3.client( + 's3', + aws_access_key_id=settings.aws_access_key_id, + aws_secret_access_key=settings.aws_secret_access_key +) + +class UploadService: + @staticmethod + async def upload_file(file: UploadFile): + try: + s3_client.upload_fileobj(file.file, settings.aws_bucket_name, file.filename) + file_url = f"https://{settings.aws_bucket_name}.s3.amazonaws.com/{file.filename}" + return {"file_url": file_url} + except NoCredentialsError: + raise HTTPException(status_code=500, detail="AWS credentials not available") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Could not upload file: {str(e)}") \ No newline at end of file diff --git a/services/shelter_service.py b/services/shelter_service.py new file mode 100644 index 0000000..65af1db --- /dev/null +++ b/services/shelter_service.py @@ -0,0 +1,71 @@ +from sqlalchemy import insert, select, update, delete +from fastapi import HTTPException +from models.db import shelters_table +from models.schemas import Shelter +from sqlalchemy.ext.asyncio import AsyncSession + + +class ShelterService: + @staticmethod + async def create_shelter(shelter: Shelter, db: AsyncSession): + 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, + ) + try: + result = await db.execute(query) + await db.commit() + shelter_id = result.inserted_primary_key[0] + return {"id": shelter_id, **shelter.dict()} + 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): + query = select(shelters_table).where(shelters_table.c.id == shelter_id) + result = await db.execute(query) + shelter = result.fetchone() + if not shelter: + raise HTTPException(status_code=404, detail="Shelter not found") + return dict(shelter) + + @staticmethod + async def get_all_shelters(db: AsyncSession): + query = select(shelters_table) + result = await db.execute(query) + return [dict(row) for row in result.fetchall()] + + @staticmethod + async def update_shelter(shelter_id: int, data: dict, db: AsyncSession): + query = ( + update(shelters_table) + .where(shelters_table.c.id == shelter_id) + .values(**data) + ) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Shelter not found") + await db.commit() + return {"message": "Shelter updated successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not update shelter: {str(e)}") + + @staticmethod + async def delete_shelter(shelter_id: int, db: AsyncSession): + query = delete(shelters_table).where(shelters_table.c.id == shelter_id) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Shelter not found") + await db.commit() + return {"message": "Shelter deleted successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not delete shelter: {str(e)}") diff --git a/services/technical_issue_service.py b/services/technical_issue_service.py new file mode 100644 index 0000000..eb22068 --- /dev/null +++ b/services/technical_issue_service.py @@ -0,0 +1,67 @@ +from sqlalchemy import insert, select, update, delete +from fastapi import HTTPException +from models.db import technical_issues_table +from models.schemas import TechnicalIssue, UpdateTechnicalIssue + + +class TechnicalIssueService: + @staticmethod + async def create_issue(issue: TechnicalIssue, db): + query = insert(technical_issues_table).values( + user_id=issue.user_id, + description=issue.description, + status=issue.status + ) + try: + result = await db.execute(query) + await db.commit() + issue_id = result.inserted_primary_key[0] + return {"id": issue_id, **issue.dict()} + 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): + query = select(technical_issues_table).where(technical_issues_table.c.id == issue_id) + result = await db.execute(query) + issue = result.fetchone() + if not issue: + raise HTTPException(status_code=404, detail="Technical issue not found") + return dict(issue) + + @staticmethod + async def get_all_issues(db): + query = select(technical_issues_table) + result = await db.execute(query) + return [dict(row) for row in result.fetchall()] + + @staticmethod + async def update_issue(issue_id: int, issue_data: UpdateTechnicalIssue, db): + query = ( + update(technical_issues_table) + .where(technical_issues_table.c.id == issue_id) + .values(**issue_data.dict(exclude_unset=True)) + ) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Technical issue not found") + await db.commit() + return {"message": "Technical issue updated successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not update issue: {str(e)}") + + @staticmethod + async def delete_issue(issue_id: int, db): + query = delete(technical_issues_table).where(technical_issues_table.c.id == issue_id) + try: + result = await db.execute(query) + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="Technical issue not found") + await db.commit() + return {"message": "Technical issue deleted successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Could not delete issue: {str(e)}") diff --git a/services/upload_service.py b/services/upload_service.py new file mode 100644 index 0000000..6d8db2f --- /dev/null +++ b/services/upload_service.py @@ -0,0 +1,17 @@ +# app/services/upload_service.py +import boto3 +from botocore.exceptions import NoCredentialsError + +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): + 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 diff --git a/services/user_service.py b/services/user_service.py new file mode 100644 index 0000000..d1f1790 --- /dev/null +++ b/services/user_service.py @@ -0,0 +1,107 @@ +from sqlalchemy import update, select +from fastapi import Depends, HTTPException, status +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 utils.security import get_password_hash +from typing import Optional + +class UserService: + + + @staticmethod + async def list_users(status: Optional[str] = None, db=Depends(get_db)): + query = select(person_reports_table) + if status: + query = query.where(users_table.c.status == status) + result = await db.execute(query) + users = result.fetchall() + return [UserResponse(**user) for user in users] + + @staticmethod + async def create_user(user: UserCreate, db: AsyncSession): + hashed_password = get_password_hash(user.password) + query = users_table.insert().values( + email=user.email, + full_name=user.full_name, + phone=user.phone, + date_of_birth=user.date_of_birth, + organization=user.organization, + hashed_password=hashed_password, + role=user.role, # Par dĂ©faut, rĂ´le "user" + is_active=True, + is_banned=False + ) + try: + await db.execute(query) + await db.commit() + return {"message": "User created successfully"} + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=f"Error creating user: {str(e)}") + + @staticmethod + async def change_user_role(user_update: UserUpdateRole, db: AsyncSession): + query = ( + update(users_table) + .where(users_table.c.email == user_update.email) + .values(role=user_update.new_role) + ) + result = await db.execute(query) + await db.commit() + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"message": f"Role updated to {user_update.new_role}"} + + @staticmethod + async def block_user(user_action: UserBlockBan, db: AsyncSession): + query = ( + update(users_table) + .where(users_table.c.email == user_action.email) + .values(is_active=False) + ) + result = await db.execute(query) + await db.commit() + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"message": f"User {user_action.email} blocked"} + + @staticmethod + async def ban_user(user_action: UserBlockBan, db: AsyncSession): + query = ( + update(users_table) + .where(users_table.c.email == user_action.email) + .values(is_banned=True) + ) + result = await db.execute(query) + await db.commit() + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"message": f"User {user_action.email} banned"} + + @staticmethod + async def unblock_user(user_action: UserBlockBan, db: AsyncSession): + query = ( + update(users_table) + .where(users_table.c.email == user_action.email) + .values(is_active=True) + ) + result = await db.execute(query) + await db.commit() + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"message": f"User {user_action.email} unblocked"} + + @staticmethod + async def unban_user(user_action: UserBlockBan, db: AsyncSession): + query = ( + update(users_table) + .where(users_table.c.email == user_action.email) + .values(is_banned=False) + ) + result = await db.execute(query) + await db.commit() + if result.rowcount == 0: + raise HTTPException(status_code=404, detail="User not found") + return {"message": f"User {user_action.email} unbanned"} diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_user.py b/tests/unit/test_user.py new file mode 100644 index 0000000..e69de29 diff --git a/tmp/restart.txt b/tmp/restart.txt index 06d7405020018ddf3cacee90fd4af10487da3d20..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 1024 ScmZQz7zLvtFd70QH3R?z00031 diff --git a/utils/helpers.py b/utils/helpers.py index 06d7405020018ddf3cacee90fd4af10487da3d20..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 1024 ScmZQz7zLvtFd70QH3R?z00031 diff --git a/utils/logging.py b/utils/logging.py index 06d7405020018ddf3cacee90fd4af10487da3d20..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 1024 ScmZQz7zLvtFd70QH3R?z00031