Files
member-console/internal/server/compose.go
T
cgalo5758 0b28a9dc29 Remediate security audit findings
- Replace gorilla/csrf with net/http CrossOriginProtection
- Require valkey-password and add TLS options for session store
- End session at /logout and revoke refresh tokens
- Re-derive identity and roles from provider every five minutes
- Process each Stripe webhook event in its own Temporal workflow
- Give each outbox entry its own workflow with Temporal retries
- Guard against stale Stripe events with provider timestamps
- Derive transport security from base-url scheme
2026-09-09 13:25:43 -05:00

119 lines
4.2 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package server
import (
"fmt"
"html/template"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
)
// Server-side composition (spec page-anatomy "A page arrives complete";
// docs/design-system.md §6). A page region that also exists as an HTMX
// partial, so a mutation can swap it back in, is rendered into the page by
// the server on first paint through an in-process request to the partial's
// own route. The browser never fetches first-paint content after load: one
// response, no spinner, and the partial stays the single source of the
// region's markup.
// Include renders a GET route in-process, with the caller's request context
// (session, request ID) and headers, and returns its HTML fragment. Core
// builds one against its router and hands it to integrations through Deps
// so their card bodies compose the same way.
type Include func(r *http.Request, path string) (template.HTML, error)
// NewInclude returns an Include that dispatches through router. The router
// is the mux the partial routes are registered on; the middleware stack sits
// outside it, so an include never re-authenticates: the session already
// rides the context it is given.
func NewInclude(router http.Handler) Include {
return func(r *http.Request, path string) (template.HTML, error) {
u, err := url.Parse(path)
if err != nil {
return "", fmt.Errorf("include %s: %w", path, err)
}
req := r.Clone(r.Context())
req.Method = http.MethodGet
req.URL = u
req.RequestURI = path
req.Body = http.NoBody
req.ContentLength = 0
req.Header.Set("HX-Request", "true")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
return "", fmt.Errorf("include %s: status %d", path, rec.Code)
}
return template.HTML(rec.Body.String()), nil
}
}
// IncludeOr renders the fragment, or an inline notice naming what could not
// be loaded, so a failing region never fails the page and never renders
// blank. A nil Include (a test set without a router) renders nothing.
func IncludeOr(inc Include, logger *slog.Logger, r *http.Request, path, what string) template.HTML {
if inc == nil {
return ""
}
out, err := inc(r, path)
if err != nil {
if logger != nil {
logger.Error("page region failed to compose", slog.String("path", path), slog.Any("error", err))
}
return template.HTML(`<div class="alert alert-danger" role="alert">Could not load ` + template.HTMLEscapeString(what) + `.</div>`)
}
return out
}
// DashboardCardView is a declared card with its body composed for this
// request. PartialPath stays the swap source for the card's RefreshEvent.
type DashboardCardView struct {
DashboardCard
Body template.HTML
}
// ProductsPageData is the /products page: the shell plus its three regions,
// composed from their partial routes.
type ProductsPageData struct {
Shell Shell
Entitlements template.HTML
Plans template.HTML
Addons template.HTML
}
// BillingPageData is the /billing page: the shell plus the invoices region.
type BillingPageData struct {
Shell Shell
Invoices template.HTML
}
// IndexPageData is the / dashboard page: the member's identity, the
// post-checkout banner state, pending domain claims, the composed
// workspaces region, and the integration dashboard cards. Named (rather
// than the handler's former anonymous struct) so the page-anatomy header
// and section methods in anatomy.go can hang off it.
type IndexPageData struct {
Name string
Username string
Email string
KeycloakAccountURL string
IsOperator bool
HasMultipleWorkspaces bool
CheckoutStatus string
DashboardCards []DashboardCardView
Workspaces template.HTML
PendingDomainClaims []PendingDomainClaim
Shell Shell
// HasEntitlement reports whether the person's organization holds any
// entitlement (a numeric or granted-boolean entitlement, or a rung on
// a plan ladder) — computed alongside DashboardCards in server.go so
// IsEmpty (server.go) can decide the dashboard's one empty state
// (member-dashboard ADDED requirement "The dashboard has one empty
// state"; design D25).
HasEntitlement bool
}