mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-08-26 04:25:46 +08:00
Revamp implementation with a manager layer and db class as dependency callable
This commit is contained in:
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user