mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2026-03-13 07:49:55 +08:00
Move forgot/reset password logic to manager
This commit is contained in:
@@ -100,32 +100,9 @@ class FastAPIUsers(Generic[models.U, models.UC, models.UU, models.UD]):
|
||||
after_verification,
|
||||
)
|
||||
|
||||
def get_reset_password_router(
|
||||
self,
|
||||
reset_password_token_secret: SecretType,
|
||||
reset_password_token_lifetime_seconds: int = 3600,
|
||||
after_forgot_password: Optional[
|
||||
Callable[[models.UD, str, Request], None]
|
||||
] = None,
|
||||
after_reset_password: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Return a reset password process router.
|
||||
|
||||
:param reset_password_token_secret: Secret to encode reset password token.
|
||||
:param reset_password_token_lifetime_seconds: Lifetime of reset password token.
|
||||
:param after_forgot_password: Optional function called after a successful
|
||||
forgot password request.
|
||||
:param after_reset_password: Optional function called after a successful
|
||||
password reset.
|
||||
"""
|
||||
return get_reset_password_router(
|
||||
self.get_user_manager,
|
||||
reset_password_token_secret,
|
||||
reset_password_token_lifetime_seconds,
|
||||
after_forgot_password,
|
||||
after_reset_password,
|
||||
)
|
||||
def get_reset_password_router(self) -> APIRouter:
|
||||
"""Return a reset password process router."""
|
||||
return get_reset_password_router(self.get_user_manager)
|
||||
|
||||
def get_auth_router(
|
||||
self, backend: BaseAuthentication, requires_verification: bool = False
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from typing import Any, Callable, Dict, Generic, Optional, Type, Union
|
||||
|
||||
import jwt
|
||||
from fastapi import Request
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic.types import UUID4
|
||||
from pydantic import UUID4
|
||||
|
||||
from fastapi_users import models, password
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
|
||||
from fastapi_users.password import get_password_hash
|
||||
|
||||
RESET_PASSWORD_TOKEN_AUDIENCE = "fastapi-users:reset"
|
||||
|
||||
|
||||
class FastAPIUsersException(Exception):
|
||||
pass
|
||||
@@ -21,10 +25,18 @@ class UserNotExists(FastAPIUsersException):
|
||||
pass
|
||||
|
||||
|
||||
class UserInactive(FastAPIUsersException):
|
||||
pass
|
||||
|
||||
|
||||
class UserAlreadyVerified(FastAPIUsersException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidResetPasswordToken(FastAPIUsersException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPasswordException(FastAPIUsersException):
|
||||
def __init__(self, reason: Any) -> None:
|
||||
self.reason = reason
|
||||
@@ -35,6 +47,10 @@ class BaseUserManager(Generic[models.UC, models.UD]):
|
||||
user_db_model: Type[models.UD]
|
||||
user_db: BaseUserDatabase[models.UD]
|
||||
|
||||
reset_password_token_secret: SecretType
|
||||
reset_password_token_lifetime_seconds: int = 3600
|
||||
reset_password_token_audience: str = RESET_PASSWORD_TOKEN_AUDIENCE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_db_model: Type[models.UD],
|
||||
@@ -88,6 +104,53 @@ class BaseUserManager(Generic[models.UC, models.UD]):
|
||||
|
||||
return created_user
|
||||
|
||||
async def forgot_password(
|
||||
self, user: models.UD, request: Optional[Request] = None
|
||||
) -> None:
|
||||
if not user.is_active:
|
||||
raise UserInactive()
|
||||
|
||||
token_data = {"user_id": str(user.id), "aud": RESET_PASSWORD_TOKEN_AUDIENCE}
|
||||
token = generate_jwt(
|
||||
token_data,
|
||||
self.reset_password_token_secret,
|
||||
self.reset_password_token_lifetime_seconds,
|
||||
)
|
||||
await self.on_after_forgot_password(user, token, request)
|
||||
|
||||
async def reset_password(
|
||||
self, token: str, password: str, request: Optional[Request] = None
|
||||
) -> models.UD:
|
||||
try:
|
||||
data = decode_jwt(
|
||||
token,
|
||||
self.reset_password_token_secret,
|
||||
[self.reset_password_token_audience],
|
||||
)
|
||||
except jwt.PyJWTError:
|
||||
raise InvalidResetPasswordToken()
|
||||
|
||||
try:
|
||||
user_id = data["user_id"]
|
||||
except KeyError:
|
||||
raise InvalidResetPasswordToken()
|
||||
|
||||
try:
|
||||
user_uuid = UUID4(user_id)
|
||||
except ValueError:
|
||||
raise InvalidResetPasswordToken()
|
||||
|
||||
user = await self.get(user_uuid)
|
||||
|
||||
if not user.is_active:
|
||||
raise UserInactive()
|
||||
|
||||
updated_user = await self._update(user, {"password": password})
|
||||
|
||||
await self.on_after_reset_password(user, request)
|
||||
|
||||
return updated_user
|
||||
|
||||
async def verify(self, user: models.UD) -> models.UD:
|
||||
if user.is_verified:
|
||||
raise UserAlreadyVerified()
|
||||
@@ -117,6 +180,16 @@ class BaseUserManager(Generic[models.UC, models.UD]):
|
||||
) -> None:
|
||||
return # pragma: no cover
|
||||
|
||||
async def on_after_forgot_password(
|
||||
self, user: models.UD, token: str, request: Optional[Request] = None
|
||||
) -> None:
|
||||
return # pragma: no cover
|
||||
|
||||
async def on_after_reset_password(
|
||||
self, user: models.UD, request: Optional[Request] = None
|
||||
) -> None:
|
||||
return # pragma: no cover
|
||||
|
||||
async def authenticate(
|
||||
self, credentials: OAuth2PasswordRequestForm
|
||||
) -> Optional[models.UD]:
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Request, status
|
||||
from pydantic import UUID4, EmailStr
|
||||
from pydantic import EmailStr
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
|
||||
from fastapi_users.manager import (
|
||||
BaseUserManager,
|
||||
InvalidPasswordException,
|
||||
InvalidResetPasswordToken,
|
||||
UserInactive,
|
||||
UserManagerDependency,
|
||||
UserNotExists,
|
||||
)
|
||||
from fastapi_users.password import get_password_hash
|
||||
from fastapi_users.router.common import ErrorCode, run_handler
|
||||
|
||||
RESET_PASSWORD_TOKEN_AUDIENCE = "fastapi-users:reset"
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
|
||||
|
||||
def get_reset_password_router(
|
||||
get_user_manager: UserManagerDependency[models.UC, models.UD],
|
||||
reset_password_token_secret: SecretType,
|
||||
reset_password_token_lifetime_seconds: int = 3600,
|
||||
after_forgot_password: Optional[Callable[[models.UD, str, Request], None]] = None,
|
||||
after_reset_password: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
get_user_manager: UserManagerDependency[models.UC, models.UD]
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the reset password routes."""
|
||||
router = APIRouter()
|
||||
@@ -39,15 +30,10 @@ def get_reset_password_router(
|
||||
except UserNotExists:
|
||||
return None
|
||||
|
||||
if user.is_active:
|
||||
token_data = {"user_id": str(user.id), "aud": RESET_PASSWORD_TOKEN_AUDIENCE}
|
||||
token = generate_jwt(
|
||||
token_data,
|
||||
reset_password_token_secret,
|
||||
reset_password_token_lifetime_seconds,
|
||||
)
|
||||
if after_forgot_password:
|
||||
await run_handler(after_forgot_password, user, token, request)
|
||||
try:
|
||||
await user_manager.forgot_password(user, request)
|
||||
except UserInactive:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@@ -59,57 +45,19 @@ def get_reset_password_router(
|
||||
user_manager: BaseUserManager[models.UC, models.UD] = Depends(get_user_manager),
|
||||
):
|
||||
try:
|
||||
data = decode_jwt(
|
||||
token, reset_password_token_secret, [RESET_PASSWORD_TOKEN_AUDIENCE]
|
||||
)
|
||||
user_id = data.get("user_id")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
try:
|
||||
user_uiid = UUID4(user_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
try:
|
||||
await user_manager.validate_password(password, user)
|
||||
except InvalidPasswordException as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD,
|
||||
"reason": e.reason,
|
||||
},
|
||||
)
|
||||
|
||||
user.hashed_password = get_password_hash(password)
|
||||
await user_manager.user_db.update(user)
|
||||
if after_reset_password:
|
||||
await run_handler(after_reset_password, user, request)
|
||||
except jwt.PyJWTError:
|
||||
await user_manager.reset_password(token, password, request)
|
||||
except (InvalidResetPasswordToken, UserNotExists, UserInactive):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
except InvalidPasswordException as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD,
|
||||
"reason": e.reason,
|
||||
},
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
Reference in New Issue
Block a user