Merge pull request 'integ-backoffice' (#6) from integ-backoffice into master

Reviewed-on: #6
This commit is contained in:
v4l3n71n 2023-11-03 20:42:24 +00:00
commit 82750bbd0c
7 changed files with 254 additions and 42 deletions

View File

@ -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

View File

@ -4,18 +4,17 @@ from passlib.context import CryptContext
from pydantic import EmailStr 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) user_repository = users.UserRepository(database=database.database)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
result = user_repository.find_one_by({'username': username}) result = user_repository.find_one_by({'username': username})
change = "added" 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: if result is not None:
result.password=pwd_context.hash(password) result.password=pwd_context.hash(password)
result.roles=roles result.roles=roles
result.disabled=disabled result.status=status
result.confirmed=confirmed
result.email=email result.email=email
user = result user = result
change = "updated" change = "updated"

View File

@ -8,14 +8,14 @@ from jose import JWTError, jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from ..models import users, token from ..models import users, token
from ..dependencies import database from ..dependencies import database, cookie
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256" ALGORITHM = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 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): def verify_password(plain_password, hashed_password):
return pwd_context.verify(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 return False
if not verify_password(password, user.password): if not verify_password(password, user.password):
return False return False
user.connected_at = datetime.today()
user_repository = users.UserRepository(database=database.database)
user_repository.save(user)
return user return user
def create_access_token(data: dict, expires_delta: timedelta | None = None): 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( async def get_current_active_user(
current_user: Annotated[users.User, Depends(get_current_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") raise HTTPException(status_code=400, detail="Inactive user")
return current_user return current_user

View File

@ -1,4 +1,5 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import users, token, mail from .routers import users, token, mail
from .dependencies import user_add from .dependencies import user_add
@ -7,6 +8,19 @@ import os
app = FastAPI() 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(users.router)
app.include_router(token.router) app.include_router(token.router)
app.include_router(mail.router) app.include_router(mail.router)

View File

@ -1,30 +1,39 @@
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
from pydantic_mongo import AbstractRepository, ObjectIdField from pydantic_mongo import AbstractRepository, ObjectIdField
from datetime import datetime, date
class User(BaseModel): class User(BaseModel):
id: ObjectIdField = None id: ObjectIdField = None
username: str username: str
password: str password: str
firstName: str = ""
name: str = ""
roles: str = "User" roles: str = "User"
disabled: bool = False status: int = 0
removed: bool = False
confirmed: bool = False
email: EmailStr 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): class UserOut(BaseModel):
id: ObjectIdField = None id: ObjectIdField = None
username: str username: str
roles: str roles: str
disabled: bool firstName: str
removed: bool name: str
confirmed: bool status: int = 0
email: EmailStr email: EmailStr
class UserIn(BaseModel): class UserIn(BaseModel):
username: str username: str
name: str
firstName: str
roles: str roles: str
password: str password: str
birth: str
email: EmailStr email: EmailStr
@ -37,6 +46,9 @@ class UserCreate(BaseModel):
class UserInDB(User): class UserInDB(User):
password: str password: str
class UserIDS(BaseModel):
ids: list[str]
class UserRepository(AbstractRepository[User]): class UserRepository(AbstractRepository[User]):
class Meta: class Meta:
collection_name = "users" collection_name = "users"

View File

@ -2,17 +2,18 @@ from datetime import datetime, timedelta
from typing import Annotated from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status, APIRouter from fastapi import Depends, FastAPI, HTTPException, status, APIRouter
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from ..dependencies import users_token from ..dependencies import users_token, permissions_checker
from ..models import token from ..models import token, users
router = APIRouter() router = APIRouter()
ACCESS_TOKEN_EXPIRE_MINUTES = 30 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( 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) user = users_token.authenticate_user(form_data.username, form_data.password)
if not user: if not user:
raise HTTPException( raise HTTPException(
@ -24,4 +25,20 @@ async def login_for_access_token(
access_token = users_token.create_access_token( access_token = users_token.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires data={"sub": user.username}, expires_delta=access_token_expires
) )
return {"access_token": access_token, "token_type": "bearer"} 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

View File

@ -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 ..dependencies import users_token, permissions_checker, database
from ..models import users from ..models import users
from pydantic import EmailStr
from typing import Annotated from typing import Annotated
from bson import ObjectId from bson import ObjectId
router = APIRouter() router = APIRouter()
@ -8,7 +11,7 @@ router = APIRouter()
@router.get("/users", tags=["users"], response_model=list[users.UserOut]) @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: if limit < 1 or skip < 0 or limit < skip:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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 limit = limit + skip
listUsers = [] listUsers = []
user_repository = users.UserRepository(database=database.database) user_repository = users.UserRepository(database=database.database)
for user_index in user_repository.find_by({}, limit=limit, skip=skip): object_search = {}
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) 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) listUsers.append(user)
return listUsers return listUsers
@router.get("/users/search", tags=["users"], response_model=list[users.UserOut]) @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: if limit < 1 or skip < 0 or limit < skip:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -38,15 +70,23 @@ async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.P
listUsers = [] listUsers = []
user_repository = users.UserRepository(database=database.database) user_repository = users.UserRepository(database=database.database)
for user_index in user_repository.find_by({key: {'$regex': value}}, limit=limit, skip=skip): 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) listUsers.append(user)
return listUsers 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"]))]): 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 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) @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"]))]): 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_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 return user
@router.delete("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "status"])
@router.delete("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "disabled"]) 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):
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):
user_repository = users.UserRepository(database=database.database) user_repository = users.UserRepository(database=database.database)
current_user.disabled = True current_user.status = 0
if remove is True: if remove is True:
current_user.removed = True current_user.status = -1
user_repository.save(current_user) user_repository.save(current_user)
return 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) @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_repository = users.UserRepository(database=database.database)
user = user_repository.find_one_by_id(ObjectId(item_id)) user = user_repository.find_one_by_id(ObjectId(item_id))
user.disabled = True user.status = 0
if remove is True: if remove is True:
user.removed = True user.status = -1
user_repository.save(user) user_repository.save(user)
return user return user
@router.put("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["id", "password", "roles", "disabled"]) @router.put("/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"]))], userSingle: users.UserIn | None = None): 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) user_repository = users.UserRepository(database=database.database)
current_user.username = userSingle.username current_user.username = userSingle.username
current_user.password = user_token.get_password_hash(userSingle.password) 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) user_repository.save(current_user)
return current_user return current_user
@router.put("/users", tags=["users"], response_model=users.User, status_code=status.HTTP_200_OK) @router.put("/users", tags=["users"], response_model=users.User, status_code=status.HTTP_201_CREATED)
async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin"]))], userSingle: users.UserIn | None = None): 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: if userSingle is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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_repository = users.UserRepository(database=database.database)
user = user_repository.find_one_by({"username": {'$eq': userSingle.username}}) user = user_repository.find_one_by({"$or":[{"username": {'$eq': userSingle.username}}, {"email": {"$eq": userSingle.email}}]})
if user is None: if user is not None:
response.status_code = status.HTTP_201_CREATED 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.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.roles = userSingle.roles
user.email = userSingle.email 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) user_repository.save(user)
return user return user