This commit is contained in:
s.mostryukov 2024-10-02 18:50:50 +03:00
parent f8008c2711
commit 8f665a57ab
21 changed files with 383 additions and 10 deletions

View File

@ -4,3 +4,7 @@ NETADM_CONFIG__RUN__RELOAD = 1
NETADM_CONFIG__DB__URL=postgresql+asyncpg://user:pwd@localhost:5432/app
NETADM_CONFIG__DB__ECHO=1
NETADM_CONFIG__ACCESS_TOKEN__RESET_PASSWORD_TOKEN_SECRET=
NETADM_CONFIG__ACCESS_TOKEN__VERIFICATION_TOKEN_SECRET=

10
api/routers/__init__.py Normal file
View File

@ -0,0 +1,10 @@
from fastapi import APIRouter
from authentication.routers.auth import router as auth_router
from authentication.routers.users import router as users_router
router = APIRouter()
router.include_router(auth_router)
router.include_router(users_router)
__all__ = ["router"]

View File

@ -0,0 +1,11 @@
from .access_tokens import get_access_tokens_db
from .users import get_users_db
from .user_manager import get_user_manager
from .backend import authentication_backend
__all__ = (
"get_access_tokens_db",
"get_users_db",
"get_user_manager",
"authentication_backend",
)

View File

@ -0,0 +1,23 @@
from typing import (
TYPE_CHECKING,
Annotated,
)
from fastapi import Depends
from models import (
db_helper,
AccessToken,
)
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
async def get_access_tokens_db(
session: Annotated[
"AsyncSession",
Depends(db_helper.session_getter),
],
):
yield AccessToken.get_db(session=session)

View File

@ -0,0 +1,10 @@
from fastapi_users.authentication import AuthenticationBackend
from authentication.transport import bearer_transport
from authentication.strategy import get_database_strategy
authentication_backend = AuthenticationBackend(
name="access-tokens-db",
transport=bearer_transport,
get_strategy=get_database_strategy,
)

View File

@ -0,0 +1,19 @@
from typing import Annotated, TYPE_CHECKING
from fastapi import Depends
from authentication.user_manager import UserManager
from .users import get_users_db
if TYPE_CHECKING:
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
async def get_user_manager(
users_db: Annotated[
"SQLAlchemyUserDatabase",
Depends(get_users_db),
]
):
yield UserManager(users_db)

View File

@ -0,0 +1,23 @@
from typing import (
TYPE_CHECKING,
Annotated,
)
from fastapi import Depends
from models import (
db_helper,
User,
)
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
async def get_users_db(
session: Annotated[
"AsyncSession",
Depends(db_helper.session_getter),
],
):
yield User.get_db(session=session)

View File

@ -0,0 +1,7 @@
from .auth import router as auth_router
from .users import router as users_router
__all__ = [
"auth_router",
"users_router",
]

View File

@ -0,0 +1,40 @@
from fastapi import APIRouter
from config import settings
from schemas.user import UserRead, UserCreate
from .user_router_helper import fastapi_users
from authentication.dependencies import authentication_backend
router = APIRouter(
prefix=settings.prefix.auth,
tags=["Аuth"],
)
# /login
# /logout
router.include_router(
router=fastapi_users.get_auth_router(
authentication_backend,
# requires_verification=True,
),
)
# /register
router.include_router(
router=fastapi_users.get_register_router(
UserRead,
UserCreate,
),
)
# /request-verify-token
# /verify
router.include_router(
router=fastapi_users.get_verify_router(UserRead),
)
# /forgot-password
# /reset-password
router.include_router(
router=fastapi_users.get_reset_password_router(),
)

View File

@ -0,0 +1,16 @@
from fastapi_users import FastAPIUsers
from models import User
from config import settings
from authentication.dependencies import get_user_manager
from authentication.dependencies import authentication_backend
fastapi_users = FastAPIUsers[User, settings.type.UserIdType](
get_user_manager,
[authentication_backend],
)
current_active_user = fastapi_users.current_user(active=True)
current_active_superuser = fastapi_users.current_user(active=True, superuser=True)

View File

@ -0,0 +1,22 @@
from fastapi import APIRouter
from .user_router_helper import fastapi_users
from config import settings
from schemas.user import (
UserRead,
UserUpdate,
)
router = APIRouter(
prefix=settings.prefix.users,
tags=["Users"],
)
# /me
# /{id}
router.include_router(
router=fastapi_users.get_users_router(
UserRead,
UserUpdate,
),
)

View File

@ -0,0 +1,28 @@
from typing import (
TYPE_CHECKING,
Annotated,
)
from fastapi import Depends
from fastapi_users.authentication.strategy.db import (
DatabaseStrategy,
)
from config import settings
from authentication.dependencies import get_access_tokens_db
if TYPE_CHECKING:
from models import AccessToken
from fastapi_users.authentication.strategy.db import AccessTokenDatabase
def get_database_strategy(
access_tokens_db: Annotated[
"AccessTokenDatabase[AccessToken]",
Depends(get_access_tokens_db),
],
) -> DatabaseStrategy:
return DatabaseStrategy(
database=access_tokens_db,
lifetime_seconds=settings.access_token.lifetime_seconds,
)

View File

