From f2e7ca1e93d8de0c872f18b6c4f63c957858d789 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 17:34:34 +0200 Subject: [PATCH 01/17] rename users_active to users_token --- app/dependencies/permissions_checker.py | 4 ++-- app/dependencies/{users_active.py => users_token.py} | 0 app/routers/token.py | 6 +++--- app/routers/users.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename app/dependencies/{users_active.py => users_token.py} (100%) diff --git a/app/dependencies/permissions_checker.py b/app/dependencies/permissions_checker.py index 896679f..9c5e414 100644 --- a/app/dependencies/permissions_checker.py +++ b/app/dependencies/permissions_checker.py @@ -1,4 +1,4 @@ -from ..dependencies import users_active +from ..dependencies import users_token from fastapi import Depends, HTTPException, status from ..models import users @@ -8,7 +8,7 @@ class PermissionChecker: def __init__(self, roles: list[str]) -> None: 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: if role == user.roles: return True diff --git a/app/dependencies/users_active.py b/app/dependencies/users_token.py similarity index 100% rename from app/dependencies/users_active.py rename to app/dependencies/users_token.py diff --git a/app/routers/token.py b/app/routers/token.py index 55ffdc4..7ef5419 100644 --- a/app/routers/token.py +++ b/app/routers/token.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, status, APIRouter from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from ..dependencies import users_active +from ..dependencies import users_token from ..models import token router = APIRouter() @@ -13,7 +13,7 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30 async def login_for_access_token( 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: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -21,7 +21,7 @@ async def login_for_access_token( headers={"WWW-Authenticate": "Bearer"}, ) 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 ) return {"access_token": access_token, "token_type": "bearer"} \ No newline at end of file diff --git a/app/routers/users.py b/app/routers/users.py index f97970c..8b1b7be 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,5 +1,5 @@ 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 typing import Annotated from bson import ObjectId @@ -43,7 +43,7 @@ async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.P @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"]))]): +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/{item_id}", tags=["users"], response_model=users.User) From 9bfd25a908647501c84b956fc9c4b3d69721f1a2 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 17:41:34 +0200 Subject: [PATCH 02/17] hide many users info from endpoint me --- app/routers/users.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routers/users.py b/app/routers/users.py index 8b1b7be..702dbe5 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -6,6 +6,8 @@ from bson import ObjectId 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): if limit < 1 or skip < 0 or limit < skip: @@ -42,7 +44,7 @@ async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.P 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_token.get_current_active_user)], authorize: Annotated[bool, Depends(permissions_checker.PermissionChecker(roles=["Admin", "User"]))]): return current_user From ecc6e7642f216af33f0ba29ae9975a7a0adfe680 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 18:20:08 +0200 Subject: [PATCH 03/17] add deleted method --- app/dependencies/user_add.py | 5 +++-- app/models/users.py | 6 +++++- app/routers/users.py | 25 +++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/app/dependencies/user_add.py b/app/dependencies/user_add.py index 198c10f..74cc7f4 100644 --- a/app/dependencies/user_add.py +++ b/app/dependencies/user_add.py @@ -3,17 +3,18 @@ from ..dependencies import database from passlib.context import CryptContext -def add(username="", password="", roles="User", disabled=False): +def add(username="", password="", roles="User", disabled=False, confirmed=True): 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) + user = users.User(username=username, password=pwd_context.hash(password), roles=roles, disabled=disabled, confirmed=confirmed) if result is not None: result.password=pwd_context.hash(password) result.roles=roles result.disabled=disabled + result.confirmed=confirmed user = result change = "updated" user_repository.save(user) diff --git a/app/models/users.py b/app/models/users.py index 743a924..01dd92b 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -6,13 +6,17 @@ class User(BaseModel): username: str password: str roles: str - disabled: bool + disabled: bool = False + removed: bool = False + confirmed: bool = False class UserOut(BaseModel): id: ObjectIdField = None username: str roles: str disabled: bool + removed: bool + confirmed: bool class UserInDB(User): password: str diff --git a/app/routers/users.py b/app/routers/users.py index 702dbe5..d7947ae 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -19,7 +19,7 @@ async def read_users(authorize: Annotated[bool, Depends(permissions_checker.Perm 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) + user = users.UserOut(id=user_index.id, username=user_index.username, disabled=user_index.disabled, roles=user_index.roles, removed=user_index.removed, confirmed=user_index.confirmed) listUsers.append(user) return listUsers @@ -39,7 +39,7 @@ 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) + user = users.UserOut(id=user_index.id, username=user_index.username, disabled=user_index.disabled, roles=user_index.roles, removed=user_index.removed, confirmed=user_index.confirmed) listUsers.append(user) return listUsers @@ -53,3 +53,24 @@ async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permis user_repository = users.UserRepository(database=database.database) user = user_repository.find_one_by_id(ObjectId(item_id)) return user + + + +@router.delete("/users/me",tags=["users"]) +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.update(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.update(user) + return user From 803d0330a18da6c5649ac3b4f33a65c4460c5137 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 18:29:07 +0200 Subject: [PATCH 04/17] fix delete user --- app/routers/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/users.py b/app/routers/users.py index d7947ae..d924877 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -62,7 +62,7 @@ async def read_users_me(current_user: Annotated[users.User, Depends(users_token. current_user.disabled = True if remove is True: current_user.removed = True - user_repository.update(current_user) + user_repository.save(current_user) return current_user @router.delete("/users/{item_id}", tags=["users"], response_model=users.User) @@ -72,5 +72,5 @@ async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permis user.disabled = True if remove is True: user.removed = True - user_repository.update(user) + user_repository.save(user) return user From b2d5481e10b01fccb7fd84a531c1667154bbe154 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 18:33:05 +0200 Subject: [PATCH 05/17] add exclude model for delete method --- app/routers/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/users.py b/app/routers/users.py index d924877..2b69f95 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -56,7 +56,7 @@ async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permis -@router.delete("/users/me",tags=["users"]) +@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 From 39f20310795dbc05ec93437e06f5bbd6e7523a5a Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 21:28:21 +0200 Subject: [PATCH 06/17] add put method --- app/models/users.py | 5 +++++ app/routers/users.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/app/models/users.py b/app/models/users.py index 01dd92b..d0b45cf 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -18,6 +18,11 @@ class UserOut(BaseModel): removed: bool confirmed: bool +class UserIn(BaseModel): + username: str + roles: str + password: str + class UserInDB(User): password: str diff --git a/app/routers/users.py b/app/routers/users.py index 2b69f95..d392cce 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -3,6 +3,8 @@ from ..dependencies import users_token, permissions_checker, database from ..models import users from typing import Annotated from bson import ObjectId +from passlib.context import CryptContext + router = APIRouter() @@ -74,3 +76,33 @@ async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permis 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) + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + current_user.username = userSingle.username + current_user.password = pwd_context.hash(userSingle.password) + current_user.roles = userSingle.roles + 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): + user_repository = users.UserRepository(database=database.database) + if userSingle is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Body request is empty" + ) + 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 + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + user.password = pwd_context.hash(userSingle.password) + user.roles = userSingle.roles + user_repository.save(user) + return user From 03ec42508ce9d0c068c6120ed9c3e8cbacaf0fe2 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 22:14:29 +0200 Subject: [PATCH 07/17] add fastapi-mail --- app/models/users.py | 6 +++++- app/routers/users.py | 28 +++++++++++++++++++--------- requierements.txt | 3 ++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/models/users.py b/app/models/users.py index d0b45cf..f56841d 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -5,7 +5,7 @@ class User(BaseModel): id: ObjectIdField = None username: str password: str - roles: str + roles: str = "User" disabled: bool = False removed: bool = False confirmed: bool = False @@ -23,6 +23,10 @@ class UserIn(BaseModel): roles: str password: str +class UserCreate(BaseModel): + username: str + password: str + class UserInDB(User): password: str diff --git a/app/routers/users.py b/app/routers/users.py index d392cce..2f38da9 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -3,8 +3,7 @@ from ..dependencies import users_token, permissions_checker, database from ..models import users from typing import Annotated from bson import ObjectId -from passlib.context import CryptContext - +from fastapi.responses import JSONResponse router = APIRouter() @@ -80,29 +79,40 @@ async def read_users_id(item_id : str, authorize: Annotated[bool, Depends(permis @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) - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") current_user.username = userSingle.username - current_user.password = pwd_context.hash(userSingle.password) + current_user.password = user_token.get_password_hash(userSingle.password) current_user.roles = userSingle.roles 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): - user_repository = users.UserRepository(database=database.database) 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 - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - user.password = pwd_context.hash(userSingle.password) + user.password = user_token.get_password_hash(userSingle.password) user.roles = userSingle.roles user_repository.save(user) return user + + +@router.post("/users",tags=["users"]) +async def read_users_me(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) + current_user = users.Users(username=userSingle.username, password=user_token.get_password_hash(userSingle.password)) + user_repository.save(current_user) + return current_user diff --git a/requierements.txt b/requierements.txt index 5bd6d38..ad8071b 100644 --- a/requierements.txt +++ b/requierements.txt @@ -3,4 +3,5 @@ uvicorn[standard] pydantic-mongo python-jose[cryptography] passlib[bcrypt] -python-multipart \ No newline at end of file +python-multipart +fastapi-mail \ No newline at end of file From cb47e5b4eb045fc5b6c3dfabab7c9558dceb0e19 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 22:21:16 +0200 Subject: [PATCH 08/17] config mail --- app/dependencies/mail.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/dependencies/mail.py diff --git a/app/dependencies/mail.py b/app/dependencies/mail.py new file mode 100644 index 0000000..9750c9e --- /dev/null +++ b/app/dependencies/mail.py @@ -0,0 +1,13 @@ +from fastapi_mail import ConnectionConfig +import os + +conf = ConnectionConfig( + MAIL_USERNAME = "YourUsername", + MAIL_PASSWORD = "strong_password", + MAIL_FROM = "your@email.com", + MAIL_PORT = 587, + MAIL_SERVER = "your mail server", + MAIL_STARTTLS = True, + MAIL_SSL_TLS = False, + TEMPLATE_FOLDER = Path(__file__).parent / 'templates', +) From ab7f2e99bc2f55997f2f639ca7b3abb4dd7d9c33 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 22:31:01 +0200 Subject: [PATCH 09/17] fix User class --- app/routers/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routers/users.py b/app/routers/users.py index 2f38da9..265d4b9 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -113,6 +113,6 @@ async def read_users_me(userSingle: users.UserCreate | None = None): detail="Body request is empty" ) user_repository = users.UserRepository(database=database.database) - current_user = users.Users(username=userSingle.username, password=user_token.get_password_hash(userSingle.password)) + current_user = users.User(username=userSingle.username, password=user_token.get_password_hash(userSingle.password)) user_repository.save(current_user) - return current_user + return JSONResponse(status_code=200, content={"message": "email has been sent"}) \ No newline at end of file From d108c54d81c2e17c8cd51f0a5109f4c73494f3da Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 22:45:12 +0200 Subject: [PATCH 10/17] send mail wip --- app/dependencies/mail.py | 3 ++- app/routers/users.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/dependencies/mail.py b/app/dependencies/mail.py index 9750c9e..dca8e78 100644 --- a/app/dependencies/mail.py +++ b/app/dependencies/mail.py @@ -1,4 +1,5 @@ from fastapi_mail import ConnectionConfig +from pathlib import Path import os conf = ConnectionConfig( @@ -9,5 +10,5 @@ conf = ConnectionConfig( MAIL_SERVER = "your mail server", MAIL_STARTTLS = True, MAIL_SSL_TLS = False, - TEMPLATE_FOLDER = Path(__file__).parent / 'templates', + TEMPLATE_FOLDER = Path(__file__).parents[1] / 'templates', ) diff --git a/app/routers/users.py b/app/routers/users.py index 265d4b9..22c3a06 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, status -from ..dependencies import users_token, permissions_checker, database +from ..dependencies import users_token, permissions_checker, database, mail from ..models import users from typing import Annotated from bson import ObjectId @@ -111,7 +111,8 @@ async def read_users_me(userSingle: users.UserCreate | None = None): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Body request is empty" - ) + ) + fm = FastMail(mail.conf) user_repository = users.UserRepository(database=database.database) current_user = users.User(username=userSingle.username, password=user_token.get_password_hash(userSingle.password)) user_repository.save(current_user) From b81119f53eb76f3609c53d0ef0ad58215c60a93a Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sat, 14 Oct 2023 23:04:19 +0200 Subject: [PATCH 11/17] add email key --- app/dependencies/user_add.py | 6 ++++-- app/models/users.py | 9 ++++++++- app/routers/users.py | 8 +++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/dependencies/user_add.py b/app/dependencies/user_add.py index 74cc7f4..fae97c5 100644 --- a/app/dependencies/user_add.py +++ b/app/dependencies/user_add.py @@ -1,20 +1,22 @@ from ..models import users from ..dependencies import database from passlib.context import CryptContext +from pydantic import EmailStr -def add(username="", password="", roles="User", disabled=False, confirmed=True): +def add(username="", password="", roles="User", disabled=False, confirmed=True, 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) + user = users.User(username=username, password=pwd_context.hash(password), roles=roles, disabled=disabled, confirmed=confirmed, email=email) if result is not None: result.password=pwd_context.hash(password) result.roles=roles result.disabled=disabled result.confirmed=confirmed + result.email=email user = result change = "updated" user_repository.save(user) diff --git a/app/models/users.py b/app/models/users.py index f56841d..4912304 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, EmailStr from pydantic_mongo import AbstractRepository, ObjectIdField class User(BaseModel): @@ -9,6 +9,7 @@ class User(BaseModel): disabled: bool = False removed: bool = False confirmed: bool = False + email: EmailStr class UserOut(BaseModel): id: ObjectIdField = None @@ -17,15 +18,21 @@ class UserOut(BaseModel): 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): password: str diff --git a/app/routers/users.py b/app/routers/users.py index 22c3a06..8d6580b 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -20,7 +20,7 @@ async def read_users(authorize: Annotated[bool, Depends(permissions_checker.Perm 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, removed=user_index.removed, confirmed=user_index.confirmed) + 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) return listUsers @@ -40,7 +40,7 @@ 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, removed=user_index.removed, confirmed=user_index.confirmed) + 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) return listUsers @@ -82,6 +82,7 @@ async def read_users_me(current_user: Annotated[users.User, Depends(users_token. 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 @@ -101,6 +102,7 @@ async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.P 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 @@ -114,6 +116,6 @@ async def read_users_me(userSingle: users.UserCreate | None = None): ) fm = FastMail(mail.conf) user_repository = users.UserRepository(database=database.database) - current_user = users.User(username=userSingle.username, password=user_token.get_password_hash(userSingle.password)) + current_user = users.User(username=userSingle.username, password=user_token.get_password_hash(userSingle.password), email=userSingle.email) user_repository.save(current_user) return JSONResponse(status_code=200, content={"message": "email has been sent"}) \ No newline at end of file From 228c9bebb892d9913e49e50b8416ab5f9daeb1b8 Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sun, 15 Oct 2023 14:12:12 +0200 Subject: [PATCH 12/17] add user email --- app/dependencies/mail.py | 10 +++++----- app/routers/users.py | 17 ++++++++++++++++- app/templates/mailer.html | 6 ++++++ 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 app/templates/mailer.html diff --git a/app/dependencies/mail.py b/app/dependencies/mail.py index dca8e78..5620481 100644 --- a/app/dependencies/mail.py +++ b/app/dependencies/mail.py @@ -3,11 +3,11 @@ from pathlib import Path import os conf = ConnectionConfig( - MAIL_USERNAME = "YourUsername", - MAIL_PASSWORD = "strong_password", - MAIL_FROM = "your@email.com", - MAIL_PORT = 587, - MAIL_SERVER = "your mail server", + 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', diff --git a/app/routers/users.py b/app/routers/users.py index 8d6580b..e175d01 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -4,6 +4,7 @@ from ..models import users from typing import Annotated from bson import ObjectId from fastapi.responses import JSONResponse +from fastapi_mail import MessageSchema, MessageType router = APIRouter() @@ -114,8 +115,22 @@ async def read_users_me(userSingle: users.UserCreate | None = None): status_code=status.HTTP_400_BAD_REQUEST, detail="Body request is empty" ) - fm = FastMail(mail.conf) user_repository = users.UserRepository(database=database.database) + user = user_repository.find_one_by({"username": {'$eq': userSingle.username}}) + if user is not None: + raise HTTPException( + status_code=status.HTTP_204_NO_CONTENT, + detail="User is already exist" + ) + + fm = FastMail(mail.conf) + message = MessageSchema( + subject="Fastapi-Mail module", + recipients=userSingle.email, + template_body=email.dict(), + subtype=MessageType.html, + ) + await fm.send_message(message, template_name="mailer.html") current_user = users.User(username=userSingle.username, password=user_token.get_password_hash(userSingle.password), email=userSingle.email) user_repository.save(current_user) return JSONResponse(status_code=200, content={"message": "email has been sent"}) \ No newline at end of file diff --git a/app/templates/mailer.html b/app/templates/mailer.html new file mode 100644 index 0000000..e9bfbcb --- /dev/null +++ b/app/templates/mailer.html @@ -0,0 +1,6 @@ + +Email +

Voici un lien https://localhost:8080/api/mail?key={key}&username={username} +

+ + From f32fa3e5b9c1cb703a92189bcf1e7e9e1c5fc86b Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sun, 15 Oct 2023 15:06:54 +0200 Subject: [PATCH 13/17] send email ok --- app/models/email.py | 7 +++++++ app/routers/users.py | 17 +++++++++++------ app/templates/mailer.html | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 app/models/email.py diff --git a/app/models/email.py b/app/models/email.py new file mode 100644 index 0000000..2981d3e --- /dev/null +++ b/app/models/email.py @@ -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] \ No newline at end of file diff --git a/app/routers/users.py b/app/routers/users.py index e175d01..8f365be 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,11 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, status from ..dependencies import users_token, permissions_checker, database, mail -from ..models import users +from ..models import users, email from typing import Annotated from bson import ObjectId from fastapi.responses import JSONResponse -from fastapi_mail import MessageSchema, MessageType - +from fastapi_mail import MessageSchema, MessageType, FastMail +import random router = APIRouter() @@ -124,13 +124,18 @@ async def read_users_me(userSingle: users.UserCreate | None = None): ) 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=userSingle.email, - template_body=email.dict(), + 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=user_token.get_password_hash(userSingle.password), email=userSingle.email) + current_user = users.User(username=userSingle.username, password=users_token.get_password_hash(userSingle.password), email=userSingle.email) user_repository.save(current_user) return JSONResponse(status_code=200, content={"message": "email has been sent"}) \ No newline at end of file diff --git a/app/templates/mailer.html b/app/templates/mailer.html index e9bfbcb..011f049 100644 --- a/app/templates/mailer.html +++ b/app/templates/mailer.html @@ -1,6 +1,6 @@ Email -

