Opening a shop is an API call; the broker learns of it in the same request
The last step of onboarding that needed a shell: provision site printed a broker password and a person typed it into Mosquitto's passwd file on the host - mounted read-only in the container, so the first attempt failed silently and the password was re-rolled. No tenant could open a second branch without us. The server now drives Mosquitto's dynamic-security plugin over its own broker login: POST /api/sites (owner) writes the row and the sealed password, registers the login and a per-site role with literal topics (the 2.0 plugin does not substitute %u - measured), and removes the row again if the broker refuses, so a shop cannot exist in the database and not on the broker. provision site goes through the same path. The head-office Shops screen gets 'Open a new shop'. broker-init converts the existing passwd file into the plugin's store with every hash intact - PBKDF2-SHA512 both sides - so the cutover re-claims no shop PC. Rehearsed locally: old logins keep working, isolation holds, the health probe works, and a PC claiming a shop opened through the API connects as that shop. run-local.sh now brings the broker up the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
@@ -36,6 +36,15 @@ import (
|
||||
type Store interface {
|
||||
// --- identity ---
|
||||
UserByEmail(ctx context.Context, email string) (UserRecord, error)
|
||||
|
||||
// --- shops ---
|
||||
// CreateSite writes the shop and its sealed broker password in one
|
||||
// transaction and returns the plaintext once, for the broker registration
|
||||
// that must follow. DeleteNewSite is the compensation when that
|
||||
// registration fails: a shop whose PC can enrol but never publish is the
|
||||
// silent failure this whole endpoint exists to end.
|
||||
CreateSite(ctx context.Context, clientID, slug, name, tz string) (NewSite, error)
|
||||
DeleteNewSite(ctx context.Context, clientID, siteID string) error
|
||||
TouchUserLogin(ctx context.Context, userID string) error
|
||||
CreateSession(ctx context.Context, s NewSession) error
|
||||
SessionByAccess(ctx context.Context, hash []byte) (auth.Principal, time.Time, error)
|
||||
@@ -168,6 +177,10 @@ type Server struct {
|
||||
// business questions the screens ask. Nil means this deployment has no
|
||||
// API key, which is supported: the UI hides the panel.
|
||||
Assistant Assistant
|
||||
// Broker registers a shop's login with Mosquitto at the moment the shop is
|
||||
// created. Nil means this deployment cannot create shops through the API
|
||||
// and says so, rather than creating one that can never publish.
|
||||
Broker SiteBroker
|
||||
// Live relays camera frames from a shop PC to whoever is watching, on
|
||||
// demand. Created on first use.
|
||||
Live *LiveHub
|
||||
@@ -264,6 +277,7 @@ func (s *Server) Routes() *http.ServeMux {
|
||||
mux.HandleFunc("GET /api/reports/footfall", s.authed(s.handleFootfall))
|
||||
mux.HandleFunc("GET /api/reports/conversion", s.authed(s.handleConversion))
|
||||
mux.HandleFunc("GET /api/sites", s.authed(s.handleSites))
|
||||
mux.HandleFunc("POST /api/sites", s.authed(s.handleCreateSite))
|
||||
|
||||
// Cameras, onboarded from head office. The shop PC still does the
|
||||
// connecting - it is the only thing on the camera's network - so these
|
||||
|
||||
Reference in New Issue
Block a user