@ -3,6 +3,5 @@ from fastapi_users.authentication import BearerTransport
from config import settings
bearer_transport = BearerTransport(
# TODO: update url
tokenUrl="auth/jwt/login",
tokenUrl=settings.prefix.bearer_token_url,
)

View File

@ -0,0 +1,53 @@
import logging
from typing import Optional, TYPE_CHECKING
from fastapi_users import (
BaseUserManager,
IntegerIDMixin,
)
from config import settings
from models import User
if TYPE_CHECKING:
from fastapi import Request
class UserManager(IntegerIDMixin, BaseUserManager[User, settings.type.UserIdType]):
reset_password_token_secret = settings.access_token.reset_password_token_secret
verification_token_secret = settings.access_token.verification_token_secret
async def on_after_register(
self,
user: User,
request: Optional["Request"] = None,
):
logging.warning(
"User %r has registered.",
user.id,
)
async def on_after_request_verify(
self,
user: User,
token: str,
request: Optional["Request"] = None,
):
logging.warning(
"Verification requested for user %r. Verification token: %r",
user.id,
token,
)
async def on_after_forgot_password(
self,
user: User,
token: str,
request: Optional["Request"] = None,
):
logging.warning(
"User %r has forgot their password. Reset token: %r",
user.id,
token,
)

View File

@ -51,6 +51,26 @@ class TypesConfig(BaseModel):
UserIdType: ClassVar = int
class AccessToken(BaseModel):
lifetime_seconds: int = 3600
reset_password_token_secret: str
verification_token_secret: str
class ApiPrefix(BaseModel):
auth: str = "/auth"
web: str = "/web"
api: str = "/api"
users: str = "/users"
@property
def bearer_token_url(self) -> str:
# /auth/login
parts = (self.auth, "/login")
path = "".join(parts)
return path.removeprefix("/")
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=(
@ -65,6 +85,8 @@ class Settings(BaseSettings):
swagger: SwaggerConfig = SwaggerConfig()
db: DatabaseConfig
type: TypesConfig = TypesConfig()
access_token: AccessToken
prefix: ApiPrefix = ApiPrefix()
settings = Settings()

View File

@ -6,6 +6,7 @@ from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from starlette.staticfiles import StaticFiles
from web.routers import router as web_router
from api.routers import router as api_router
from models import db_helper
@ -24,9 +25,8 @@ main_app = FastAPI(
docs_url=None,
)
main_app.include_router(
web_router,
)
main_app.include_router(api_router)
main_app.include_router(web_router)
main_app.mount(

View File

@ -8,15 +8,15 @@ from fastapi_users_db_sqlalchemy import (
SQLAlchemyBaseUserTable,
SQLAlchemyUserDatabase,
)
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
class User(Base, IdIntPkMixin, SQLAlchemyBaseUserTable[settings.type.UserIdType]):
name: Mapped[str] = mapped_column(nullable=True)
tg_id: Mapped[int] = mapped_column(BigInteger, nullable=True)
name: Mapped[Optional[str]] = mapped_column(nullable=True)
tg_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
@classmethod
def get_db(cls, session: "AsyncSession"):

68
poetry.lock generated
View File

@ -791,6 +791,72 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "orjson"
version = "3.10.7"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"},
{file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"},
{file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"},
{file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"},
{file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"},
{file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"},
{file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"},
{file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"},
{file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"},
{file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"},
{file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"},
{file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"},
{file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"},
{file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"},
{file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"},
{file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"},
{file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"},
{file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"},
{file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"},
{file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"},
{file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"},
{file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"},
{file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"},
{file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"},
{file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"},
{file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"},
{file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"},
{file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"},
{file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"},
{file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"},
{file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"},
{file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"},
{file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"},
{file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"},
{file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"},
{file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"},
{file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"},
{file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"},
{file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"},
{file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"},
{file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"},
{file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"},
{file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"},
{file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"},
{file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"},
{file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"},
{file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"},
{file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"},
{file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"},
{file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"},
{file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"},
{file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"},
{file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"},
{file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"},
{file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"},
{file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"},
{file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"},
]
[[package]]
name = "packaging"
version = "24.1"
@ -1536,4 +1602,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "48335dd82829883c1d2ac4b19b8ae09d8455bf3b9add9aa6bd83bb07e61336f2"
content-hash = "41f38830f56655c559bf42be3e15ee645b33259384d5cd3a66632f839a34c3f4"

View File

@ -16,6 +16,7 @@ alembic = "^1.13.3"
asyncpg = "^0.29.0"
sqlalchemy = {extras = ["asyncio"], version = "^2.0.35"}
fastapi-users = {extras = ["sqlalchemy"], version = "^13.0.0"}
orjson = "^3.10.7"

0
schemas/__init__.py Normal file
View File

19
schemas/user.py Normal file
View File

@ -0,0 +1,19 @@
from fastapi_users import schemas
from config import settings
from typing import Optional
class UserRead(schemas.BaseUser[settings.type.UserIdType]):
name: Optional[str] = None
tg_id: Optional[int] = None
class UserCreate(schemas.BaseUserCreate):
name: Optional[str] = None
tg_id: Optional[int] = None
class UserUpdate(schemas.BaseUserUpdate):
name: Optional[str] = None
tg_id: Optional[int] = None