Skip to content

Phase 1 - Frontend to production-served build and implement auth  #72

@bbartling

Description

@bbartling

Summary

Before starting Phase 1 auth work, we should stop relying on frontend hot reload in the stack and move the UI to a production-style build/serve flow. This ensures E2E and manual testing exercise the same frontend bundle that will be deployed, instead of the current npm run dev HMR setup.

At the same time, we should define and begin implementing the next auth direction for Open-FDD: expiring bearer tokens, a login flow, refresh support, and a single bootstrap-managed app user with a hashed password stored in a file.


Frontend hot reload

Current state

The stack’s frontend container currently runs:

npm run dev

That is convenient during active UI work, but once the UI is stable enough, it becomes a mismatch from deployed behavior because HMR/dev server behavior is not the same as a production build.

Goal

Turn off frontend hot reload once the UI is solid enough and before Phase 1 auth begins.

Run the frontend as a production build so:

  • manual testing hits the real built bundle
  • E2E testing hits the same assets we plan to deploy
  • there is no HMR masking runtime issues
  • behavior matches production more closely

Expected direction

Switch from dev server mode to one of these:

  • build the frontend and serve frontend/dist via Caddy
  • or build the frontend and serve static assets from the API
  • or otherwise use a static file server, but no HMR/dev mode in the deployed stack

Security / auth – next steps

Current state

Auth is currently based on a single shared bearer token from stack/.env:

  • OFDD_API_KEY

  • bootstrap generates a key if missing unless --no-auth

  • frontend uses the same key, such as VITE_OFDD_API_KEY

  • frontend sends:

    • Authorization: Bearer <key> for HTTP
    • ?token= for WebSocket

Limitations of current model:

  • single shared secret
  • no expiry
  • no refresh flow
  • no user identity
  • frontend and backend share one long-lived credential

Planned direction

1. Expiring bearer tokens

Move to short-lived access tokens with expiry.

Preferred pattern:

  • short-lived access token, for example 15–60 minutes
  • longer-lived refresh token or session-based refresh flow

Behavior:

  • frontend uses access token for API and WebSocket
  • when access token expires, frontend calls refresh endpoint
  • on refresh failure, user is redirected to login

Implementation notes:

  • JWT access tokens are a natural fit using exp
  • refresh token may be opaque and stored server-side, or JWT with longer expiry
  • token validation must apply to both API and WebSocket access
  • revoked or expired tokens must be rejected

2. Add FastAPI auth endpoints

Add auth endpoints for login and refresh.

Planned routes:

  • POST /auth/login
  • POST /auth/refresh

Example login response:

{
  "access_token": "...",
  "refresh_token": "...",
  "expires_in": 3600
}

Example refresh response:

{
  "access_token": "...",
  "expires_in": 3600
}

Notes:

  • refresh token rotation is preferred if practical
  • backend should preserve current bearer validation style, but validate access tokens instead of a static shared key
  • WebSocket auth should use the current valid access token

3. Add React login flow

Add a login screen and app-level token handling.

Requirements:

  • login page at /login or equivalent
  • successful login stores auth state and redirects into app
  • API calls use current access token
  • WebSocket uses current access token
  • refresh logic happens in one central place
  • if refresh fails, clear auth state and redirect to login

Recommended direction:

  • central API/auth layer for token injection and refresh retry
  • one retry on 401 before forcing logout

4. Bootstrap-managed single app user

Bootstrap should support creating one application user with username/password.

Proposed bootstrap args:

--user openfdd --password <pwd>

This single user is the initial UI login account.

Notes:

  • for now, keep this as a one-user system
  • changing credentials can initially be done by rerunning bootstrap
  • a dedicated change-password flow can come later

5. Store hashed password in a file

Do not store plaintext passwords.

Use a proper password hashing scheme:

  • Argon2 preferred
  • bcrypt acceptable

Proposed storage model:

  • file-based only for now
  • no database required for single-user auth
  • similar in spirit to Node-RED credentials storage

Possible locations:

  • stack/auth.env
  • stack/.env
  • or a dedicated auth config file

Example values:

OFDD_APP_USER=openfdd
OFDD_APP_USER_HASH=<argon2 hash>

Backend behavior:

  • read configured username and password hash at startup
  • keep single user credentials in memory for login verification
  • on login, verify with Argon2 or bcrypt
  • return 401 on failure without leaking whether username exists

6. Decide what happens to OFDD_API_KEY

We need to decide whether to keep a machine/API credential alongside UI login.

Options:

  1. Keep OFDD_API_KEY for machine-to-machine access

    • useful for Home Assistant, scripts, and non-browser clients
    • React UI uses username/password login flow
  2. Remove OFDD_API_KEY

    • everything uses the new login/token flow
    • simpler long term, but may be less convenient for non-browser integrations

This should be explicitly decided during implementation.


Suggested implementation order

  1. Disable frontend dev/HMR in the stack and serve a production build
  2. Add bootstrap support for --user and --password
  3. Hash password with Argon2 or bcrypt and store it in a file
  4. Add POST /auth/login
  5. Add expiring access tokens
  6. Add POST /auth/refresh
  7. Add React login page and token management
  8. Update API and WebSocket auth to use access tokens
  9. Decide whether OFDD_API_KEY remains for machine access

Acceptance criteria

Frontend

  • frontend is no longer run via npm run dev in the deployed stack
  • stack serves a built production frontend bundle
  • E2E and manual testing target the production-served bundle
  • no HMR is present in stack-based testing workflow

Backend auth

  • bootstrap can create a single app user with username/password
  • plaintext password is never stored
  • password hash is stored in a file and loaded by backend at startup
  • POST /auth/login issues an access token
  • access tokens expire
  • POST /auth/refresh can issue a new access token
  • API rejects expired or invalid tokens
  • WebSocket rejects expired or invalid tokens

Frontend auth

  • React app has a login screen
  • frontend stores and uses current access token
  • frontend can refresh expired access token automatically
  • failed refresh redirects user back to login

Security baseline

  • password verification uses Argon2 or bcrypt
  • access tokens are short-lived
  • auth flow is compatible with HTTPS-first deployment
  • refresh token handling is defined clearly before merge

Open questions

  • Should OFDD_API_KEY remain for machine/non-browser clients?
  • Should refresh tokens be stored in an httpOnly cookie or returned in JSON?
  • Should refresh tokens be rotated on every refresh?
  • Should the backend use JWTs only, or opaque refresh tokens plus JWT access tokens?
  • Should static frontend files be served by Caddy or by FastAPI?

Notes

This issue is intended as the bridge from the current shared-secret bootstrap auth model into a more production-ready Phase 1 auth foundation, while also aligning frontend testing with real deployment behavior.

If you want, I can also turn this into a shorter GitHub issue version or split it into 2–3 separate issues like:

  1. frontend production build serving,
  2. backend auth/token system,
  3. React login flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions