fastapi-auth-starter 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. fastapi_auth_starter/__init__.py +7 -0
  2. fastapi_auth_starter/cli.py +326 -0
  3. fastapi_auth_starter-0.1.3.data/data/README.md +247 -0
  4. fastapi_auth_starter-0.1.3.data/data/alembic/README +1 -0
  5. fastapi_auth_starter-0.1.3.data/data/alembic/env.py +100 -0
  6. fastapi_auth_starter-0.1.3.data/data/alembic/script.py.mako +28 -0
  7. fastapi_auth_starter-0.1.3.data/data/alembic/versions/279c472f4fd8_add_user_table.py +42 -0
  8. fastapi_auth_starter-0.1.3.data/data/alembic/versions/5f062b3648fa_change_user_id_from_uuid_to_string_for_.py +38 -0
  9. fastapi_auth_starter-0.1.3.data/data/alembic/versions/8d275132562b_create_tasks_table.py +44 -0
  10. fastapi_auth_starter-0.1.3.data/data/alembic.ini +150 -0
  11. fastapi_auth_starter-0.1.3.data/data/app/__init__.py +5 -0
  12. fastapi_auth_starter-0.1.3.data/data/app/api/__init__.py +4 -0
  13. fastapi_auth_starter-0.1.3.data/data/app/api/v1/__init__.py +4 -0
  14. fastapi_auth_starter-0.1.3.data/data/app/api/v1/api.py +21 -0
  15. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/__init__.py +4 -0
  16. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/auth.py +513 -0
  17. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/health.py +50 -0
  18. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/task.py +182 -0
  19. fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/user.py +144 -0
  20. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/__init__.py +8 -0
  21. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/auth.py +198 -0
  22. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/task.py +61 -0
  23. fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/user.py +96 -0
  24. fastapi_auth_starter-0.1.3.data/data/app/core/__init__.py +4 -0
  25. fastapi_auth_starter-0.1.3.data/data/app/core/config.py +107 -0
  26. fastapi_auth_starter-0.1.3.data/data/app/core/database.py +106 -0
  27. fastapi_auth_starter-0.1.3.data/data/app/core/dependencies.py +148 -0
  28. fastapi_auth_starter-0.1.3.data/data/app/core/exceptions.py +7 -0
  29. fastapi_auth_starter-0.1.3.data/data/app/db/__init__.py +4 -0
  30. fastapi_auth_starter-0.1.3.data/data/app/main.py +91 -0
  31. fastapi_auth_starter-0.1.3.data/data/app/models/__init__.py +14 -0
  32. fastapi_auth_starter-0.1.3.data/data/app/models/task.py +56 -0
  33. fastapi_auth_starter-0.1.3.data/data/app/models/user.py +45 -0
  34. fastapi_auth_starter-0.1.3.data/data/app/services/__init__.py +8 -0
  35. fastapi_auth_starter-0.1.3.data/data/app/services/auth.py +405 -0
  36. fastapi_auth_starter-0.1.3.data/data/app/services/task.py +165 -0
  37. fastapi_auth_starter-0.1.3.data/data/app/services/user.py +108 -0
  38. fastapi_auth_starter-0.1.3.data/data/pyproject.toml +77 -0
  39. fastapi_auth_starter-0.1.3.data/data/runtime.txt +2 -0
  40. fastapi_auth_starter-0.1.3.data/data/vercel.json +19 -0
  41. fastapi_auth_starter-0.1.3.dist-info/METADATA +283 -0
  42. fastapi_auth_starter-0.1.3.dist-info/RECORD +44 -0
  43. fastapi_auth_starter-0.1.3.dist-info/WHEEL +4 -0
  44. fastapi_auth_starter-0.1.3.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,107 @@
