Repository navigation
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.
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.pyholds runtime constants, environment parsing, the role and permission tables, redirect-safety helpers, and the password policy. -
models.pydefines the SQLAlchemy ORM models for both databases and the small schema-compatibility helpers that add columns to older auth databases. -
access.pyis the authorization layer: effective-permission lookup, therequire_permissiondecorator, the user-management query helpers, and the access-event logger. -
auth.pyregisters 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.pyholds the page handlers: dashboard, orders, customers, inventory, products, product batches, the users console, and the MFA and account-settings flows. -
admin.pymounts a Flask-Admin console for low-level table browsing, gated to the Dev Admin role. -
cli.pyadds Click commands for database setup, admin creation, demo seeding, and backups. -
rate_limit.pyis a small in-process sliding-window limiter for sensitive POST paths and the admin console.
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
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.
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.
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
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