diff --git a/app/dependencies/cookie.py b/app/dependencies/cookie.py new file mode 100644 index 0000000..2078dac --- /dev/null +++ b/app/dependencies/cookie.py @@ -0,0 +1,37 @@ +from fastapi.security import OAuth2 +from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel +from fastapi import Request +from fastapi.security.utils import get_authorization_scheme_param +from fastapi import HTTPException +from fastapi import status +from typing import Optional +from typing import Dict + + +class OAuth2PasswordBearerWithCookie(OAuth2): + def __init__( + self, + tokenUrl: str, + scheme_name: Optional[str] = None, + scopes: Optional[Dict[str, str]] = None, + auto_error: bool = True, + ): + if not scopes: + scopes = {} + flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) + super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) + + async def __call__(self, request: Request) -> Optional[str]: + authorization: str = request.cookies.get("access_token") #changed to accept access token from httpOnly Cookie + + scheme, param = get_authorization_scheme_param(authorization) + if not authorization or scheme.lower() != "bearer": + if self.auto_error: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + else: + return None + return param \ No newline at end of file diff --git a/app/dependencies/user_add.py b/app/dependencies/user_add.py index fae97c5..9c8e85d 100644 --- a/app/dependencies/user_add.py +++ b/app/dependencies/user_add.py @@ -4,18 +4,17 @@ from passlib.context import CryptContext from pydantic import EmailStr -def add(username="", password="", roles="User", disabled=False, confirmed=True, email="test@toto.com"): +def add(username="", password="", roles="User", status=1, email="test@toto.com"): 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, confirmed=confirmed, email=email) + user = users.User(username=username, password=pwd_context.hash(password), roles=roles, status=status, email=email) if result is not None: result.password=pwd_context.hash(password) result.roles=roles - result.disabled=disabled - result.confirmed=confirmed + result.status=status result.email=email user = result change = "updated" diff --git a/app/dependencies/users_token.py b/app/dependencies/users_token.py index 341075e..b494a37 100644 --- a/app/dependencies/users_token.py +++ b/app/dependencies/users_token.py @@ -8,14 +8,14 @@ from jose import JWTError, jwt from passlib.context import CryptContext from ..models import users, token -from ..dependencies import database +from ..dependencies import database, cookie SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" ALGORITHM = "HS256" pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +oauth2_scheme = cookie.OAuth2PasswordBearerWithCookie(tokenUrl="token") def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) @@ -35,6 +35,9 @@ def authenticate_user(username: str, password: str): return False if not verify_password(password, user.password): return False + user.connected_at = datetime.today() + user_repository = users.UserRepository(database=database.database) + user_repository.save(user) return user def create_access_token(data: dict, expires_delta: timedelta | None = None): @@ -70,6 +73,6 @@ async def get_current_user(token_str: Annotated[str, Depends(oauth2_scheme)]): async def get_current_active_user( current_user: Annotated[users.User, Depends(get_current_user)] ): - if current_user.disabled: + if current_user.status == 0: raise HTTPException(status_code=400, detail="Inactive user") return current_user \ No newline at end of file diff --git a/app/main.py b/app/main.py index 55aaf51..8e8d558 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from .routers import users, token, mail from .dependencies import user_add @@ -7,6 +8,19 @@ import os app = FastAPI() +origins = [ + "http://localhost:8084", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + app.include_router(users.router) app.include_router(token.router) app.include_router(mail.router) diff --git a/app/models/users.py b/app/models/users.py index 4912304..9d71f9d 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -1,30 +1,39 @@ from pydantic import BaseModel, EmailStr from pydantic_mongo import AbstractRepository, ObjectIdField +from datetime import datetime, date class User(BaseModel): id: ObjectIdField = None username: str password: str + firstName: str = "" + name: str = "" roles: str = "User" - disabled: bool = False - removed: bool = False - confirmed: bool = False + status: int = 0 email: EmailStr + birth: str | None = None + created_at: datetime = datetime.today() + connected_at: datetime | None = None + updated_at: datetime = datetime.today() + deleted_at: datetime | None = None class UserOut(BaseModel): id: ObjectIdField = None username: str roles: str - disabled: bool - removed: bool - confirmed: bool + firstName: str + name: str + status: int = 0 email: EmailStr class UserIn(BaseModel): username: str + name: str + firstName: str roles: str password: str + birth: str email: EmailStr @@ -37,6 +46,9 @@ class UserCreate(BaseModel): class UserInDB(User): password: str +class UserIDS(BaseModel): + ids: list[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 7ef5419..037f6e6 100644 --- a/app/routers/token.py +++ b/app/routers/token.py @@ -2,17 +2,18 @@ from datetime import datetime, timedelta from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, status, APIRouter +from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from ..dependencies import users_token -from ..models import token +from ..dependencies import users_token, permissions_checker +from ..models import token, users + router = APIRouter() ACCESS_TOKEN_EXPIRE_MINUTES = 30 -@router.post("/token", response_model=token.Token, tags=["token"]) +@router.post("/token", tags=["token"]) async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): + form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): user = users_token.authenticate_user(form_data.username, form_data.password) if not user: raise HTTPException( @@ -24,4 +25,20 @@ async def login_for_access_token( access_token = users_token.create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} \ No newline at end of file + content = {"roles":user.roles,"message": "Access token generated"} + response = JSONResponse(content=content) + response.set_cookie(key="access_token", value="Bearer {0}".format(access_token), httponly=True) + return response + +@router.get("/token",tags=["token"]) +async def check_token(current_user: Annotated[users.User, Depends(users_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))]): + content = {"message": "Check token"} + response = JSONResponse(content=content) + return response + +@router.delete("/token",tags=["token"]) +async def check_token(current_user: Annotated[users.User, Depends(users_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))]): + content = {"message": "Token deleted"} + response = JSONResponse(content=content) + response.set_cookie(key="access_token", value="", httponly=True) + return response \ No newline at end of file diff --git a/app/routers/users.py b/app/routers/users.py index 6b79929..309ce13 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,6 +1,9 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Response +from fastapi.responses import JSONResponse +from datetime import datetime from ..dependencies import users_token, permissions_checker, database from ..models import users +from pydantic import EmailStr from typing import Annotated from bson import ObjectId router = APIRouter() @@ -8,7 +11,7 @@ router = APIRouter() @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): +async def read_users(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], skip: int = 0, limit: int = 20, id_user: str | None = None, roles: str | None = None, status: int | None = None, email: EmailStr | None = None): if limit < 1 or skip < 0 or limit < skip: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -17,13 +20,42 @@ async def read_users(authorize: Annotated[bool, Depends(permissions_checker.Perm 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, email=user_index.email, disabled=user_index.disabled, roles=user_index.roles, removed=user_index.removed, confirmed=user_index.confirmed) + object_search = {} + if status is not None and roles is not None: + object_search = {"$and":[{"roles":{"$eq": roles}}, {"status":{"$eq":status}}]} + else: + if status is not None: + object_search = {"status":{"$eq": status}} + if roles is not None: + object_search = {"roles":{"$eq":roles}} + if id_user is not None: + userid = ObjectId(id_user) + object_search = {"id": {"$regex": userid}} + if status is not None and roles is not None: + object_search = {"$and":[{"id":{"$regex": userid}}, {"roles":{"$eq": roles}}, {"status":{"$eq":status}}]} + else: + if status is not None: + object_search = {"$and":[{"id":{"$regex": userid}}, {"status":{"$eq":status}}]} + if roles is not None: + object_search = {"$and":[{"id":{"$regex": userid}}, {"roles":{"$eq":roles}}]} + if email is not None: + object_search = {"email": {"$eq": email}} + if status is not None and roles is not None: + object_search = {"$and":[{"email":{"$eq": email}}, {"roles":{"$eq": roles}}, {"status":{"$eq":status}}]} + else: + if status is not None: + object_search = {"$and":[{"email":{"$eq": email}}, {"status":{"$eq":status}}]} + if roles is not None: + object_search = {"$and":[{"email":{"$eq": email}}, {"roles":{"$eq":roles}}]} + + + for user_index in user_repository.find_by(object_search, limit=limit, skip=skip): + user = users.UserOut(id=user_index.id, username=user_index.username, email=user_index.email, status=user_index.status, roles=user_index.roles, firstName=user_index.firstName, name=user_index.name) listUsers.append(user) return listUsers @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): +async def read_users_search(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], skip: int = 0, limit: int = 20): if limit < 1 or skip < 0 or limit < skip: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -38,15 +70,23 @@ async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.P 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, email=user_index.email, removed=user_index.removed, confirmed=user_index.confirmed) + user = users.UserOut(id=user_index.id, username=user_index.username, status=user_index.status, roles=user_index.roles, email=user_index.email, firstName=user_index.firstName, name=user_index.name) listUsers.append(user) return listUsers -@router.get("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "disabled"]) +@router.get("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "status"]) async def read_users_me(current_user: Annotated[users.User, Depends(users_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))]): return current_user +@router.get("/users/count", tags=["users"]) +async def read_users_count(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))]): + count = database.database.get_collection("users").estimated_document_count() + content = {"count":count} + response = JSONResponse(content=content) + return response + + @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) @@ -54,28 +94,49 @@ async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permis return user - -@router.delete("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "disabled"]) -async def read_users_me(current_user: Annotated[users.User, Depends(users_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], remove: bool = False): +@router.delete("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "status"]) +async def delete_users_me(current_user: Annotated[users.User, Depends(users_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], remove: bool = False): user_repository = users.UserRepository(database=database.database) - current_user.disabled = True + current_user.status = 0 if remove is True: - current_user.removed = True + current_user.status = -1 user_repository.save(current_user) return current_user +@router.delete("/users/groups",tags=["users"]) +async def delete_users_groups(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], remove: bool = False, userids: users.UserIDS | None = None): + if len(userids.ids) == 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="userids should be greater than 0" + ) + + user_repository = users.UserRepository(database=database.database) + content = {"message": "users are disabled"} + for i in userids.ids: + user = user_repository.find_one_by_id(ObjectId(i)) + user.status = 0 + if remove is True: + user.status = -1 + content = {"message": "users are deleted "} + user_repository.save(user) + + + response = JSONResponse(content=content) + return response + @router.delete("/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"]))], remove : bool = False): +async def delete_users_id(item_id : str, authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], remove : bool = False): user_repository = users.UserRepository(database=database.database) user = user_repository.find_one_by_id(ObjectId(item_id)) - user.disabled = True + user.status = 0 if remove is True: - user.removed = True + user.status = -1 user_repository.save(user) return user -@router.put("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "disabled"]) -async def read_users_me(current_user: Annotated[users.User, Depends(users_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], userSingle: users.UserIn | None = None): +@router.put("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "status"]) +async def update_users_me(current_user: Annotated[users.User, Depends(users_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))], userSingle: users.UserIn | None = None): user_repository = users.UserRepository(database=database.database) current_user.username = userSingle.username current_user.password = user_token.get_password_hash(userSingle.password) @@ -84,8 +145,8 @@ async def read_users_me(current_user: Annotated[users.User, Depends(users_token. user_repository.save(current_user) return current_user -@router.put("/users", tags=["users"], response_model=users.User, status_code=status.HTTP_200_OK) -async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], userSingle: users.UserIn | None = None): +@router.put("/users", tags=["users"], response_model=users.User, status_code=status.HTTP_201_CREATED) +async def update_users(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], userSingle: users.UserIn | None = None, response: Response = Response): if userSingle is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -93,13 +154,82 @@ async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.P ) user_repository = users.UserRepository(database=database.database) - user = user_repository.find_one_by({"username": {'$eq': userSingle.username}}) - if user is None: + user = user_repository.find_one_by({"$or":[{"username": {'$eq': userSingle.username}}, {"email": {"$eq": userSingle.email}}]}) + if user is not None: response.status_code = status.HTTP_201_CREATED - user = users.User() + if user.username == userSingle.username: + raise HTTPException( + status_code=status.HTTP_204_NO_CONTENT, + detail="username" + ) + if user.email == userSingle.email: + raise HTTPException( + status_code=status.HTTP_204_NO_CONTENT, + detail="email" + ) user.username = userSingle.username - user.password = user_token.get_password_hash(userSingle.password) + user.password = users_token.get_password_hash(userSingle.password) user.roles = userSingle.roles user.email = userSingle.email + user.firstName = userSingle.firstName + user.name = userSingle.name + user.birth = userSingle.birth + user.updated_at = datetime.today() + user_repository.save(user) + return user + + +@router.put("/users/{item_id}", tags=["users"], response_model=users.User, status_code=status.HTTP_200_OK) +async def update_users_id(item_id: str, authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], userSingle: users.UserIn | None = None, response: Response = Response): + if userSingle is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Body request is empty" + ) + user_repository = users.UserRepository(database=database.database) + + user = user_repository.find_one_by({"id": {'$eq': ObjectId(item_id)}}) + if user is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + user.username = userSingle.username + user.password = users_token.get_password_hash(userSingle.password) + user.roles = userSingle.roles + user.email = userSingle.email + user.firstName = userSingle.firstName + user.name = userSingle.name + user.birth = userSingle.birth + user.updated_at = datetime.today() + user_repository.save(user) + return user + + +@router.patch("/users/groups",tags=["users"]) +async def patch_users_groups(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], userids: users.UserIDS | None = None): + if len(userids.ids) == 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="userids should be greater than 0" + ) + + user_repository = users.UserRepository(database=database.database) + content = {"message": "users are enabled"} + for i in userids.ids: + user = user_repository.find_one_by_id(ObjectId(i)) + user.status = 1 + user_repository.save(user) + + + response = JSONResponse(content=content) + return response + +@router.patch("/users/{item_id}", tags=["users"], response_model=users.User) +async def patch_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)) + user.status = 1 user_repository.save(user) return user \ No newline at end of file