Revamp implementation with a manager layer and db class as dependency callable

This commit is contained in:
François Voron
2021-09-14 11:12:34 +02:00
parent ccbdd09659
commit dade8c263d
28 changed files with 589 additions and 496 deletions

View File

@ -3,13 +3,13 @@ from fastapi.security import OAuth2PasswordRequestForm
from fastapi_users import models
from fastapi_users.authentication import Authenticator, BaseAuthentication
from fastapi_users.db import BaseUserDatabase
from fastapi_users.manager import UserManager, UserManagerDependency
from fastapi_users.router.common import ErrorCode
def get_auth_router(
backend: BaseAuthentication,
user_db: BaseUserDatabase[models.BaseUserDB],
get_user_manager: UserManagerDependency[models.BaseUserDB],
authenticator: Authenticator,
requires_verification: bool = False,
) -> APIRouter:
@ -21,9 +21,11 @@ def get_auth_router(
@router.post("/login")
async def login(
response: Response, credentials: OAuth2PasswordRequestForm = Depends()
response: Response,
credentials: OAuth2PasswordRequestForm = Depends(),
user_manager: UserManager[models.BaseUserDB] = Depends(get_user_manager),
):
user = await user_db.authenticate(credentials)
user = await user_manager.authenticate(credentials)
if user is None or not user.is_active:
raise HTTPException(

View File

@ -7,8 +7,8 @@ from httpx_oauth.oauth2 import BaseOAuth2
from fastapi_users import models
from fastapi_users.authentication import Authenticator
from fastapi_users.db import BaseUserDatabase
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
from fastapi_users.manager import UserManager, UserManagerDependency, UserNotExists
from fastapi_users.password import generate_password, get_password_hash
from fastapi_users.router.common import ErrorCode, run_handler
@ -24,7 +24,7 @@ def generate_state_token(
def get_oauth_router(
oauth_client: BaseOAuth2,
user_db: BaseUserDatabase[models.BaseUserDB],
get_user_manager: UserManagerDependency[models.BaseUserDB],
user_db_model: Type[models.BaseUserDB],
authenticator: Authenticator,
state_secret: SecretType,
@ -83,6 +83,7 @@ def get_oauth_router(
request: Request,
response: Response,
access_token_state=Depends(oauth2_authorize_callback),
user_manager: UserManager[models.BaseUserDB] = Depends(get_user_manager),
):
token, state = access_token_state
account_id, account_email = await oauth_client.get_id_email(
@ -94,8 +95,6 @@ def get_oauth_router(
except jwt.DecodeError:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
user = await user_db.get_by_oauth_account(oauth_client.name, account_id)
new_oauth_account = models.BaseOAuthAccount(
oauth_name=oauth_client.name,
access_token=token["access_token"],
@ -105,13 +104,17 @@ def get_oauth_router(
account_email=account_email,
)
if not user:
user = await user_db.get_by_email(account_email)
if user:
try:
user = await user_manager.get_by_oauth_account(
oauth_client.name, account_id
)
except UserNotExists:
try:
# Link account
user = await user_manager.get_by_email(account_email)
user.oauth_accounts.append(new_oauth_account) # type: ignore
await user_db.update(user)
else:
await user_manager.user_db.update(user)
except UserNotExists:
# Create account
password = generate_password()
user = user_db_model(
@ -119,7 +122,7 @@ def get_oauth_router(
hashed_password=get_password_hash(password),
oauth_accounts=[new_oauth_account],
)
await user_db.create(user)
await user_manager.user_db.create(user)
if after_register:
await run_handler(after_register, user, request)
else:
@ -131,7 +134,7 @@ def get_oauth_router(
else:
updated_oauth_accounts.append(oauth_account)
user.oauth_accounts = updated_oauth_accounts # type: ignore
await user_db.update(user)
await user_manager.user_db.update(user)
if not user.is_active:
raise HTTPException(

View File

@ -1,23 +1,22 @@
from typing import Callable, Optional, Type, cast
from fastapi import APIRouter, HTTPException, Request, status
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi_users import models
from fastapi_users.router.common import ErrorCode, run_handler
from fastapi_users.user import (
CreateUserProtocol,
from fastapi_users.manager import (
InvalidPasswordException,
UserAlreadyExists,
ValidatePasswordProtocol,
UserManager,
UserManagerDependency,
)
from fastapi_users.router.common import ErrorCode, run_handler
def get_register_router(
create_user: CreateUserProtocol,
get_user_manager: UserManagerDependency[models.BaseUserDB],
user_model: Type[models.BaseUser],
user_create_model: Type[models.BaseUserCreate],
after_register: Optional[Callable[[models.UD, Request], None]] = None,
validate_password: Optional[ValidatePasswordProtocol] = None,
) -> APIRouter:
"""Generate a router with the register route."""
router = APIRouter()
@ -25,27 +24,28 @@ def get_register_router(
@router.post(
"/register", response_model=user_model, status_code=status.HTTP_201_CREATED
)
async def register(request: Request, user: user_create_model): # type: ignore
async def register(
request: Request,
user: user_create_model, # type: ignore
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
user = cast(models.BaseUserCreate, user) # Prevent mypy complain
if validate_password:
try:
await validate_password(user.password, user)
except InvalidPasswordException as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": ErrorCode.REGISTER_INVALID_PASSWORD,
"reason": e.reason,
},
)
try:
created_user = await create_user(user, safe=True)
created_user = await user_manager.create(user, safe=True)
except UserAlreadyExists:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS,
)
except InvalidPasswordException as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": ErrorCode.REGISTER_INVALID_PASSWORD,
"reason": e.reason,
},
)
if after_register:
await run_handler(after_register, created_user, request)

View File

@ -1,21 +1,26 @@
from typing import Callable, Optional
import jwt
from fastapi import APIRouter, Body, HTTPException, Request, status
from fastapi import APIRouter, Body, Depends, HTTPException, Request, status
from pydantic import UUID4, EmailStr
from fastapi_users import models
from fastapi_users.db import BaseUserDatabase
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
from fastapi_users.manager import (
InvalidPasswordException,
UserManager,
UserManagerDependency,
UserNotExists,
ValidatePasswordProtocol,
)
from fastapi_users.password import get_password_hash
from fastapi_users.router.common import ErrorCode, run_handler
from fastapi_users.user import InvalidPasswordException, ValidatePasswordProtocol
RESET_PASSWORD_TOKEN_AUDIENCE = "fastapi-users:reset"
def get_reset_password_router(
user_db: BaseUserDatabase[models.BaseUserDB],
get_user_manager: UserManagerDependency[models.BaseUserDB],
reset_password_token_secret: SecretType,
reset_password_token_lifetime_seconds: int = 3600,
after_forgot_password: Optional[Callable[[models.UD, str, Request], None]] = None,
@ -27,11 +32,16 @@ def get_reset_password_router(
@router.post("/forgot-password", status_code=status.HTTP_202_ACCEPTED)
async def forgot_password(
request: Request, email: EmailStr = Body(..., embed=True)
request: Request,
email: EmailStr = Body(..., embed=True),
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
user = await user_db.get_by_email(email)
try:
user = await user_manager.get_by_email(email)
except UserNotExists:
return None
if user is not None and user.is_active:
if user.is_active:
token_data = {"user_id": str(user.id), "aud": RESET_PASSWORD_TOKEN_AUDIENCE}
token = generate_jwt(
token_data,
@ -45,7 +55,10 @@ def get_reset_password_router(
@router.post("/reset-password")
async def reset_password(
request: Request, token: str = Body(...), password: str = Body(...)
request: Request,
token: str = Body(...),
password: str = Body(...),
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
try:
data = decode_jwt(
@ -66,8 +79,15 @@ def get_reset_password_router(
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
)
user = await user_db.get(user_uiid)
if user is None or not user.is_active:
try:
user = await user_manager.get(user_uiid)
except UserNotExists:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
@ -86,7 +106,7 @@ def get_reset_password_router(
)
user.hashed_password = get_password_hash(password)
await user_db.update(user)
await user_manager.user_db.update(user)
if after_reset_password:
await run_handler(after_reset_password, user, request)
except jwt.PyJWTError:

View File

@ -1,25 +1,28 @@
from typing import Any, Callable, Dict, Optional, Type, cast
from typing import Any, Callable, Dict, Optional, Type
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from pydantic import UUID4
from fastapi_users import models
from fastapi_users.authentication import Authenticator
from fastapi_users.db import BaseUserDatabase
from fastapi_users.password import get_password_hash
from fastapi_users.manager import (
InvalidPasswordException,
UserAlreadyExists,
UserManager,
UserManagerDependency,
UserNotExists,
)
from fastapi_users.router.common import ErrorCode, run_handler
from fastapi_users.user import InvalidPasswordException, ValidatePasswordProtocol
def get_users_router(
user_db: BaseUserDatabase[models.BaseUserDB],
get_user_manager: UserManagerDependency[models.UD],
user_model: Type[models.BaseUser],
user_update_model: Type[models.BaseUserUpdate],
user_db_model: Type[models.BaseUserDB],
authenticator: Authenticator,
after_update: Optional[Callable[[models.UD, Dict[str, Any], Request], None]] = None,
requires_verification: bool = False,
validate_password: Optional[ValidatePasswordProtocol] = None,
) -> APIRouter:
"""Generate a router with the authentication routes."""
router = APIRouter()
@ -31,42 +34,13 @@ def get_users_router(
active=True, verified=requires_verification, superuser=True
)
async def _get_or_404(id: UUID4) -> models.BaseUserDB:
user = await user_db.get(id)
if user is None:
async def get_user_or_404(
id: UUID4, user_manager: UserManager[models.UD] = Depends(get_user_manager)
) -> models.BaseUserDB:
try:
return await user_manager.get(id)
except UserNotExists:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return user
async def _check_unique_email(
updated_user: user_update_model, # type: ignore
) -> None:
updated_user = cast(
models.BaseUserUpdate, updated_user
) # Prevent mypy complain
if updated_user.email:
user = await user_db.get_by_email(updated_user.email)
if user is not None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
)
async def _update_user(
user: models.BaseUserDB, update_dict: Dict[str, Any], request: Request
):
for field in update_dict:
if field == "password":
password = update_dict[field]
if validate_password:
await validate_password(password, user)
hashed_password = get_password_hash(password)
user.hashed_password = hashed_password
else:
setattr(user, field, update_dict[field])
updated_user = await user_db.update(user)
if after_update:
await run_handler(after_update, updated_user, update_dict, request)
return updated_user
@router.get("/me", response_model=user_model)
async def me(
@ -77,21 +51,24 @@ def get_users_router(
@router.patch(
"/me",
response_model=user_model,
dependencies=[Depends(get_current_active_user), Depends(_check_unique_email)],
dependencies=[Depends(get_current_active_user)],
)
async def update_me(
request: Request,
updated_user: user_update_model, # type: ignore
user_update: user_update_model, # type: ignore
user: user_db_model = Depends(get_current_active_user), # type: ignore
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
updated_user = cast(
models.BaseUserUpdate,
updated_user,
) # Prevent mypy complain
updated_user_data = updated_user.create_update_dict()
try:
return await _update_user(user, updated_user_data, request)
updated_user = await user_manager.update(user_update, user, safe=True)
if after_update:
await run_handler(
after_update,
updated_user,
user_update.create_update_dict(), # type: ignore
request,
)
return updated_user
except InvalidPasswordException as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@ -100,32 +77,41 @@ def get_users_router(
"reason": e.reason,
},
)
except UserAlreadyExists:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
)
@router.get(
"/{id:uuid}",
response_model=user_model,
dependencies=[Depends(get_current_superuser)],
)
async def get_user(id: UUID4):
return await _get_or_404(id)
async def get_user(user=Depends(get_user_or_404)):
return user
@router.patch(
"/{id:uuid}",
response_model=user_model,
dependencies=[Depends(get_current_superuser), Depends(_check_unique_email)],
dependencies=[Depends(get_current_superuser)],
)
async def update_user(
id: UUID4, updated_user: user_update_model, request: Request # type: ignore
user_update: user_update_model, # type: ignore
request: Request,
user=Depends(get_user_or_404),
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
updated_user = cast(
models.BaseUserUpdate,
updated_user,
) # Prevent mypy complain
user = await _get_or_404(id)
updated_user_data = updated_user.create_update_dict_superuser()
try:
return await _update_user(user, updated_user_data, request)
updated_user = await user_manager.update(user_update, user, safe=False)
if after_update:
await run_handler(
after_update,
updated_user,
user_update.create_update_dict_superuser(), # type: ignore
request,
)
return updated_user
except InvalidPasswordException as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@ -134,6 +120,11 @@ def get_users_router(
"reason": e.reason,
},
)
except UserAlreadyExists:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
)
@router.delete(
"/{id:uuid}",
@ -141,9 +132,11 @@ def get_users_router(
response_class=Response,
dependencies=[Depends(get_current_superuser)],
)
async def delete_user(id: UUID4):
user = await _get_or_404(id)
await user_db.delete(user)
async def delete_user(
user=Depends(get_user_or_404),
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
await user_manager.delete(user)
return None
return router

View File

@ -1,25 +1,24 @@
from typing import Callable, Optional, Type, cast
from typing import Callable, Optional, Type
import jwt
from fastapi import APIRouter, Body, HTTPException, Request, status
from fastapi import APIRouter, Body, Depends, HTTPException, Request, status
from pydantic import UUID4, EmailStr
from fastapi_users import models
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
from fastapi_users.router.common import ErrorCode, run_handler
from fastapi_users.user import (
GetUserProtocol,
from fastapi_users.manager import (
UserAlreadyVerified,
UserManager,
UserManagerDependency,
UserNotExists,
VerifyUserProtocol,
)
from fastapi_users.router.common import ErrorCode, run_handler
VERIFY_USER_TOKEN_AUDIENCE = "fastapi-users:verify"
def get_verify_router(
verify_user: VerifyUserProtocol,
get_user: GetUserProtocol,
get_user_manager: UserManagerDependency[models.UD],
user_model: Type[models.BaseUser],
verification_token_secret: SecretType,
verification_token_lifetime_seconds: int = 3600,
@ -32,10 +31,12 @@ def get_verify_router(
@router.post("/request-verify-token", status_code=status.HTTP_202_ACCEPTED)
async def request_verify_token(
request: Request, email: EmailStr = Body(..., embed=True)
request: Request,
email: EmailStr = Body(..., embed=True),
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
try:
user = await get_user(email)
user = await user_manager.get_by_email(email)
if not user.is_verified and user.is_active:
token_data = {
"user_id": str(user.id),
@ -56,7 +57,11 @@ def get_verify_router(
return None
@router.post("/verify", response_model=user_model)
async def verify(request: Request, token: str = Body(..., embed=True)):
async def verify(
request: Request,
token: str = Body(..., embed=True),
user_manager: UserManager[models.UD] = Depends(get_user_manager),
):
try:
data = decode_jwt(
token, verification_token_secret, [VERIFY_USER_TOKEN_AUDIENCE]
@ -72,17 +77,17 @@ def get_verify_router(
detail=ErrorCode.VERIFY_USER_BAD_TOKEN,
)
user_id = data.get("user_id")
email = cast(EmailStr, data.get("email"))
if user_id is None:
try:
user_id = data["user_id"]
email = data["email"]
except KeyError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.VERIFY_USER_BAD_TOKEN,
)
try:
user_check = await get_user(email)
user_check = await user_manager.get_by_email(email)
except UserNotExists:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@ -104,7 +109,7 @@ def get_verify_router(
)
try:
user = await verify_user(user_check)
user = await user_manager.verify(user_check)
except UserAlreadyVerified:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,