Skip to content

Architecture

BrandonRobare edited this page Jun 2, 2026 · 1 revision

Architecture

ShyneBeauty is one Flask application, organized as the shyne_app/ package. The 8-line shyne.py at the repo root imports the package and exposes shyne_app.app:app for gunicorn or the dev server.

How the package fits together

extensions.py builds the shared objects once: the Flask app, the SQLAlchemy db, the CSRF protector, and the Flask-Login manager. Every other module imports those objects and attaches itself to them. There are no Flask blueprints here; routes register directly on the shared app through decorators, and the modules load in a fixed order so hooks and views are in place before the first request.

  • config.py holds runtime constants, environment parsing, the role and permission tables, redirect-safety helpers, and the password policy.
  • models.py defines the SQLAlchemy ORM models for both databases and the small schema-compatibility helpers that add columns to older auth databases.
  • access.py is the authorization layer: effective-permission lookup, the require_permission decorator, the user-management query helpers, and the access-event logger.
  • auth.py registers the request lifecycle hooks: rate limiting, auth-schema compatibility, revoked-session checks, forced password change, HTTPS redirect under live-prod, and the response security headers. It also holds the error handlers and the navigation context processor.
  • routes.py holds the page handlers: dashboard, orders, customers, inventory, products, product batches, the users console, and the MFA and account-settings flows.
  • admin.py mounts a Flask-Admin console for low-level table browsing, gated to the Dev Admin role.
  • cli.py adds Click commands for database setup, admin creation, demo seeding, and backups.
  • rate_limit.py is a small in-process sliding-window limiter for sensitive POST paths and the admin console.

Request flow

A request first passes through the before_request hooks in auth.py: rate-limit check, auth-schema compatibility, revoked-session check, and forced-password-change redirect. It then reaches a route handler, which calls require_permission(...) before doing work. Handlers read and write through the ORM models, render a Jinja2 template from templates/, and the response leaves through the after_request hook that sets the content security policy and other headers.

flowchart TD
    Browser["Staff browser<br/>(login, dashboard, forms)"]
    subgraph Flask["Flask app (shyne_app)"]
        Ext["extensions.py<br/>app, db, csrf, login_manager"]
        Auth["auth.py<br/>before/after request<br/>session, headers, rate limit"]
        Access["access.py<br/>roles, permissions,<br/>require_permission"]
        Routes["routes.py<br/>dashboard, orders, customers,<br/>inventory, products, batches, users, MFA"]
        Admin["admin.py<br/>Flask-Admin console"]
        Models["models.py<br/>SQLAlchemy ORM models"]
        CLI["cli.py<br/>init-db, create-admin, export-data"]
    end
    Templates["templates/<br/>Jinja2 (base_authenticated, forms)"]
    Static["static/<br/>CSS, fonts, icon"]
    BizDB[("SQLite<br/>business DB")]
    AuthDB[("SQLite<br/>auth DB (bind: auth)")]

    Browser -->|HTTP request| Auth
    Auth --> Access
    Access --> Routes
    Routes --> Admin
    Routes --> Models
    Models --> BizDB
    Models --> AuthDB
    CLI --> Models
    Routes --> Templates
    Templates --> Static
    Templates -->|HTML response| Browser
Loading

Two databases

Business data and staff accounts live in separate SQLite files. The auth database is reached through a SQLAlchemy bind key (auth), so the AdminUser, access-event, and login-throttle tables stay isolated from customer and order data. The runtime picks the file pair from APP_RUNTIME: demo-dev uses the *_demo files, and live-prod uses the *_live files. DATABASE_URL and AUTH_DATABASE_URL override either side. Splitting the two means a business backup or restore never touches credentials, and the demo seed can wipe and rebuild business data without disturbing the auth store layout.

Runtimes

demo-dev is the default. It seeds four deterministic demo accounts and a set of sample business records on every init-db, and it leaves session cookies usable over plain HTTP for local work. live-prod is explicit: it refuses init-db, blocks FLASK_DEBUG, forces secure session cookies, and adds HSTS. The split keeps demo conveniences out of a real deployment.

Key flows

Staff sign-in, with the IP throttle, per-account lockout, and the optional TOTP challenge:

sequenceDiagram
    actor Staff
    participant Browser
    participant Flask as Flask (auth/routes)
    participant AuthDB as Auth SQLite

    Staff->>Browser: Enter email + password
    Browser->>Flask: POST /login (CSRF token)
    Flask->>AuthDB: Look up AdminUser, IP throttle
    AuthDB-->>Flask: User record + throttle state
    alt Locked or throttled
        Flask-->>Browser: Generic "Invalid email or password"
    else Valid credentials
        Flask->>AuthDB: Reset login state, record event
        alt MFA enabled
            Flask-->>Browser: Redirect /mfa/challenge
            Staff->>Browser: Enter TOTP code
            Browser->>Flask: POST /mfa/challenge
            Flask->>AuthDB: Verify code, set last_login_at
            Flask-->>Browser: Start session, redirect to dashboard
        else No MFA
            Flask->>AuthDB: Set last_login_at
            Flask-->>Browser: Start session, redirect to dashboard
        end
    end
Loading

Order creation from the Add Order form:

sequenceDiagram
    actor Operator as Staff Operator
    participant Browser
    participant Flask as Flask (routes/access)
    participant DB as Business SQLite

    Operator->>Browser: Open Add Order form
    Browser->>Flask: GET /add-order
    Flask->>Flask: require_permission(orders.edit)
    Flask->>DB: Load customers + active products
    DB-->>Flask: Records
    Flask-->>Browser: Render addOrder.html
    Operator->>Browser: Pick customer, add line items, submit
    Browser->>Flask: POST /add-order (CSRF token)
    Flask->>Flask: Validate customer, platform, status, line items
    alt Validation fails
        Flask-->>Browser: Re-render form with flash errors
    else Valid
        Flask->>DB: Insert Order + OrderItems
        Flask->>DB: Insert OrderStatusEvent (initial)
        DB-->>Flask: Commit
        Flask-->>Browser: Redirect to orders list (success flash)
    end
Loading

Clone this wiki locally