Skip to content

Latest commit

 

History

History
106 lines (90 loc) · 9.15 KB

File metadata and controls

106 lines (90 loc) · 9.15 KB

Technical Documentation

System Overview

An async FastAPI application that handles authentication and Stripe-backed subscription billing. It uses SQLAlchemy for data access, Stripe Checkout for payments, and Celery (with Redis) for background jobs such as subscription emails and expiry sweeps.

Architecture Overview

  • Entry point: src/main.py wires routers, CORS, slowapi rate limiting, request logging, and validation error handling.
  • Routing: src/auth/router.py and src/billing/router.py expose the API surface. Dependencies inject repositories/services and enforce auth/admin checks.
  • Services: Business rules live in src/auth/service.py and src/billing/service.py.
  • Repositories: Data access layer (src/repository.py, src/auth/repository.py, src/billing/repository.py) built on async SQLAlchemy sessions.
  • Data models: src/auth/models.py, src/billing/models.py, src/models.py define tables for users, profiles, plans, subscriptions, payments, refresh tokens, and OTP codes.
  • Utilities: JWT handling (src/jwt.py), hashing (src/hashing.py), email config (src/utils.py), rate limiting (src/rate_limiter.py), logging (src/logging.py), and OAuth/OTP helpers (src/auth/utils.py).
  • Background work: Celery worker/beat in src/celery_app.py with tasks in src/billing/tasks.py and placeholder src/tasks.py.
  • Templates: Jinja email templates under templates/email/ for verification, reset, OTP, and subscription emails.
  • Infrastructure: Docker Compose for API, Postgres, Redis, Celery worker/beat, and pgAdmin.

Request Lifecycle

  1. FastAPI receives the request, rate limiting runs via slowapi, and CORS headers are applied.
  2. Router-level dependencies resolve repositories and current user/admin guards (src/auth_bearer.py).
  3. Service methods apply business rules and call repositories to read/write Postgres.
  4. Token/cookie handling occurs at the router layer for login/refresh/OAuth flows.
  5. Responses return Pydantic schemas; validation errors use a custom handler returning {errors: {field: message}}.

Authentication Domain

  • Registration & Profiles: UserService.register_user validates unique email/username, hashes passwords, and auto-creates an empty Profile via UserRepository.create.
  • Login (password): Verifies Argon2 hash, issues access/refresh JWTs (src/jwt.py), stores hashed refresh token with JTI in refresh_tokens (rotation enforced), and sets httpOnly cookie.
  • Refresh rotation: /refresh-token verifies JWT, looks up JTI in DB, validates non-revoked/non-expired, issues new tokens, revokes old, and stores new hashed refresh token.
  • Email verification: Validation token generated by validation_secret_key; /verify marks is_verified; /request/verify resends via BackgroundTasks.
  • Password reset/change: Reset token emailed via send_password_reset_email; /new-password updates hash; /change-password checks old password for authenticated users.
  • OTP login: /request/login-code creates hashed code (LoginCodeRepository), emails it; /login/code verifies against latest, enforces expiry, deletes on use, and issues tokens.
  • OAuth (Google/GitHub): State cookie set on login endpoints; callback exchanges code for token (auth/utils.py), fetches profile/email, auto-provisions user with provider flag, and issues tokens.
  • Account deactivation: /deactivate marks is_active=False; further access is blocked by auth dependency checks.
  • Access control: auth_bearer.py enforces active/verified users and admin-only routes for plan management.

Billing & Subscription Domain

  • Plans: CRUD via PlanService/PlanRepository; plan tiers (PlanTier) stored on plans for feature gating; soft delete sets is_active=False. Stripe product/price is created/updated via StripeGateway and stored on the plan.
  • Subscriptions: SubscriptionRepoistory tracks access windows (current_period_end). New subscriptions start PAST_DUE until webhook confirmation. Duplicate active subs are blocked.
  • Checkout & Upgrade: /billing/subscriptions/subscribe and /upgrade create Stripe Checkout sessions with metadata (plan/user and optional upgrade_from_subscription_id). Customer is ensured/created before checkout.
  • Cancellation: /billing/subscriptions/cancel marks cancel_at_period_end and, for Stripe, modifies the subscription. Local record updated with canceled_at/period end.
  • Payments: PaymentRepository stores invoices (provider invoice id, amount, currency, status). Recorded on invoice.payment_succeeded webhooks.