1
+ """
2
+ Application configuration settings
3
+ Handles environment variables and configuration management
4
+ All configuration values should be set in .env file or environment variables
5
+ Reference: https://fastapi.tiangolo.com/advanced/settings/
6
+ """
7
+ import json
8
+ from pydantic import Field, ConfigDict
9
+ from pydantic_settings import BaseSettings
10
+
11
+
12
+ class Settings(BaseSettings):
13
+ """
14
+ Application settings loaded from environment variables
15
+ Uses pydantic BaseSettings for validation and type conversion
16
+
17
+ All values should be set in .env file (see .env.example for template)
18
+ """
19
+ # API Configuration
20
+ # These can have defaults but should be overridden in .env
21
+ API_V1_PREFIX: str = "/api/v1"
22
+ PROJECT_NAME: str = "FastAPI Auth Starter"
23
+ VERSION: str = "0.1.0"
24
+
25
+ # Database Configuration
26
+ # PostgreSQL connection string format: postgresql+asyncpg://user:password@host:port/dbname
27
+ # Reference: https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
28
+ # Set in .env file or Vercel environment variables
29
+ # Note: Special characters in password must be URL-encoded (e.g., ! = %21)
30
+ DATABASE_URL: str = Field(
31
+ ...,
32
+ description="PostgreSQL database URL. Must be set via environment variable."
33
+ )
34
+
35
+ # WorkOS Configuration
36
+ # WorkOS API key for user management
37
+ # Reference: https://workos.com/docs/reference/api-reference/user-management
38
+ WORKOS_API_KEY: str = Field(
39
+ ...,
40
+ description="WorkOS API key. Must be set via environment variable."
41
+ )
42
+ WORKOS_CLIENT_ID: str = Field(
43
+ ...,
44
+ description="WorkOS client ID. Must be set via environment variable."
45
+ )
46
+
47
+ WORKOS_DEFAULT_CONNECTION_ID: str | None = Field(
48
+ None,
49
+ description="Default WorkOS SSO connection ID. Can be overridden by frontend."
50
+ )
51
+ # Allowed redirect URIs (comma-separated or JSON array)
52
+ # Security: Only these URIs are allowed for OAuth redirects
53
+ WORKOS_ALLOWED_REDIRECT_URIS: str = Field(
54
+ ...,
55
+ description="Comma-separated list of allowed redirect URIs for OAuth"
56
+ )
57
+
58
+ @property
59
+ def allowed_redirect_uris_list(self) -> list[str]:
60
+ """
61
+ Parse allowed redirect URIs into a list.
62
+
63
+ Supports two formats:
64
+ 1. JSON array: ["https://app.example.com/callback", "https://app2.example.com/callback"]
65
+ 2. Comma-separated: https://app.example.com/callback,https://app2.example.com/callback
66
+
67
+ Reference: https://docs.python.org/3/library/json.html
68
+ """
69
+ raw = self.WORKOS_ALLOWED_REDIRECT_URIS.strip()
70
+ if not raw:
71
+ return []
72
+
73
+ # Try parsing as JSON first (supports JSON array format)
74
+ try:
75
+ parsed = json.loads(raw)
76
+ except json.JSONDecodeError:
77
+ # Fall back to comma-separated string format
78
+ return [uri.strip() for uri in raw.split(",") if uri.strip()]
79
+
80
+ # Handle parsed JSON result
81
+ if isinstance(parsed, str):
82
+ return [parsed.strip()]
83
+ if isinstance(parsed, list):
84
+ return [str(uri).strip() for uri in parsed if str(uri).strip()]
85
+
86
+ raise ValueError(
87
+ "WORKOS_ALLOWED_REDIRECT_URIS must be a JSON array or comma-separated string"
88
+ )
89
+ # Alembic Configuration
90
+ # Used for database migrations
91
+ # Reference: https://alembic.sqlalchemy.org/en/latest/tutorial.html
92
+ ALEMBIC_CONFIG: str = "alembic.ini"
93
+
94
+ # Pydantic v2 configuration
95
+ # Reference: https://docs.pydantic.dev/latest/api/config/
96
+ model_config = ConfigDict(
97
+ env_file=".env", # Load from .env file (required)
98
+ case_sensitive=True, # Environment variable names are case-sensitive
99
+ extra="ignore", # Ignore extra environment variables not defined in this class
100
+ # This allows additional env vars (like WorkOS config) to exist without causing validation errors
101
+ )
102
+
103
+
104
+ # Global settings instance
105
+ # Import this in other modules to access configuration
106
+ settings = Settings()
107
+
@@ -0,0 +1,106 @@
1
+ """
2
+ Database connection and session management
3
+ Uses SQLAlchemy async engine for PostgreSQL
4
+ Reference: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
5
+ """
6
+ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
7
+ from sqlalchemy.orm import DeclarativeBase
8
+ from sqlalchemy.pool import NullPool
9
+
10
+ from app.core.config import settings
11
+
12
+
13
+ # Validate DATABASE_URL is set
14
+ if not settings.DATABASE_URL:
15
+ raise ValueError(
16
+ "DATABASE_URL environment variable is not set. "
17
+ "Please set it in your .env file or Vercel environment variables."
18
+ )
19
+
20
+ # Create async engine for PostgreSQL
21
+ # Using NullPool for serverless (Vercel) - each request gets a fresh connection
22
+ # Connection pooling doesn't work well in serverless environments where functions
23
+ # are isolated and can be terminated/cold-started
24
+ # Reference: https://docs.sqlalchemy.org/en/20/core/pooling.html#switching-pool-implementations
25
+ # Reference: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#using-multiple-asyncio-event-loops
26
+ engine = create_async_engine(
27
+ settings.DATABASE_URL,
28
+ echo=False, # Set to True for SQL query logging (useful for debugging)
29
+ poolclass=NullPool, # No connection pooling - each request gets a new connection
30
+ # This is critical for serverless environments where connection reuse
31
+ # across function invocations causes connection termination errors
32
+ # Reference: https://docs.sqlalchemy.org/en/20/core/pooling.html#disabling-pooling-using-nullpool
33
+ connect_args={
34
+ # asyncpg-specific connection arguments
35
+ # Reference: https://magicstack.github.io/asyncpg/current/api/index.html#connection
36
+ "command_timeout": 60, # Increased timeout for serverless network latency
37
+ "server_settings": {
38
+ "application_name": "fastapi_auth_starter",
39
+ },
40
+ },
41
+ )
42
+
43
+
44
+ # Create async session factory
45
+ # This is used to create database sessions throughout the application
46
+ # Reference: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#session-basics
47
+ async_session_maker = async_sessionmaker(
48
+ engine,
49
+ class_=AsyncSession,
50
+ expire_on_commit=False, # Keep objects accessible after commit
51
+ # This is important for serverless - objects remain accessible after commit
52
+ # allowing timestamps to be read even if not refreshed
53
+ autocommit=False,
54
+ autoflush=False,
55
+ )
56
+
57
+
58
+ # Base class for all database models
59
+ # All models should inherit from this class
60
+ # Reference: https://docs.sqlalchemy.org/en/20/orm/declarative_styles.html
61
+ class Base(DeclarativeBase):
62
+ """Base class for SQLAlchemy declarative models"""
63
+ pass
64
+
65
+
66
+ # Dependency to get database session
67
+ # Used in FastAPI route handlers via dependency injection
68
+ # Reference: https://fastapi.tiangolo.com/tutorial/dependencies/
69
+ async def get_db() -> AsyncSession:
70
+ """
71
+ Dependency function that provides a database session
72
+ Automatically closes the session after the request completes
73
+
74
+ For serverless environments (Vercel), this ensures proper transaction handling:
75
+ - Commits on success
76
+ - Rolls back on error
77
+ - Always closes the session
78
+ """
79
+ async with async_session_maker() as session:
80
+ try:
81
+ yield session
82
+ # Commit transaction on success
83
+ # This commits all changes made during the request
84
+ # In serverless, this must complete before the function terminates
85
+ await session.commit()
86
+ except Exception as e:
87
+ # Rollback on any exception to maintain data consistency
88
+ try:
89
+ await session.rollback()
90
+ except Exception:
91
+ # If rollback fails, connection is likely already closed
92
+ # This can happen in serverless when connections are terminated
93
+ pass
94
+ # Re-raise the original exception so FastAPI can handle it properly
95
+ raise
96
+ finally:
97
+ # Always close the session to release connection
98
+ # With NullPool, this closes the connection completely
99
+ # In serverless, this is critical to prevent connection leaks
100
+ try:
101
+ await session.close()
102
+ except Exception:
103
+ # If close fails, connection is likely already closed or terminated
104
+ # This is acceptable - we're in cleanup anyway
105
+ pass
106
+
@@ -0,0 +1,148 @@
1
+ """
2
+ Authentication dependencies for protecting endpoints
3
+ Reference: https://fastapi.tiangolo.com/tutorial/dependencies/
4
+ """
5
+ import asyncio
6
+ import logging
7
+ from functools import lru_cache
8
+ from fastapi import Depends, HTTPException, status
9
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
10
+ from workos import WorkOSClient
11
+ from workos.exceptions import AuthenticationException, NotFoundException
12
+
13
+ from app.services.auth import AuthService
14
+ from app.api.v1.schemas.auth import WorkOSUserResponse
15
+ from app.core.config import settings
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # HTTPBearer automatically extracts Bearer token from Authorization header
20
+ # Reference: https://fastapi.tiangolo.com/reference/security/#fastapi.security.HTTPBearer
21
+ security = HTTPBearer()
22
+
23
+
24
+ @lru_cache()
25
+ def get_auth_service() -> AuthService:
26
+ """
27
+ Get a singleton AuthService instance.
28
+
29
+ Using lru_cache ensures the same AuthService instance is reused across requests,
30
+ which preserves the JWKS cache (_jwks_cache and _jwks_cache_expiry) at the
31
+ application level rather than request level. This avoids repeated JWKS API calls
32
+ to WorkOS and improves performance.
33
+
34
+ Reference: https://docs.python.org/3/library/functools.html#functools.lru_cache
35
+ """
36
+ return AuthService()
37
+
38
+
39
+ async def get_current_user(
40
+ credentials: HTTPAuthorizationCredentials = Depends(security)
41
+ ) -> WorkOSUserResponse:
42
+ """
43
+ Dependency to get the current authenticated user.
44
+
45
+ Validates the JWT access token from the Authorization header using WorkOS JWKS,
46
+ then fetches full user details from WorkOS.
47
+
48
+ Usage:
49
+ @router.get("/protected")
50
+ async def protected_route(current_user = Depends(get_current_user)):
51
+ return {"user_id": current_user.id, "email": current_user.email}
52
+
53
+ Args:
54
+ credentials: HTTPAuthorizationCredentials containing the Bearer token
55
+
56
+ Returns:
57
+ WorkOSUserResponse: The authenticated user with full details
58
+
59
+ Raises:
60
+ HTTPException: 401 if token is invalid or missing
61
+ """
62
+ auth_service = get_auth_service()
63
+
64
+ try:
65
+ # Extract the token from credentials
66
+ access_token = credentials.credentials
67
+ logger.debug(f"Verifying session with token: {access_token[:20]}...")
68
+
69
+ # Verify the session with WorkOS (validates JWT signature and expiration)
70
+ # Reference: https://workos.com/docs/reference/authkit/session-tokens/access-token
71
+ session_data = await auth_service.verify_session(access_token)
72
+
73
+ user_id = session_data.get('user_id')
74
+ if not user_id:
75
+ logger.error("Token missing user_id (sub claim)")
76
+ raise HTTPException(
77
+ status_code=status.HTTP_401_UNAUTHORIZED,
78
+ detail="Invalid token: missing user information",
79
+ headers={"WWW-Authenticate": "Bearer"},
80
+ )
81
+
82
+ logger.debug(f"Token verified successfully. User ID: {user_id}")
83
+
84
+ # Fetch full user details from WorkOS
85
+ # The JWT contains basic info, but we fetch full details for complete user object
86
+ workos_client = WorkOSClient(
87
+ api_key=settings.WORKOS_API_KEY,
88
+ client_id=settings.WORKOS_CLIENT_ID
89
+ )
90
+
91
+ # Offload synchronous WorkOS call to thread pool
92
+ workos_user = await asyncio.to_thread(
93
+ workos_client.user_management.get_user,
94
+ user_id=user_id
95
+ )
96
+
97
+ # Convert WorkOS user to our schema
98
+ user = WorkOSUserResponse(
99
+ object=workos_user.object,
100
+ id=workos_user.id,
101
+ email=workos_user.email,
102
+ first_name=workos_user.first_name,
103
+ last_name=workos_user.last_name,
104
+ email_verified=workos_user.email_verified,
105
+ profile_picture_url=workos_user.profile_picture_url,
106
+ created_at=workos_user.created_at,
107
+ updated_at=workos_user.updated_at,
108
+ )
109
+
110
+ logger.debug(f"User fetched: {user.id} ({user.email})")
111
+ return user
112
+
113
+ except ValueError as e:
114
+ # Map error to RFC6750-compliant WWW-Authenticate header
115
+ msg = str(e)
116
+ # Default values
117
+ error = "invalid_token"
118
+ description = msg or "The access token is invalid"
119
+
120
+ if "expired" in msg.lower():
121
+ description = "The access token expired"
122
+
123
+ raise HTTPException(
124
+ status_code=status.HTTP_401_UNAUTHORIZED,
125
+ detail=msg,
126
+ headers={
127
+ "WWW-Authenticate": f'Bearer realm="api", error="{error}", error_description="{description}"'
128
+ },
129
+ ) from e
130
+ except NotFoundException:
131
+ # User not found in WorkOS (shouldn't happen if token is valid)
132
+ logger.error(f"User not found in WorkOS after token verification")
133
+ raise HTTPException(
134
+ status_code=status.HTTP_401_UNAUTHORIZED,
135
+ detail="User not found",
136
+ headers={"WWW-Authenticate": "Bearer"},
137
+ )
138
+ except HTTPException:
139
+ # Re-raise HTTP exceptions (already formatted)
140
+ raise
141
+ except Exception as e:
142
+ # Unexpected error - log full details for debugging
143
+ logger.error(f"Unexpected authentication error: {type(e).__name__}: {e}", exc_info=True)
144
+ raise HTTPException(
145
+ status_code=status.HTTP_401_UNAUTHORIZED,
146
+ detail=f"Authentication failed: {str(e)}",
147
+ headers={"WWW-Authenticate": "Bearer"},
148
+ ) from e
@@ -0,0 +1,7 @@
1
+ class InvalidPasswordException(Exception):
2
+ """
3
+ Exception raised when the old password is incorrect
4
+ """
5
+ def __init__(self, message: str = "Old password is incorrect"):
6
+ self.message = message
7
+ super().__init__(self.message)
@@ -0,0 +1,4 @@
1
+ """
2
+ Database utilities and helpers
3
+ """
4
+
@@ -0,0 +1,91 @@
1
+ """
2
+ FastAPI application entry point
3
+ Main application factory and configuration
4
+ Reference: https://fastapi.tiangolo.com/tutorial/bigger-applications/
5
+ """
6
+ import logging
7
+ from contextlib import asynccontextmanager
8
+
9
+ from fastapi import FastAPI
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from sqlalchemy import text
12
+
13
+ from app.api.v1.api import api_router
14
+ from app.core.config import settings
15
+ from app.core.database import engine
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @asynccontextmanager
21
+ async def lifespan(app: FastAPI):
22
+ """
23
+ Lifespan context manager for startup and shutdown events
24
+ Validates database connection on startup
25
+ Reference: https://fastapi.tiangolo.com/advanced/events/
26
+ """
27
+ # Startup: Test database connection
28
+ try:
29
+ async with engine.begin() as conn:
30
+ await conn.execute(text("SELECT 1"))
31
+ logger.info("✓ Database connection successful")
32
+ except Exception as e:
33
+ logger.error(f"✗ Database connection failed: {e}")
34
+ logger.error(
35
+ "Please check:\n"
36
+ "1. DATABASE_URL is set correctly in Vercel environment variables\n"
37
+ "2. AWS RDS security group allows connections from Vercel IP ranges\n"
38
+ "3. Database is accessible and credentials are correct"
39
+ )
40
+ # Don't raise - let the app start but connections will fail
41
+ # This allows health checks to work
42
+
43
+ yield
44
+
45
+ # Shutdown: Dispose of database connections
46
+ await engine.dispose()
47
+ logger.info("Database connections closed")
48
+
49
+
50
+ # Create FastAPI application instance
51
+ # Reference: https://fastapi.tiangolo.com/reference/fastapi/
52
+ app = FastAPI(
53
+ title=settings.PROJECT_NAME,
54
+ version=settings.VERSION,
55
+ description="A clean architecture FastAPI starter with PostgreSQL and Alembic",
56
+ docs_url="/docs", # Swagger UI documentation
57
+ redoc_url="/redoc", # ReDoc documentation
58
+ lifespan=lifespan, # Add lifespan for startup/shutdown events
59
+ )
60
+
61
+ allowed_origins = [
62
+ "http://localhost:3000",
63
+ # add other trusted origins here
64
+ ]
65
+
66
+ app.add_middleware(
67
+ CORSMiddleware,
68
+ allow_origins=allowed_origins,
69
+ allow_credentials=True,
70
+ allow_methods=["*"],
71
+ allow_headers=["*"],
72
+ )
73
+
74
+
75
+ # Include API routers
76
+ # All routes from api_router will be included in the main app
77
+ app.include_router(api_router)
78
+
79
+
80
+ @app.get("/")
81
+ async def root():
82
+ """
83
+ Root endpoint
84
+ Provides basic information about the API
85
+ """
86
+ return {
87
+ "message": f"Welcome to {settings.PROJECT_NAME}",
88
+ "version": settings.VERSION,
89
+ "docs": "/docs",
90
+ }
91
+
@@ -0,0 +1,14 @@
1
+ """
2
+ Database models
3
+ All SQLAlchemy models should be defined here or imported here
4
+ """
5
+
6
+ # Import Base for models to inherit from
7
+ from app.core.database import Base
8
+
9
+ # Import models here as they are created
10
+ from app.models.task import Task
11
+ from app.models.user import User
12
+
13
+ # Export all models for easy imports
14
+ __all__ = ["Base", "Task", "User"]
@@ -0,0 +1,56 @@
1
+ """
2
+ Task database model
3
+ SQLAlchemy model for tasks
4
+ Reference: https://docs.sqlalchemy.org/en/20/orm/declarative_styles.html
5
+ """
6
+ from datetime import datetime
7
+ from sqlalchemy import Integer, String, Boolean, DateTime, func
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+
10
+ from app.core.database import Base
11
+
12
+
13
+ class Task(Base):
14
+ """
15
+ Task model representing a task in the database
16
+
17
+ Attributes:
18
+ id: Primary key, auto-incrementing integer
19
+ title: Task title (required)
20
+ description: Optional task description
21
+ completed: Whether the task is completed (default: False)
22
+ created_at: Timestamp when task was created (auto-generated)
23
+ updated_at: Timestamp when task was last updated (auto-generated)
24
+
25
+ Reference: https://docs.sqlalchemy.org/en/20/orm/mapped_sql_expressions.html
26
+ """
27
+ __tablename__ = "tasks"
28
+
29
+ # Primary key
30
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
31
+
32
+ # Task fields
33
+ title: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
34
+ description: Mapped[str | None] = mapped_column(String(1000), nullable=True)
35
+ completed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
36
+
37
+ # Timestamps
38
+ # Using server_default with func.now() for automatic timestamp generation
39
+ # These are set by PostgreSQL on INSERT/UPDATE
40
+ # Reference: https://docs.sqlalchemy.org/en/20/core/defaults.html#server-side-defaults
41
+ created_at: Mapped[datetime] = mapped_column(
42
+ DateTime(timezone=True),
43
+ server_default=func.now(), # Server-side default for creation time
44
+ nullable=False,
45
+ )
46
+ updated_at: Mapped[datetime] = mapped_column(
47
+ DateTime(timezone=True),
48
+ server_default=func.now(), # Server-side default for initial value
49
+ onupdate=func.now(), # Update on modification
50
+ nullable=False,
51
+ )
52
+
53
+ def __repr__(self) -> str:
54
+ """String representation of Task"""
55
+ return f"<Task(id={self.id}, title='{self.title}', completed={self.completed})>"
56
+
@@ -0,0 +1,45 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from sqlalchemy import Boolean, DateTime, Integer, String, func
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+ from app.core.database import Base
6
+
7
+
8
+ class User(Base):
9
+ """
10
+ User model representing a user in the database
11
+
12
+ Attributes:
13
+ id: Primary key, UUID
14
+ email: User email (required)
15
+ first_name: User first name (optional)
16
+ last_name: User last name (optional)
17
+ created_at: Timestamp when user was created (auto-generated)
18
+ updated_at: Timestamp when user was last updated (auto-generated)
19
+
20
+ Reference: https://docs.sqlalchemy.org/en/20/orm/mapped_sql_expressions.html
21
+ """
22
+ __tablename__ = "users"
23
+
24
+ id: Mapped[str] = mapped_column(String, primary_key=True)
25
+ email: Mapped[str] = mapped_column(String, unique=True, index=True)
26
+ first_name: Mapped[Optional[str]] = mapped_column(String, nullable=True)
27
+ last_name: Mapped[Optional[str]] = mapped_column(String, nullable=True)
28
+ # pending_verification_token: Mapped[Optional[str]] = mapped_column(String, nullable=True)
29
+ # token_expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
30
+ # is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
31
+ created_at: Mapped[datetime] = mapped_column(
32
+ DateTime(timezone=True),
33
+ server_default=func.now(), # Server-side default for creation time
34
+ nullable=False,
35
+ )
36
+ updated_at: Mapped[datetime] = mapped_column(
37
+ DateTime(timezone=True),
38
+ server_default=func.now(), # Server-side default for initial value
39
+ onupdate=func.now(), # Update on modification
40
+ nullable=False,
41
+ )
42
+
43
+ def __repr__(self) -> str:
44
+ "String representation of user"
45
+ return f"<User(id={self.id}, email='{self.email}', created_at={self.created_at})>"
@@ -0,0 +1,8 @@
1
+ """
2
+ Business logic services
3
+ Service layer for application business logic
4
+ """
5
+
6
+ from app.services.task import TaskService
7
+
8
+ __all__ = ["TaskService"]