Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
21 changes: 21 additions & 0 deletions backend/src/infrastructure/auth/crudauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from crudauth import BearerTransport, CRUDAuth, SessionTransport

from ...modules.user.models import User
from ..config.settings import get_settings
from ..database.session import async_session

settings = get_settings()

auth = CRUDAuth(
session=async_session,
user_model=User,
SECRET_KEY=settings.SECRET_KEY,
transports=[
SessionTransport(
backend="redis",
redis_url=settings.SESSION_REDIS_URL,
csrf=True,
),
BearerTransport(access_ttl=900, refresh="cookie"),
],
)
10 changes: 10 additions & 0 deletions backend/src/infrastructure/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
4 changes: 4 additions & 0 deletions backend/src/interfaces/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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
Expand All @@ -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.initialize()

default_lifespan = lifespan_factory(settings)

async with default_lifespan(app):
yield

await auth.shutdown()

app = create_application(
router=router,
Expand Down
32 changes: 5 additions & 27 deletions backend/src/modules/user/models.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
41 changes: 41 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading