From 0e80ac8c76c6448d458c657ac67a7817b70b5e84 Mon Sep 17 00:00:00 2001 From: Madiwka3 Date: Sun, 3 Sep 2023 19:26:53 +0600 Subject: [PATCH] Added authentication --- app/apis/base.py | 3 +- app/apis/v1/route_auth.py | 60 ++++++++++++++++++++++++++++++++++++ app/apis/v1/route_user.py | 17 +++++++--- app/apis/v1/route_vehicle.py | 25 +++++++++++++-- app/core/auth.py | 16 ++++++++++ app/core/config.py | 21 +++++++------ app/db/base.py | 2 -- app/db/models/user.py | 3 +- app/db/models/vehicle.py | 5 --- app/db/repository/user.py | 10 ++++++ app/db/repository/vehicle.py | 16 ++++++++-- app/schemas/token.py | 6 ++++ app/schemas/vehicle.py | 3 +- requirements.txt | 4 ++- 14 files changed, 160 insertions(+), 31 deletions(-) create mode 100644 app/apis/v1/route_auth.py create mode 100644 app/core/auth.py create mode 100644 app/schemas/token.py diff --git a/app/apis/base.py b/app/apis/base.py index 2cd47a5..f42c022 100644 --- a/app/apis/base.py +++ b/app/apis/base.py @@ -1,8 +1,9 @@ # Base API router -- collecting all APIs here to not clutter main.py from fastapi import APIRouter -from apis.v1 import route_user, route_vehicle +from apis.v1 import route_user, route_vehicle, route_auth api_router = APIRouter() api_router.include_router(route_user.router, prefix="/user", tags=["users"]) api_router.include_router(route_vehicle.router, prefix="/vehicle", tags=["vehicles"]) +api_router.include_router(route_auth.router, prefix="", tags=["auth"]) diff --git a/app/apis/v1/route_auth.py b/app/apis/v1/route_auth.py new file mode 100644 index 0000000..7b45469 --- /dev/null +++ b/app/apis/v1/route_auth.py @@ -0,0 +1,60 @@ +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi import Depends, APIRouter +from sqlalchemy.orm import Session +from fastapi import status, HTTPException +from typing import Annotated +from db.session import get_db +from core.hashing import Hasher +from core.config import settings +from jose import JWTError, jwt +from schemas.token import Token +from db.repository.user import get_user_by_email, get_user_by_phone +from core.auth import create_access_token + +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + + +def authenticate_user(login: str, password: str, db: Session): + print("Trying to auth...") + user = None + if ("@" in login): + user = get_user_by_email(email=login, db=db) + elif ("+" in login): + user = get_user_by_phone(phone=login, db=db) + else: + return False + if not user: + return False + if not Hasher.verify_password(password, user.HashedPassword): + return False + return user + + +def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[Session, Depends(get_db)]): + print("Getting current user...") + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials") + except JWTError: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials") + if ("@" in username): + user = get_user_by_email(email=username, db=db) + elif ("+" in username): + user = get_user_by_phone(phone=username, db=db) + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials") + return user + + +@router.post("/token", response_model=Token) +def access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + print("Getting token...") + user = authenticate_user(form_data.username, form_data.password, db) + print(user) + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password") + access_token = create_access_token(data={"sub": user.Email}) + return {"access_token": access_token, "token_type": "bearer"} diff --git a/app/apis/v1/route_user.py b/app/apis/v1/route_user.py index 8f5b197..ea1a331 100644 --- a/app/apis/v1/route_user.py +++ b/app/apis/v1/route_user.py @@ -2,8 +2,9 @@ from fastapi import APIRouter, HTTPException, status from sqlalchemy.orm import Session from fastapi import Depends -from typing import List - +from typing import List, Annotated +from apis.v1.route_auth import get_current_user +from db.models.user import User from schemas.user import UserCreate, ShowUser from db.session import get_db from db.repository.user import create_new_user, list_users, get_user_by_id @@ -13,20 +14,28 @@ router = APIRouter() @router.post("/", response_model=ShowUser, status_code=status.HTTP_201_CREATED) -def create_user(user: UserCreate, db: Session = Depends(get_db)): +def create_user(user: UserCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if current_user.Role != "Admin": + raise HTTPException(status_code=403, detail="You are not authorized to perform this action") user = create_new_user(user=user, db=db) return user @router.get("/", response_model=List[ShowUser], status_code=status.HTTP_200_OK) def get_all_users(db: Session = Depends(get_db), role: str = None): - if role == None: + if role is None: users = list_users(db=db) return users users = list_users(db=db, role=role) return users +@router.get("/me", response_model=ShowUser, status_code=status.HTTP_200_OK) +def get_user_me(current_user: Annotated[User, Depends(get_current_user)], db: Annotated[Session, Depends(get_db)]): + print("Getting current user...") + return current_user + + @router.get("/{user_id}", response_model=ShowUser, status_code=status.HTTP_200_OK) def get_user(user_id: int, db: Session = Depends(get_db)): user = get_user_by_id(user_id=user_id, db=db) diff --git a/app/apis/v1/route_vehicle.py b/app/apis/v1/route_vehicle.py index 663f3c2..8fb2a81 100644 --- a/app/apis/v1/route_vehicle.py +++ b/app/apis/v1/route_vehicle.py @@ -10,13 +10,18 @@ from db.repository.vehicle import ( list_vehicles, get_vehicle_by_id, replace_vehicle_data, + delete_vehicle_data, ) +from db.models.user import User +from apis.v1.route_auth import get_current_user router = APIRouter() @router.post("/", response_model=OutputVehicle, status_code=status.HTTP_201_CREATED) -async def create_vehicle(vehicle: CreateVehicle, db: Session = Depends(get_db)): +async def create_vehicle(vehicle: CreateVehicle, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if current_user.Role != "Admin": + raise HTTPException(status_code=403, detail="You are not authorized to perform this action") vehicle = create_new_vehicle(vehicle=vehicle, db=db) return vehicle @@ -47,7 +52,9 @@ async def create_vehicle(vehicle: CreateVehicle, db: Session = Depends(get_db)): response_model=OutputVehicle, status_code=status.HTTP_200_OK, ) -async def assign_driver(vehicle_id: int, driver_id: int, db: Session = Depends(get_db)): +async def assign_driver(vehicle_id: int, driver_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if current_user.Role != "Admin": + raise HTTPException(status_code=403, detail="You are not authorized to perform this action") vehicle = assign_vehicle_driver(vehicle_id=vehicle_id, driver_id=driver_id, db=db) if vehicle == "nodriver": raise HTTPException( @@ -85,11 +92,23 @@ async def get_vehicle(vehicle_id: int, db: Session = Depends(get_db)): "/{vehicle_id}", response_model=OutputVehicle, status_code=status.HTTP_200_OK ) def update_vehicle( - vehicle_id: int, vehicle: UpdateVehicle, db: Session = Depends(get_db) + vehicle_id: int, vehicle: UpdateVehicle, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): + if current_user.Role != "Admin": + raise HTTPException(status_code=403, detail="You are not authorized to perform this action") vehicleRes = replace_vehicle_data(id=vehicle_id, vehicle=vehicle, db=db) if vehicleRes == "vehicleNotFound": raise HTTPException(status_code=404, detail="Vehicle not found") elif vehicleRes == "badreq": raise HTTPException(status_code=502, detail="Bad request") return vehicleRes + + +@router.delete("/{vehicle_id}", status_code=status.HTTP_200_OK) +def delete_vehicle(vehicle_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if current_user.Role != "Admin": + raise HTTPException(status_code=403, detail="You are not authorized to perform this action") + result = delete_vehicle_data(id=vehicle_id, db=db) + if result == "vehicleNotFound": + raise HTTPException(status_code=404, detail="Vehicle not found") + return {"msg": "Vehicle deleted successfully"} diff --git a/app/core/auth.py b/app/core/auth.py new file mode 100644 index 0000000..a4a51b0 --- /dev/null +++ b/app/core/auth.py @@ -0,0 +1,16 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import jwt + +from core.config import settings + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return encoded_jwt diff --git a/app/core/config.py b/app/core/config.py index 2309793..c5d24eb 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,12 +1,15 @@ class Settings: - PROJECT_NAME:str = "VMS" - PROJECT_VERSION:str = "1.0.0" - - POSTGRES_USER : str = "VMSBase" + PROJECT_NAME: str = "VMS" + PROJECT_VERSION: str = "1.0.0" + POSTGRES_USER: str = "VMSBase" POSTGRES_PASSWORD = "VMSBasePass" - POSTGRES_SERVER : str = "localhost" - POSTGRES_PORT : str = "5432" - POSTGRES_DB : str = "VMSData" + POSTGRES_SERVER: str = "localhost" + POSTGRES_PORT: str = "5432" + POSTGRES_DB: str = "VMSData" DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:{POSTGRES_PORT}/{POSTGRES_DB}" - -settings = Settings() \ No newline at end of file + ACCESS_TOKEN_EXPIRE: int = 30 + SECRET_KEY: str = "tH357aC6oA7ofCaN3yTffYkRh" + ALGORITHM: str = "HS256" + + +settings = Settings() diff --git a/app/db/base.py b/app/db/base.py index e3b4d1b..e9b7650 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -2,5 +2,3 @@ from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() -from db.models.user import User -from db.models.vehicle import Vehicle diff --git a/app/db/models/user.py b/app/db/models/user.py index 778a467..de4af6a 100644 --- a/app/db/models/user.py +++ b/app/db/models/user.py @@ -1,6 +1,5 @@ # PostgreSQL table model for users -from sqlalchemy import Column, Integer, String, DateTime, Boolean, URL -from sqlalchemy.orm import relationship +from sqlalchemy import Column, Integer, String, DateTime from db.base import Base diff --git a/app/db/models/vehicle.py b/app/db/models/vehicle.py index e9e71e5..ddd386e 100644 --- a/app/db/models/vehicle.py +++ b/app/db/models/vehicle.py @@ -3,13 +3,8 @@ from sqlalchemy import ( Column, Integer, String, - DateTime, - Boolean, - URL, ARRAY, - ForeignKey, ) -from sqlalchemy.orm import relationship from db.base import Base diff --git a/app/db/repository/user.py b/app/db/repository/user.py index 14cb7aa..754b664 100644 --- a/app/db/repository/user.py +++ b/app/db/repository/user.py @@ -28,6 +28,16 @@ def get_user_by_id(user_id: int, db: Session): return user +def get_user_by_email(email: str, db: Session): + user = db.query(User).filter(User.Email == email).first() + return user + + +def get_user_by_phone(phone: str, db: Session): + user = db.query(User).filter(User.ContactNumber == phone).first() + return user + + def verify_driver_exists(driver_id: int, db: Session): driver = db.query(User).filter(User.Id == driver_id).first() if not driver: diff --git a/app/db/repository/vehicle.py b/app/db/repository/vehicle.py index e645f78..0690482 100644 --- a/app/db/repository/vehicle.py +++ b/app/db/repository/vehicle.py @@ -1,5 +1,5 @@ from sqlalchemy.orm import Session -from schemas.vehicle import CreateVehicle, OutputVehicle, UpdateVehicle +from schemas.vehicle import CreateVehicle, UpdateVehicle from db.models.vehicle import Vehicle from db.repository.user import verify_driver_exists @@ -27,7 +27,9 @@ def assign_vehicle_driver(vehicle_id: int, driver_id: int, db: Session): return "alreadyassigned" if verify_driver_exists(driver_id=driver_id, db=db): print(vehicle.AssignedDriverIds) - vehicledb.update({"AssignedDriverIds": vehicle.AssignedDriverIds + [driver_id]}) + vehicledb.update( + {"AssignedDriverIds": vehicle.AssignedDriverIds + [driver_id]} + ) print(vehicle.AssignedDriverIds) db.add(vehicle) db.commit() @@ -65,3 +67,13 @@ def replace_vehicle_data(id: int, vehicle: UpdateVehicle, db: Session): db.add(vehicle_object) db.commit() return vehicle_object + + +def delete_vehicle_data(id: int, db: Session): + vehicle_db = db.query(Vehicle).filter(Vehicle.Id == id) + vehicle_object = vehicle_db.first() + if not vehicle_object: + return "vehiclenotfound" + db.delete(vehicle_object) + db.commit() + return vehicle_object diff --git a/app/schemas/token.py b/app/schemas/token.py new file mode 100644 index 0000000..3fded6a --- /dev/null +++ b/app/schemas/token.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class Token(BaseModel): + access_token: str + token_type: str diff --git a/app/schemas/vehicle.py b/app/schemas/vehicle.py index d14d382..73bba43 100644 --- a/app/schemas/vehicle.py +++ b/app/schemas/vehicle.py @@ -1,6 +1,5 @@ from typing import Optional -from pydantic import BaseModel, root_validator -from datetime import datetime +from pydantic import BaseModel class CreateVehicle(BaseModel): diff --git a/requirements.txt b/requirements.txt index 9af92db..4b80819 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ pydantic sqlalchemy psycopg2 alembic==1.12.0 -passlib \ No newline at end of file +passlib +python-jose==3.3.0 +python-multipart==0.0.6 \ No newline at end of file