Merge pull request 'users' (#3) from users into master

Reviewed-on: #3
This commit is contained in:
v4l3n71n 2023-10-17 12:05:41 +00:00
commit 8724b1e93a
13 changed files with 187 additions and 20 deletions

View File

@ -1,5 +1,7 @@
from pymongo import MongoClient from pymongo import MongoClient
import os import os, redis
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"])) 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"]] database = client[os.environ["MONGO_DATABASE"]]
connect_redis = redis.Redis(host=os.environ["REDIS_URL"], port=os.environ["REDIS_PORT"], decode_responses=True)

14
app/dependencies/mail.py Normal file
View File

@ -0,0 +1,14 @@
from fastapi_mail import ConnectionConfig
from pathlib import Path
import os
conf = ConnectionConfig(
MAIL_USERNAME = os.environ["MAILER_USERNAME"],
MAIL_PASSWORD = os.environ["MAILER_PASSWORD"],
MAIL_FROM = os.environ["MAILER_FROM"],
MAIL_PORT = os.environ["MAILER_PORT"],
MAIL_SERVER = os.environ["MAILER_HOST"],
MAIL_STARTTLS = True,
MAIL_SSL_TLS = False,
TEMPLATE_FOLDER = Path(__file__).parents[1] / 'templates',
)

View File

@ -1,4 +1,4 @@
from ..dependencies import users_active from ..dependencies import users_token
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from ..models import users from ..models import users
@ -8,7 +8,7 @@ class PermissionChecker:
def __init__(self, roles: list[str]) -> None: def __init__(self, roles: list[str]) -> None:
self.roles = roles self.roles = roles
def __call__(self, user: users.User = Depends(users_active.get_current_active_user)) -> bool: def __call__(self, user: users.User = Depends(users_token.get_current_active_user)) -> bool:
for role in self.roles: for role in self.roles:
if role == user.roles: if role == user.roles:
return True return True

View File

@ -1,19 +1,22 @@
from ..models import users from ..models import users
from ..dependencies import database from ..dependencies import database
from passlib.context import CryptContext from passlib.context import CryptContext
from pydantic import EmailStr
def add(username="", password="", roles="User", disabled=False): def add(username="", password="", roles="User", disabled=False, confirmed=True, 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) user = users.User(username=username, password=pwd_context.hash(password), roles=roles, disabled=disabled, confirmed=confirmed, 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.disabled=disabled
result.confirmed=confirmed
result.email=email
user = result user = result
change = "updated" change = "updated"
user_repository.save(user) user_repository.save(user)

View File

@ -1,12 +1,13 @@
from fastapi import FastAPI from fastapi import FastAPI
from .routers import users, token from .routers import users, token, mail
from .dependencies import user_add from .dependencies import user_add
app = FastAPI() app = FastAPI()
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.on_event("startup") @app.on_event("startup")

7
app/models/email.py Normal file
View File

@ -0,0 +1,7 @@
from pydantic import BaseModel, EmailStr
from typing import List, Any, Dict
class EmailSchema(BaseModel):
email: List[EmailStr]
body: Dict[str, Any]

View File

@ -1,18 +1,38 @@
from pydantic import BaseModel from pydantic import BaseModel, EmailStr
from pydantic_mongo import AbstractRepository, ObjectIdField from pydantic_mongo import AbstractRepository, ObjectIdField
class User(BaseModel): class User(BaseModel):
id: ObjectIdField = None id: ObjectIdField = None
username: str username: str
password: str password: str
roles: str roles: str = "User"
disabled: bool disabled: bool = False
removed: bool = False
confirmed: bool = False
email: EmailStr
class UserOut(BaseModel): class UserOut(BaseModel):
id: ObjectIdField = None id: ObjectIdField = None
username: str username: str
roles: str roles: str
disabled: bool disabled: bool
removed: bool
confirmed: bool
email: EmailStr
class UserIn(BaseModel):
username: str
roles: str
password: str
email: EmailStr
class UserCreate(BaseModel):
username: str
password: str
email: EmailStr
class UserInDB(User): class UserInDB(User):
password: str password: str

60
app/routers/mail.py Normal file
View File

@ -0,0 +1,60 @@
from fastapi import APIRouter, HTTPException, status
from ..dependencies import users_token, database, mail
from ..models import users, email
from fastapi.responses import JSONResponse
from fastapi_mail import MessageSchema, MessageType, FastMail
import random
router = APIRouter()
@router.post("/mail",tags=["mail"])
async def create_user(userSingle: users.UserCreate | None = None):
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({"$or": [{"username": {'$eq': userSingle.username}},{"email": {"$eq": userSingle.email}}] })
if user is not None:
raise HTTPException(
status_code=status.HTTP_204_NO_CONTENT,
detail="User is already exist"
)
fm = FastMail(mail.conf)
numberkey = str(random.Random())
key_hashed = users_token.get_password_hash(numberkey)
email_body = {"key":key_hashed, "username":userSingle.username}
email_schema = email.EmailSchema(email=[userSingle.email], body=email_body)
message = MessageSchema(
subject="Fastapi-Mail module",
recipients=email_schema.dict().get("email"),
template_body=email_schema.dict().get("body"),
subtype=MessageType.html,
)
await fm.send_message(message, template_name="mailer.html")
current_user = users.User(username=userSingle.username, password=users_token.get_password_hash(userSingle.password), email=userSingle.email)
user_repository.save(current_user)
database.connect_redis.set(userSingle.username, key_hashed)
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": "email has been sent"})
@router.get("/mail",tags=["mail"])
async def confirm_user(key: str | None = None, username: str | None = None):
if key is None or username is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Parameter key or/and username is empty"
)
user_repository = users.UserRepository(database=database.database)
user = user_repository.find_one_by({"username": {'$eq': username}})
key_hashed = database.connect_redis.get(username)
if key_hashed != key:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Key is invalid"
)
user.confirmed = True
user_repository.save(user)
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": "user account confirmed"})

