v0.165.0: Indítópult megosztása — guest launcher via capability URL (+ optional password, QR)

Mint a 160-bit capability URL (/s/<token>) serving a standalone read-only guest
launcher: same tiles, opens apps in new tabs, no account, no admin session.
Information only, zero control — every privilege stays behind each app's own auth.

- /s/ pre-auth pass-through (after the claim gate) + session-CSRF exemption; guest
  password POST carries its own pre-auth HMAC CSRF.
- Constant-time token match; empty stored token = disabled = byte-identical mux 404.
- Optional per-share password: separate bcrypt hash + own attempt map; signed cookie
  = HMAC(token|passwordHash) keyed with web.session_secret, so rotate/change invalidates.
- Guest labels ride the v0.164.0 ruling; never expose internal state vocabulary.
- Token redacted in logs (/s/<redacted>); never in CHANGELOG/REPORT/CONTEXT.
- Admin modal: copy-link, QR (go-qrcode), set/clear password, rotate, disable.
- Tests: Groups A-G (14) + 3 red-proofs verified red.
This commit is contained in:
2026-07-24 12:08:43 +02:00
parent 8e5edb2865
commit 15206314ab
19 changed files with 1341 additions and 115 deletions
+26 -5
View File
@@ -272,18 +272,39 @@ func buildLauncherApps(stackList []stacks.Stack, subdomains map[string]string) [
return apps
}
// launcherHandler renders the Indítópult: a grid of large tappable tiles, one per openable deployed
// app (subdomain presence is the single openability criterion — see buildLauncherApps). Behind
// RequireAuth like every page; the "/" landing page stays the Vezérlőpult.
func (s *Server) launcherHandler(w http.ResponseWriter, r *http.Request) {
// launcherApps assembles the sorted launcher tile list (deployed/protected stacks with a subdomain,
// controller excluded). Extracted from launcherHandler so the guest share page (v0.165.0) renders
// the EXACT same app slice as the admin launcher.
func (s *Server) launcherApps() []LauncherApp {
var eligible []stacks.Stack
for _, st := range s.stackMgr.GetStacks() {
if st.Deployed || st.Protected {
eligible = append(eligible, st)
}
}
return buildLauncherApps(eligible, s.subdomainMap(eligible))
}
// launcherHandler renders the Indítópult: a grid of large tappable tiles, one per openable deployed
// app (subdomain presence is the single openability criterion — see buildLauncherApps). Behind
// RequireAuth like every page; the "/" landing page stays the Vezérlőpult. It also carries the
// "Indítópult megosztása" share state (v0.165.0) for the modal.
func (s *Server) launcherHandler(w http.ResponseWriter, r *http.Request) {
data := s.baseData("launcher", "Indítópult")
data["Apps"] = buildLauncherApps(eligible, s.subdomainMap(eligible))
data["Apps"] = s.launcherApps()
// Share modal state. The share URL is built from the request Host at render time (the canonical
// controller subdomain is not persisted anywhere reachable here); it carries the live token, which
// is fine to show the authed admin inside the modal — the ONE admin surface allowed to reveal it.
token := s.settings.GetLauncherShareToken()
data["ShareEnabled"] = token != ""
if token != "" {
data["ShareURL"] = "https://" + r.Host + "/s/" + token
}
data["SharePasswordSet"] = s.settings.GetLauncherSharePasswordHash() != ""
if f := strings.TrimSpace(r.URL.Query().Get("flash")); f != "" {
data["ShareFlash"] = f
}
s.executeTemplate(w, r, "launcher", data)
}