Stripe Integration & Webhooks

  • Webhook endpoint /billing/stripe/webhook verifies signature via stripe.Webhook.construct_event.
  • Events handled:
    • checkout.session.completed: fetches Stripe subscription, handles upgrade (cancels old), and creates a local subscription.
    • invoice.payment_succeeded: retrieves subscription, updates period start/end, records payment, and dispatches Celery email tasks (create vs renewal based on billing_reason).
    • customer.subscription.deleted: cancels local subscription, setting current_period_end to now.
    • invoice.payment_failed: marks subscription PAST_DUE.
  • Stripe metadata carries plan_id, plan_code, user_id, and upgrade source IDs for proper reconciliation.

Background Tasks (Celery)

  • Celery worker (src.celery_app.celery_app) and beat (src.celery_app.beat_app) use Redis URLs from env.
  • Tasks: send_subscription_email_task and send_update_subscription_email_task dispatch templated emails; expire_subscriptions_task (beat) cancels subscriptions whose current_period_end has passed.
  • Legacy placeholder src/tasks.py:expire_subscriptions prints a TODO and is unused by beat.
  • Sync DB engine (SYNC_DATABASE_URL) is used inside Celery tasks via get_sync_session.

Database Schema

  • users: id, email, username, password (nullable for social), admin/active/verified flags, provider, stripe_customer_id, timestamps; 1-1 profile, 1-many subscriptions and refresh tokens.
  • profiles: per-user names/image, timestamps; created alongside user.
  • login_codes: hashed OTPs with expiry per user.
  • refresh_tokens: hashed token with JTI, expiry, revoked flags, replacement JTI.
  • plans: name/code, price_cents, currency, billing_period, tier, is_active, Stripe product/price IDs, timestamps.
  • subscriptions: user/plan FKs, status, provider/provider IDs, period start/end, cancel flags/timestamps.
  • payments: subscription/user FKs, provider invoice id, amount/currency, status, provider enum; unique per provider/invoice id.

API Surface (High Level)

  • Auth: registration/login/refresh, email verification (request/verify), password reset/change, OTP login, Google/GitHub OAuth, deactivate account.
  • Billing: plans list/create/get/update/delete (admin for mutations); subscriptions me/subscribe/upgrade/cancel; payments me; Stripe webhook (rate-limit exempt).
  • Rate limiting: default 5/min per Authorization header or IP (src/rate_limiter.py).

Error Handling

  • Validation errors use validation_exception_handler to return {"errors": {field: message}} with 422 status.
  • Business rule violations raise HTTPException with descriptive messages and appropriate HTTP codes.
  • Stripe webhooks return error strings on signature/validation failures; otherwise return True.

Security

  • Passwords and OTP codes hashed with Argon2 (src/hashing.py).
  • JWTs include jti, exp, iat; refresh tokens stored hashed in DB and rotated on every refresh/login.
  • Cookies for refresh tokens are httpOnly, samesite="lax"; set secure=True in production.
  • OAuth state cookies defend against CSRF on social callbacks.
  • Rate limiting is enabled globally; webhook route is exempt.
  • CORS is wide open by default; restrict origins/headers/methods for production.

Configuration & Environments

  • Settings loaded by pydantic-settings (src/config.py) from .env.
  • Provide distinct URLs for async API DB, sync Celery DB, and tests.
  • Logging configured via src/logging.py to stdout and logs/app.log with rotation.

Developer Workflow

  1. Install deps with uv sync and create .env (see README).
  2. Run uv run alembic upgrade head to create tables/seeds (plan seed migration exists under alembic/versions).
  3. Start services: uv run uvicorn src.main:app --reload, Celery worker/beat, Postgres, and Redis.
  4. Develop features in service/repository layers; add/extend Pydantic schemas and tests.
  5. Run uv run pytest before merging; mock Stripe/email where needed.

Testing Approach

  • Pytest via uv run pytest with pytest-asyncio, httpx AsyncClient + ASGITransport hitting the FastAPI app directly.
  • DB setup/teardown uses TEST_DATABASE_URL and creates/drops schema per session (tests/conftest.py).
  • Rate limiter disabled in tests; Stripe and email interactions are monkeypatched in billing/auth tests.

Known Gaps / TODOs

  • ProfileReposiotry.get_by_user_id and ProfileService are stubs.
  • src/tasks.py:expire_subscriptions is unimplemented (beat uses billing.tasks.expire_subscriptions_task instead).
  • Placeholder modules for constants/config/exceptions exist under src/auth and src/billing.
  • Enum options PAYMOB and PaymentStatus states are defined but not fully wired.