Voici un lien https://localhost:8080/api/mail?key={key}&username={username} +

Voici un lien https://localhost:8080/api/mail?key={{ key }}&username={{ username }}

From dc4043acd9477a2626f6bf267ef682a3a2d2b6ac Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sun, 15 Oct 2023 16:17:37 +0200 Subject: [PATCH 14/17] add router mail --- app/main.py | 3 ++- app/routers/mail.py | 40 ++++++++++++++++++++++++++++++++++++++++ app/routers/users.py | 42 +++--------------------------------------- requierements.txt | 3 ++- 4 files changed, 47 insertions(+), 41 deletions(-) create mode 100644 app/routers/mail.py diff --git a/app/main.py b/app/main.py index 8297dde..f49d8ae 100644 --- a/app/main.py +++ b/app/main.py @@ -1,12 +1,13 @@ from fastapi import FastAPI -from .routers import users, token +from .routers import users, token, mail from .dependencies import user_add app = FastAPI() app.include_router(users.router) app.include_router(token.router) +app.include_router(mail.router) @app.on_event("startup") diff --git a/app/routers/mail.py b/app/routers/mail.py new file mode 100644 index 0000000..247210c --- /dev/null +++ b/app/routers/mail.py @@ -0,0 +1,40 @@ +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 read_users_me(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({"username": {'$eq': userSingle.username}}) + 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) + return JSONResponse(status_code=200, content={"message": "email has been sent"}) \ No newline at end of file diff --git a/app/routers/users.py b/app/routers/users.py index 8f365be..6b79929 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,11 +1,8 @@ from fastapi import APIRouter, Depends, HTTPException, status -from ..dependencies import users_token, permissions_checker, database, mail -from ..models import users, email +from ..dependencies import users_token, permissions_checker, database +from ..models import users from typing import Annotated from bson import ObjectId -from fastapi.responses import JSONResponse -from fastapi_mail import MessageSchema, MessageType, FastMail -import random router = APIRouter() @@ -105,37 +102,4 @@ async def read_users_id(authorize: Annotated[bool, Depends(permissions_checker.P user.roles = userSingle.roles user.email = userSingle.email user_repository.save(user) - return user - - -@router.post("/users",tags=["users"]) -async def read_users_me(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({"username": {'$eq': userSingle.username}}) - 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) - return JSONResponse(status_code=200, content={"message": "email has been sent"}) \ No newline at end of file + return user \ No newline at end of file diff --git a/requierements.txt b/requierements.txt index ad8071b..2645894 100644 --- a/requierements.txt +++ b/requierements.txt @@ -4,4 +4,5 @@ pydantic-mongo python-jose[cryptography] passlib[bcrypt] python-multipart -fastapi-mail \ No newline at end of file +fastapi-mail +redis \ No newline at end of file From 3392d8720fdd8a69f4f08ef2cf2cae764431979f Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sun, 15 Oct 2023 16:26:20 +0200 Subject: [PATCH 15/17] add or operator --- app/routers/mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/mail.py b/app/routers/mail.py index 247210c..919402e 100644 --- a/app/routers/mail.py +++ b/app/routers/mail.py @@ -15,7 +15,7 @@ async def read_users_me(userSingle: users.UserCreate | None = None): detail="Body request is empty" ) 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 not None: raise HTTPException( status_code=status.HTTP_204_NO_CONTENT, From f2b7c63eee725275e92fc836a972c35fca0c7dac Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sun, 15 Oct 2023 16:48:58 +0200 Subject: [PATCH 16/17] add redis storage --- app/dependencies/database.py | 6 ++++-- app/routers/mail.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/dependencies/database.py b/app/dependencies/database.py index 7ff0550..cbf875c 100644 --- a/app/dependencies/database.py +++ b/app/dependencies/database.py @@ -1,5 +1,7 @@ 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"])) -database = client[os.environ["MONGO_DATABASE"]] \ No newline at end of file +database = client[os.environ["MONGO_DATABASE"]] + +connect_redis = redis.Redis(host=os.environ["REDIS_URL"], port=os.environ["REDIS_PORT"], decode_responses=True) \ No newline at end of file diff --git a/app/routers/mail.py b/app/routers/mail.py index 919402e..1641840 100644 --- a/app/routers/mail.py +++ b/app/routers/mail.py @@ -8,7 +8,7 @@ router = APIRouter() @router.post("/mail",tags=["mail"]) -async def read_users_me(userSingle: users.UserCreate | None = None): +async def create_user(userSingle: users.UserCreate | None = None): if userSingle is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -37,4 +37,5 @@ async def read_users_me(userSingle: users.UserCreate | None = None): 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) - return JSONResponse(status_code=200, content={"message": "email has been sent"}) \ No newline at end of file + database.connect_redis.set(userSingle.username, key_hashed) + return JSONResponse(status_code=status.HTTP_200_OK, content={"message": "email has been sent"}) \ No newline at end of file From fef8bc92d4ca17ccbfb711fb5279921a44fdddfb Mon Sep 17 00:00:00 2001 From: Valentin CZERYBA Date: Sun, 15 Oct 2023 17:06:48 +0200 Subject: [PATCH 17/17] user confirm by mail --- app/routers/mail.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/routers/mail.py b/app/routers/mail.py index 1641840..e140385 100644 --- a/app/routers/mail.py +++ b/app/routers/mail.py @@ -38,4 +38,23 @@ async def create_user(userSingle: users.UserCreate | None = None): 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"}) \ No newline at end of file + 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"}) \ No newline at end of file