The family gate (decisions 63/64, R-780): family members with their own logins, a permanent forwardAuth door per family app, anchored exceptions, min_controller
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
- internal/family: the family list (bcrypt, generated 4x4 passwords shown once) + 30-day sessions in family.json (0600, atomic); a reset (generation), a removal or a logout ends sessions at the next request. - internal/stacks/family_gate.go: family_gate / family_gate_except / min_controller in .felhom.yml; the door is written BEFORE the first start (install and a removed app's restore), a life record in app.yaml, reconciled by the gate loop; priority below the install hold, setup gate and sign-up block; every exception anchored ^/prefix(/|$) (finding F1). - internal/web/family_gate.go: forwardAuth /__felhom_gate/family (app cookie felhom_famgate, host-only, names a store session); /__family/start|login|logout on the dashboard host (session cookie felhom_family, Path=/__family); sign-in counted per visitor (clientIP) AND per name, short windows; the household's dashboard session vouches. RequireAuth never reads a family cookie. The "Család" card on the security page: add / new password / remove. Red-proofs RP-F1..RP-F7 (felhom.eu audits/family-gate-2026-10-02/A/). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -184,6 +184,8 @@ type AppConfig struct {
|
||||
// SetupGate (v0.280.0, decision 46) is the app's setup gate: closed from a fresh install until the first
|
||||
// setup is done. A life record (carried across a restore). See setup_gate.go.
|
||||
SetupGate *SetupGateRecord `yaml:"setup_gate,omitempty" json:"setup_gate,omitempty"`
|
||||
// FamilyGate (v0.287.0, decisions 63/64): the app's permanent family gate, on since its install. A life record.
|
||||
FamilyGate *FamilyGateRecord `yaml:"family_gate,omitempty" json:"family_gate,omitempty"`
|
||||
// InstallHold (R-741, decision 45): an after_install app is held (the gate's door) from its fresh install until
|
||||
// its known first login is replaced. Same record shape as SetupGate. See install_hold.go.
|
||||
InstallHold *SetupGateRecord `yaml:"install_hold,omitempty" json:"install_hold,omitempty"`
|
||||
@@ -428,6 +430,12 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.287.0: a template that needs a newer controller is refused before anything is written.
|
||||
if err := checkMinController(&meta); err != nil {
|
||||
clearDeploying()
|
||||
m.logger.Printf("[ERROR] [stacks] Deploy %s REFUSED: %v", req.StackName, err)
|
||||
return "", util.MsgError("err.stacks.needs_newer_controller", err.Error())
|
||||
}
|
||||
// `09` §3 decision 46: a gated template is installed CLOSED, and the gate's traefik file is written BEFORE
|
||||
// the first start (spike F2). Cannot write it → the install is refused: never published open.
|
||||
var gate *SetupGateRecord
|
||||
@@ -455,6 +463,24 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
hold = h
|
||||
}
|
||||
|
||||
// v0.287.0 (decisions 63/64): a family app is never published open — its door is written before the first start.
|
||||
var family *FamilyGateRecord
|
||||
if meta.FamilyGate {
|
||||
f, err := m.prepareFamilyGate(req.StackName, stack.ComposePath, env, &meta)
|
||||
if err != nil {
|
||||
clearDeploying()
|
||||
if gate != nil {
|
||||
_ = m.removeSetupGateFile(req.StackName)
|
||||
}
|
||||
if hold != nil {
|
||||
_ = os.Remove(m.installHoldPath(req.StackName))
|
||||
}
|
||||
m.logger.Printf("[ERROR] [stacks] Deploy %s REFUSED: the family gate could not be prepared: %v", req.StackName, err)
|
||||
return "", util.MsgError("err.stacks.family_gate_failed", err.Error())
|
||||
}
|
||||
family = f
|
||||
}
|
||||
|
||||
// Save app.yaml.
|
||||
// CTRL-T2-1: persist the env now, but mark the ON-DISK state Deployed:false
|
||||
// until `docker compose up -d` actually succeeds (done in runComposeDeploy).
|
||||
@@ -476,6 +502,7 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
DesiredState: DesiredStateRunning,
|
||||
SetupGate: gate,
|
||||
InstallHold: hold,
|
||||
FamilyGate: family,
|
||||
}
|
||||
|
||||
diskCfg := *appCfg
|
||||
@@ -772,6 +799,14 @@ func (m *Manager) PersistUnitRedeployConfig(name string, env map[string]string)
|
||||
// its setup: the lock record and its block, written HERE — before anything starts. An app that was never removed
|
||||
// keeps exactly the record it had (carryLifeRecords): a restore never adds a lock to an installed app that the
|
||||
// household has not closed (decision 49). Pinned by TestR773_*.
|
||||
// v0.287.0: a removed family app restored from its backup gets its door before anything starts (the R-773 lesson).
|
||||
if priorRaw == nil && cfg.FamilyGate == nil && meta.FamilyGate {
|
||||
rec, err := m.prepareFamilyGate(name, stack.ComposePath, env, &meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the family gate could not be prepared (the app was not started): %w", err)
|
||||
}
|
||||
cfg.FamilyGate = rec
|
||||
}
|
||||
if priorRaw == nil && cfg.SetupGate == nil {
|
||||
rec, err := m.restoreSignupLock(name, stack.ComposePath, env, &meta)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user