View File

@ -3,7 +3,7 @@ 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.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from ..dependencies import users_active from ..dependencies import users_token
from ..models import token from ..models import token
router = APIRouter() router = APIRouter()
@ -13,7 +13,7 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30
async def login_for_access_token( async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()] form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
): ):
user = users_active.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(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -21,7 +21,7 @@ async def login_for_access_token(
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = users_active.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"} return {"access_token": access_token, "token_type": "bearer"}

View File

@ -1,11 +1,12 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from ..dependencies import users_active, permissions_checker, database from ..dependencies import users_token, permissions_checker, database
from ..models import users from ..models import users
from typing import Annotated from typing import Annotated
from bson import ObjectId from bson import ObjectId
router = APIRouter() 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):
if limit < 1 or skip < 0 or limit < skip: if limit < 1 or skip < 0 or limit < skip:
@ -17,7 +18,7 @@ async def read_users(authorize: Annotated[bool, Depends(permissions_checker.Perm
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): 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) 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)
listUsers.append(user) listUsers.append(user)
return listUsers return listUsers
@ -37,13 +38,13 @@ 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) 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)
listUsers.append(user) listUsers.append(user)
return listUsers return listUsers
@router.get("/users/me",tags=["users"], response_model=users.User, response_model_exclude=["password"]) @router.get("/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_active.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/{item_id}", tags=["users"], response_model=users.User) @router.get("/users/{item_id}", tags=["users"], response_model=users.User)
@ -51,3 +52,54 @@ async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permis
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))
return user 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):
user_repository = users.UserRepository(database=database.database)
current_user.disabled = True
if remove is True:
current_user.removed = True
user_repository.save(current_user)
return current_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):
user_repository = users.UserRepository(database=database.database)
user = user_repository.find_one_by_id(ObjectId(item_id))
user.disabled = True
if remove is True:
user.removed = True
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):
user_repository = users.UserRepository(database=database.database)
current_user.username = userSingle.username
current_user.password = user_token.get_password_hash(userSingle.password)
current_user.roles = userSingle.roles
current_user.email = userSingle.email
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):
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({"username": {'$eq': userSingle.username}})
if user is None:
response.status_code = status.HTTP_201_CREATED
user = users.User()
user.username = userSingle.username
user.password = user_token.get_password_hash(userSingle.password)
user.roles = userSingle.roles
user.email = userSingle.email
user_repository.save(user)
return user

View File

@ -0,0 +1,6 @@
<html>
<head><title>Email</title></head>
<body><p>Voici un lien https://localhost:8080/api/mail?key={{ key }}&username={{ username }}
</p></body>
</html>

View File

@ -4,3 +4,5 @@ pydantic-mongo
python-jose[cryptography] python-jose[cryptography]
passlib[bcrypt] passlib[bcrypt]
python-multipart python-multipart
fastapi-mail
redis