From fc50d6e43e03dcaf67941d077e2dee2d5854d465 Mon Sep 17 00:00:00 2001 From: LucasQR Date: Fri, 19 Jun 2026 11:16:56 -0300 Subject: [PATCH 1/2] configuring and updating user --- backend/src/infrastructure/auth/crudauth.py | 20 ++++++++++ backend/src/infrastructure/config/settings.py | 10 +++++ backend/src/interfaces/main.py | 4 ++ backend/src/modules/user/models.py | 32 +++------------ pyproject.toml | 3 ++ uv.lock | 39 +++++++++++++++++++ 6 files changed, 81 insertions(+), 27 deletions(-) create mode 100644 backend/src/infrastructure/auth/crudauth.py diff --git a/backend/src/infrastructure/auth/crudauth.py b/backend/src/infrastructure/auth/crudauth.py new file mode 100644 index 00000000..17e15e9d --- /dev/null +++ b/backend/src/infrastructure/auth/crudauth.py @@ -0,0 +1,20 @@ +from crudauth import CRUDAuth, SessionTransport +from crudauth.transports.bearer import BearerTransport, +from ..config.settings import get_settings +from ..database.session import async_session +from ..modules.user.models import User + +settinhs - get_settings() +auth = CRUDAuth( + session = async_session, + user_model = User, + SECRET_KEY = settings.SECRET_KEY, + transports = [ + SessionTransport( + backends = "redis", + redis_url = settings.SESSION_REDIS_URL, + csrf = True, + ), + BearerTransport(access_ttl=900, refresh="cookie"), + ], +) diff --git a/backend/src/infrastructure/config/settings.py b/backend/src/infrastructure/config/settings.py index 525b3c8f..78d2dc62 100644 --- a/backend/src/infrastructure/config/settings.py +++ b/backend/src/infrastructure/config/settings.py @@ -248,6 +248,16 @@ class AuthSettings(BaseSettings): OAUTH_GITHUB_CLIENT_SECRET: str = config("OAUTH_GITHUB_CLIENT_SECRET", default="") OAUTH_REDIRECT_BASE_URL: str = config("OAUTH_REDIRECT_BASE_URL", default="http://localhost:8000") + SESSION_REDIS_HOST: str = config("SESSION_REDIS_HOST", default="localhost") + SESSION_REDIS_PORT: int = config("SESSION_REDIS_PORT", default=6379, cast=int) + SESSION_REDIS_DB: int = config("SESSION_REDIS_DB", default=2, cast=int) + SESSION_REDIS_PASSWORD: str | None = config("SESSION_REDIS_PASSWORD", default=None) + + @property + def SESSION_REDIS_URL(self) -> str: + password_part = f":{self.SESSION_REDIS_PASSWORD}@" if self.SESSION_REDIS_PASSWORD else "" + return f"redis://{password_part}{self.SESSION_REDIS_HOST}:{self.SESSION_REDIS_PORT}/{self.SESSION_REDIS_DB}" + class APISettings(BaseSettings): """API-related settings.""" diff --git a/backend/src/interfaces/main.py b/backend/src/interfaces/main.py index 9c3f3037..15e60fb0 100644 --- a/backend/src/interfaces/main.py +++ b/backend/src/interfaces/main.py @@ -9,6 +9,7 @@ from ..infrastructure.security import validate_production_security from ..interfaces.api import router from .admin.initialize import create_admin_interface +from ..infrastructure.auth.crudauth import auth settings = get_settings() @@ -19,11 +20,14 @@ async def lifespan_with_security(app: FastAPI) -> AsyncGenerator[None, None]: if settings.PRODUCTION_SECURITY_VALIDATION_ENABLED: validate_production_security(settings) + await auth.initializa() + default_lifespan = lifespan_factory(settings) async with default_lifespan(app): yield + await auth.shutdoen() app = create_application( router=router, diff --git a/backend/src/modules/user/models.py b/backend/src/modules/user/models.py index a294e557..19800917 100644 --- a/backend/src/modules/user/models.py +++ b/backend/src/modules/user/models.py @@ -1,35 +1,22 @@ -from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKey, Integer, String +from crudauth.models import AuthUserMixin +from sqlalchemy import ForeignKey, Integer, String from sqlalchemy.orm import Mapped, mapped_column, relationship -from ...infrastructure.database.models import SoftDeleteMixin, TimestampMixin +from ...infrastructure.database.models import SoftDeleteMixin from ...infrastructure.database.session import Base if TYPE_CHECKING: from ..tier.models import Tier -class User(Base, TimestampMixin, SoftDeleteMixin): +class User(Base, SoftDeleteMixin, AuthUserMixin): """User model representing application users.""" __tablename__ = "user" - id: Mapped[int] = mapped_column( - "id", - autoincrement=True, - nullable=False, - unique=True, - primary_key=True, - init=False, - ) - - name: Mapped[str] = mapped_column(String(30)) - username: Mapped[str] = mapped_column(String(20), unique=True, index=True) - email: Mapped[str] = mapped_column(String(50), unique=True, index=True) - hashed_password: Mapped[str] = mapped_column(String(100)) - + name: Mapped[str] = mapped_column(String(30), kw_only=True) profile_image_url: Mapped[str] = mapped_column(String, default="https://profileimageurl.com") tier_id: Mapped[int | None] = mapped_column( @@ -39,15 +26,6 @@ class User(Base, TimestampMixin, SoftDeleteMixin): default=None, ) - is_superuser: Mapped[bool] = mapped_column(default=False) - - google_id: Mapped[str | None] = mapped_column(String(50), unique=True, index=True, default=None) - github_id: Mapped[str | None] = mapped_column(String(50), unique=True, index=True, default=None) - oauth_provider: Mapped[str | None] = mapped_column(String(20), default=None) - email_verified: Mapped[bool] = mapped_column(default=False) - oauth_created_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None) - oauth_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None) - tier: Mapped["Tier | None"] = relationship("Tier", back_populates="users", lazy="selectin", init=False) def __repr__(self) -> str: diff --git a/pyproject.toml b/pyproject.toml index 0fe84d5b..1cc03f80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,9 @@ name = "fastapi-boilerplate-workspace" version = "0" description = "Workspace root — see backend/ and cli/ for actual packages." requires-python = ">=3.11" +dependencies = [ + "crudauth[all]>=0.2.1", +] [tool.uv.workspace] members = ["backend", "cli"] diff --git a/uv.lock b/uv.lock index 49f86dcd..3ca4c039 100644 --- a/uv.lock +++ b/uv.lock @@ -533,6 +533,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "crudauth" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "email-validator" }, + { name = "fastapi" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "python-multipart" }, + { name = "sqlalchemy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/2dc0ba53d2ea240eb65fc24dc8121c1ed394bbea38baec7cff372efe5261/crudauth-0.2.1-py3-none-any.whl", hash = "sha256:b78857fc15e8cfd379c52999bd0947b13ac2b53c61306126d1a16e47cdb107cf", size = 103673, upload-time = "2026-06-15T05:15:40.339Z" }, +] + +[package.optional-dependencies] +all = [ + { name = "httpx" }, + { name = "redis" }, + { name = "user-agents" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -712,6 +736,12 @@ requires-dist = [ name = "fastapi-boilerplate-workspace" version = "0" source = { virtual = "." } +dependencies = [ + { name = "crudauth", extra = ["all"] }, +] + +[package.metadata] +requires-dist = [{ name = "crudauth", extras = ["all"], specifier = ">=0.2.1" }] [[package]] name = "fastapi-cli" @@ -1841,6 +1871,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + [[package]] name = "pytest" version = "9.0.3" From d9c7fe1cd1bc030d0cd35dbbfd5544353d946de4 Mon Sep 17 00:00:00 2001 From: LucasQR Date: Fri, 19 Jun 2026 11:25:52 -0300 Subject: [PATCH 2/2] forgot the tests 1 --- backend/pyproject.toml | 1 + backend/src/infrastructure/auth/crudauth.py | 23 +++++++++++---------- backend/src/interfaces/main.py | 6 +++--- uv.lock | 2 ++ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 30df749f..96113616 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "taskiq-redis>=1.1.2", "taskiq-aio-pika>=0.4.3", "user-agents>=2.2.0", + "testcontainers>=4.14.2", ] [project.optional-dependencies] diff --git a/backend/src/infrastructure/auth/crudauth.py b/backend/src/infrastructure/auth/crudauth.py index 17e15e9d..e22235be 100644 --- a/backend/src/infrastructure/auth/crudauth.py +++ b/backend/src/infrastructure/auth/crudauth.py @@ -1,19 +1,20 @@ -from crudauth import CRUDAuth, SessionTransport -from crudauth.transports.bearer import BearerTransport, +from crudauth import BearerTransport, CRUDAuth, SessionTransport + +from ...modules.user.models import User from ..config.settings import get_settings from ..database.session import async_session -from ..modules.user.models import User -settinhs - get_settings() +settings = get_settings() + auth = CRUDAuth( - session = async_session, - user_model = User, - SECRET_KEY = settings.SECRET_KEY, - transports = [ + session=async_session, + user_model=User, + SECRET_KEY=settings.SECRET_KEY, + transports=[ SessionTransport( - backends = "redis", - redis_url = settings.SESSION_REDIS_URL, - csrf = True, + backend="redis", + redis_url=settings.SESSION_REDIS_URL, + csrf=True, ), BearerTransport(access_ttl=900, refresh="cookie"), ], diff --git a/backend/src/interfaces/main.py b/backend/src/interfaces/main.py index 15e60fb0..624bc832 100644 --- a/backend/src/interfaces/main.py +++ b/backend/src/interfaces/main.py @@ -5,11 +5,11 @@ from starlette.middleware.sessions import SessionMiddleware from ..infrastructure.app_factory import create_application, lifespan_factory +from ..infrastructure.auth.crudauth import auth from ..infrastructure.config.settings import get_settings from ..infrastructure.security import validate_production_security from ..interfaces.api import router from .admin.initialize import create_admin_interface -from ..infrastructure.auth.crudauth import auth settings = get_settings() @@ -20,14 +20,14 @@ async def lifespan_with_security(app: FastAPI) -> AsyncGenerator[None, None]: if settings.PRODUCTION_SECURITY_VALIDATION_ENABLED: validate_production_security(settings) - await auth.initializa() + await auth.initialize() default_lifespan = lifespan_factory(settings) async with default_lifespan(app): yield - await auth.shutdoen() + await auth.shutdown() app = create_application( router=router, diff --git a/uv.lock b/uv.lock index 3ca4c039..19813595 100644 --- a/uv.lock +++ b/uv.lock @@ -668,6 +668,7 @@ dependencies = [ { name = "taskiq" }, { name = "taskiq-aio-pika" }, { name = "taskiq-redis" }, + { name = "testcontainers" }, { name = "user-agents" }, ] @@ -710,6 +711,7 @@ requires-dist = [ { name = "taskiq", specifier = ">=0.11.20" }, { name = "taskiq-aio-pika", specifier = ">=0.4.3" }, { name = "taskiq-redis", specifier = ">=1.1.2" }, + { name = "testcontainers", specifier = ">=4.14.2" }, { name = "testcontainers", extras = ["postgres"], marker = "extra == 'dev'", specifier = ">=4.10.0" }, { name = "user-agents", specifier = ">=2.2.0" }, ]