diff --git a/.gitignore b/.gitignore index ed8ebf5..6d17870 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__ \ No newline at end of file +__pycache__ +.env \ No newline at end of file diff --git a/app/dependencies/database.py b/app/dependencies/database.py new file mode 100644 index 0000000..7ff0550 --- /dev/null +++ b/app/dependencies/database.py @@ -0,0 +1,5 @@ +from pymongo import MongoClient +import os + +client = MongoClient("mongodb+srv://{0}:{1}@{2}/?retryWrites=true&w=majority&appName=AtlasApp".format(os.environ["MONGO_USER"], os.environ["MONGO_PASSWORD"], os.environ["MONGO_HOST"])) +database = client[os.environ["MONGO_DATABASE"]] \ No newline at end of file diff --git a/app/dependencies/user_add.py b/app/dependencies/user_add.py new file mode 100644 index 0000000..198c10f --- /dev/null +++ b/app/dependencies/user_add.py @@ -0,0 +1,20 @@ +from ..models import users +from ..dependencies import database +from passlib.context import CryptContext + + +def add(username="", password="", roles="User", disabled=False): + user_repository = users.UserRepository(database=database.database) + + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + result = user_repository.find_one_by({'username': username}) + change = "added" + user = users.User(username=username, password=pwd_context.hash(password), roles=roles, disabled=disabled) + if result is not None: + result.password=pwd_context.hash(password) + result.roles=roles + result.disabled=disabled + user = result + change = "updated" + user_repository.save(user) + print("{0} {1}".format(username, change)) diff --git a/app/dependencies/users_active.py b/app/dependencies/users_active.py index 9ca2b78..341075e 100644 --- a/app/dependencies/users_active.py +++ b/app/dependencies/users_active.py @@ -1,27 +1,17 @@ from datetime import datetime, timedelta from typing import Annotated +from pymongo import MongoClient from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from passlib.context import CryptContext from ..models import users, token - - -fake_users = [ - # password foo - {'id': 1, 'username': 'admin', 'password': '$2b$12$N.i74Kle18n5Toxhas.rVOjZreVC2WM34fCidNDyhSNgxVlbKwX7i', - 'roles': 'Admin', 'disabled': False - }, - # password bar - {'id': 2, 'username': 'client', 'password': '$2b$12$KUgpw1m0LF/s9NS1ZB5rRO2cA5D13MqRm56ab7ik2ixftXW/aqEyq', - 'roles':'User', 'disabled':False} -] +from ..dependencies import database SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -33,13 +23,14 @@ def verify_password(plain_password, hashed_password): def get_password_hash(password): return pwd_context.hash(password) -def get_user(db, username: str): - for user in db: - if username == user['username']: - return users.UserInDB(**user) +def get_user(username: str): + user_repository = users.UserRepository(database=database.database) + user = user_repository.find_one_by({'username': username}) + return user -def authenticate_user(fake_db, username: str, password: str): - user = get_user(fake_db, username) +def authenticate_user(username: str, password: str): + + user = get_user(username) if not user: return False if not verify_password(password, user.password): @@ -70,7 +61,8 @@ async def get_current_user(token_str: Annotated[str, Depends(oauth2_scheme)]): token_data = token.TokenData(username=username) except JWTError: raise credentials_exception - user = get_user(fake_users, username=token_data.username) + + user = get_user(token_data.username) if user is None: raise credentials_exception return user diff --git a/app/main.py b/app/main.py index 28a1294..8297dde 100644 --- a/app/main.py +++ b/app/main.py @@ -1,13 +1,19 @@ from fastapi import FastAPI from .routers import users, token - +from .dependencies import user_add app = FastAPI() app.include_router(users.router) app.include_router(token.router) + +@app.on_event("startup") +async def startup_event(): + user_add.add(username="Peter93", password="toto", roles="Admin") + user_add.add(username="Robert80", password="titi", roles="User") + @app.get("/") async def root(): return {"message": "Hello World !"} \ No newline at end of file diff --git a/app/models/users.py b/app/models/users.py index de11fcd..743a924 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -1,12 +1,22 @@ from pydantic import BaseModel - +from pydantic_mongo import AbstractRepository, ObjectIdField class User(BaseModel): - id: int + id: ObjectIdField = None username: str password: str roles: str disabled: bool +class UserOut(BaseModel): + id: ObjectIdField = None + username: str + roles: str + disabled: bool + class UserInDB(User): - password: str \ No newline at end of file + password: str + +class UserRepository(AbstractRepository[User]): + class Meta: + collection_name = "users" \ No newline at end of file diff --git a/app/routers/token.py b/app/routers/token.py index 4d99f28..55ffdc4 100644 --- a/app/routers/token.py +++ b/app/routers/token.py @@ -7,20 +7,20 @@ from ..dependencies import users_active from ..models import token router = APIRouter() - +ACCESS_TOKEN_EXPIRE_MINUTES = 30 @router.post("/token", response_model=token.Token, tags=["token"]) async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] ): - user = users_active.authenticate_user(users_active.fake_users, form_data.username, form_data.password) + user = users_active.authenticate_user(form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) - access_token_expires = timedelta(minutes=users_active.ACCESS_TOKEN_EXPIRE_MINUTES) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = users_active.create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) diff --git a/app/routers/users.py b/app/routers/users.py index 0ec1ccf..f97970c 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,15 +1,53 @@ -from fastapi import APIRouter, Depends -from ..dependencies import users_active, permissions_checker +from fastapi import APIRouter, Depends, HTTPException, status +from ..dependencies import users_active, permissions_checker, database from ..models import users from typing import Annotated - +from bson import ObjectId router = APIRouter() -@router.get("/users/", tags=["users"], response_model=list[users.User]) -async def read_users(current_user: Annotated[users.User, Depends(users_active.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))]): - return users_active.fake_users +@router.get("/users", tags=["users"], response_model=list[users.UserOut]) +async def read_users(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], skip: int = 0, limit: int = 20): + if limit < 1 or skip < 0 or limit < skip: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="skip should be greater than 0 and limit should be greater than 1. Limit should be greater than skip" + ) + limit = limit + skip + listUsers = [] + user_repository = users.UserRepository(database=database.database) + for user_index in user_repository.find_by({}, limit=limit, skip=skip): + user = users.UserOut(id=user_index.id, username=user_index.username, disabled=user_index.disabled, roles=user_index.roles) + listUsers.append(user) + return listUsers -@router.get("/users/me",tags=["users"], response_model=users.User) +@router.get("/users/search", tags=["users"], response_model=list[users.UserOut]) +async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], skip: int = 0, limit: int = 20, key: str | None = None, value: str | None= None): + if limit < 1 or skip < 0 or limit < skip: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="skip should be greater than 0 and limit should be greater than 1. Limit should be greater than skip" + ) + if key is None or value is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Key or/and value parameter is empty" + ) + limit = limit + skip + listUsers = [] + user_repository = users.UserRepository(database=database.database) + for user_index in user_repository.find_by({key: {'$regex': value}}, limit=limit, skip=skip): + user = users.UserOut(id=user_index.id, username=user_index.username, disabled=user_index.disabled, roles=user_index.roles) + listUsers.append(user) + return listUsers + + +@router.get("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["password"]) async def read_users_me(current_user: Annotated[users.User, Depends(users_active.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))]): - return current_user \ No newline at end of file + return current_user + +@router.get("/users/{item_id}", tags=["users"], response_model=users.User) +async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))]): + user_repository = users.UserRepository(database=database.database) + user = user_repository.find_one_by_id(ObjectId(item_id)) + return user