commit
8724b1e93a
@ -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
14
app/dependencies/mail.py
Normal 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',
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
7
app/models/email.py
Normal 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]
|
@ -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
60
app/routers/mail.py
Normal 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"})
|
@ -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"}
|
@ -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
|
6
app/templates/mailer.html
Normal file
6
app/templates/mailer.html
Normal 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>
|
@ -4,3 +4,5 @@ pydantic-mongo
|
|||||||
python-jose[cryptography]
|
python-jose[cryptography]
|
||||||
passlib[bcrypt]
|
passlib[bcrypt]
|
||||||
python-multipart
|
python-multipart
|
||||||
|
fastapi-mail
|
||||||
|
redis
|
Loading…
x
Reference in New Issue
Block a user