Skip to content

Security model ​

The dashboard and its read APIs (state, history, settings, SSE) have no authentication, and the /auth and /token OAuth endpoints are stubs that accept any credentials. Setting a 4-digit passcode under Settings → Security raises the floor: the routes that change configuration (PUT /api/settings, POST /api/settings/reset, and the passcode routes) require a session token; the diagnostic and pairing routes (/api/test/*, /api/discover/gateway, /api/homekit/pairing) require it too; and an unauthenticated read of /api/settings blanks the values worth hiding (the gateway host, the HomeKit pairing PIN, and each alert's webhook URL) until a valid token unlocks them. Separately, /fulfillment checks the Google bearer with a timing-safe compare whenever GOOGLE_AUTH_TOKEN is set, so Google's calls carry the token the stub /token handed them.

How the passcode is stored ​

The passcode is stored as a salted scrypt hash in the security section of settings.json; the plaintext is never written, and the API returns only passcodeSet: true|false, never the hash. POST /api/unlock checks a submitted passcode and, on a match, hands back an in-memory session token (12-hour TTL) that the browser keeps in sessionStorage and sends as Authorization: Bearer … on every mutating call. Five wrong tries trips a one-minute lockout. Tokens live in server memory only, so a restart logs everyone out while the hash persists. When no passcode is set the gate is open, which is how you set the first one.

What it is and isn't ​

This is a deterrent against casual changes on a shared LAN, not real auth: reads stay open and it rides plain HTTP with no user accounts behind it. The recovery path if you forget the passcode is the server file: delete settings.json (or remove its security block) and restart, or reset configuration from another unlocked session. Keep the raw service off the public internet; to reach it remotely, front the tunnel with Cloudflare Access (Remote access with Cloudflare Access), which authenticates at the edge before any request reaches the origin and so supplies the login the app itself lacks. The Google fulfillment path is the deliberate exception that stays public, since Google's servers can't sign in, so treat GOOGLE_AUTH_TOKEN as a shared secret rather than real auth.

The passcode UI ​

On the client, the 4-digit entry is one component (PinPad.vue) with two input modes chosen by (hover: hover): touch devices get a tappable keypad that auto-submits on the fourth digit, hover-capable (desktop) devices get a styled keyboard text field instead of the dot grid. The Security settings section sets or changes the passcode with the same component in a choose-then-confirm sequence. In landscape the keypad reflows to two columns (header beside the grid) so it never needs scrolling: a phone, short on height, gets compact keys, while a tablet gets the same two columns scaled up to fill the screen with full-size keys. That split keys on the landscape viewport height rather than its width, because iOS Safari can report a width small enough under display zoom to mistake a tablet for a phone. PasscodeGate.vue is a fullscreen takeover rather than a card sitting under the settings header: it drops the usual settings chrome and offers a labelled Cancel that backs out to wherever you came from (dashboard or trends) without unlocking. Cancel sits where the eye already is, so it follows the layout: under the keypad in portrait, under the text field on desktop, and under the left-column text in the landscape two-column form (one button per position, toggled by the same media query); on desktop the Escape key does the same thing. Because the gate animates away moments after it succeeds, it works to hold its layout still: on success Cancel mutes in place rather than unmounting, the landscape form pins its two columns to equal halves so the keypad never slides when the status text changes, and in that form the status splits onto two lines ("Incorrect passcode." over "3 tries left.") instead of reflowing the column width. It verifies the code, plays a brief lock-springs-open success animation, holds for a beat, then commits the returned token. A wrong try and the lockout countdown replace the "Enter your passcode to access." prompt in place (red for a bad attempt, amber while locked out) and clear on the next keypress, so a failed attempt recolours one line instead of appending a second one and shifting the layout; on success that same line turns green and reads "Access granted." through the celebration. That verify-then-commit split is deliberate: committing the token flips the gate off and reveals Settings, so the animation runs first and the commit lands at the end, otherwise the celebration would be cut short. prefers-reduced-motion collapses it to a quick fade.

MIT licensed. Not affiliated with or endorsed by Sigenergy, Apple, Google, or Cloudflare.