diff --git a/apps/trust/index.html b/apps/trust/index.html
index 5ca5c8958..dc311f698 100644
--- a/apps/trust/index.html
+++ b/apps/trust/index.html
@@ -38,7 +38,12 @@
-
Compliance Page
+ {{.Title}}
+
+
+
+
+
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 3aa156e27..c0aa8bbdd 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -37,6 +37,7 @@ import (
console_web "go.probo.inc/probo/pkg/server/web"
"go.probo.inc/probo/pkg/slack"
"go.probo.inc/probo/pkg/trust"
+ "go.gearno.de/x/ref"
)
type Config struct {
@@ -97,7 +98,7 @@ func NewServer(cfg Config) (*Server, error) {
return nil, err
}
- trustWebServer, err := trust_web.NewServer()
+ trustWebServer, err := trust_web.NewServer(compliancePageHeadData(cfg.Trust))
if err != nil {
return nil, err
}
@@ -195,3 +196,25 @@ func (s *Server) TrustCenterHandler() http.Handler {
return r
}
+
+func compliancePageHeadData(trustService *trust.Service) trust_web.HeadDataFunc {
+ return func(r *http.Request) trust_web.HeadData {
+ tc := compliancepage.CompliancePageFromContext(r.Context())
+ if tc == nil {
+ return trust_web.HeadData{Title: "Compliance Page"}
+ }
+
+ org, err := trustService.GetOrganizationByTrustCenterID(r.Context(), tc.ID)
+ if err != nil || org == nil {
+ return trust_web.HeadData{Title: "Compliance Page"}
+ }
+
+ baseURL := compliancepage.CompliancePageBaseURLFromContext(r.Context())
+
+ return trust_web.HeadData{
+ Title: org.Name + " — Compliance",
+ Description: org.Name + " Compliance Page",
+ OGURL: ref.UnrefOrZero(baseURL),
+ }
+ }
+}
diff --git a/pkg/server/statichandler/statichandler.go b/pkg/server/statichandler/statichandler.go
index b7e1bbd13..830cfa895 100644
--- a/pkg/server/statichandler/statichandler.go
+++ b/pkg/server/statichandler/statichandler.go
@@ -27,20 +27,39 @@ import (
"strings"
)
-type GzipOptions struct {
- EnableFileTypeCheck bool
- FileTypes []string
+type (
+ // FileRenderer renders dynamic content for a given file path. When
+ // registered via WithFileRenderer, the server calls it instead of serving
+ // the static embedded bytes. The renderer writes the response body to w.
+ FileRenderer func(w io.Writer, r *http.Request) error
+
+ Option func(*Server)
+
+ GzipOptions struct {
+ EnableFileTypeCheck bool
+ FileTypes []string
+ }
+
+ Server struct {
+ spaFS http.FileSystem
+ etags map[string]string
+ indexETag string
+ indexContent []byte
+ gzipOptions GzipOptions
+ fileRenderers map[string]FileRenderer
+ }
+)
+
+// WithFileRenderer registers a dynamic renderer for the given path (e.g.
+// "/index.html"). When the server would serve that file, it calls the
+// renderer instead. ETag-based caching is disabled for rendered files.
+func WithFileRenderer(path string, renderer FileRenderer) Option {
+ return func(s *Server) {
+ s.fileRenderers[path] = renderer
+ }
}
-type Server struct {
- spaFS http.FileSystem
- etags map[string]string
- indexETag string
- indexContent []byte
- gzipOptions GzipOptions
-}
-
-func NewServer(staticFiles fs.FS, distPath string, gzipOptions GzipOptions) (*Server, error) {
+func NewServer(staticFiles fs.FS, distPath string, gzipOptions GzipOptions, opts ...Option) (*Server, error) {
subFS, err := fs.Sub(staticFiles, distPath)
if err != nil {
return nil, err
@@ -104,13 +123,41 @@ func NewServer(staticFiles fs.FS, distPath string, gzipOptions GzipOptions) (*Se
return nil, err
}
- return &Server{
- spaFS: http.FS(subFS),
- indexETag: indexETag,
- indexContent: indexContent,
- etags: etags,
- gzipOptions: gzipOptions,
- }, nil
+ s := &Server{
+ spaFS: http.FS(subFS),
+ indexETag: indexETag,
+ indexContent: indexContent,
+ etags: etags,
+ gzipOptions: gzipOptions,
+ fileRenderers: make(map[string]FileRenderer),
+ }
+
+ for _, opt := range opts {
+ opt(s)
+ }
+
+ return s, nil
+}
+
+func (s *Server) serveIndex(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+
+ if renderer, ok := s.fileRenderers["/index.html"]; ok {
+ w.WriteHeader(http.StatusOK)
+ _ = renderer(w, r)
+ return
+ }
+
+ w.Header().Set("ETag", `"`+s.indexETag+`"`)
+
+ if r.Header.Get("If-None-Match") == `"`+s.indexETag+`"` {
+ w.WriteHeader(http.StatusNotModified)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(s.indexContent)
}
func (s *Server) ServeSPA(w http.ResponseWriter, r *http.Request) {
@@ -118,18 +165,7 @@ func (s *Server) ServeSPA(w http.ResponseWriter, r *http.Request) {
f, err := s.spaFS.Open(path)
if err != nil {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.Header().Set("ETag", `"`+s.indexETag+`"`)
-
- if r.Header.Get("If-None-Match") == `"`+s.indexETag+`"` {
- w.WriteHeader(http.StatusNotModified)
- return
- }
-
- w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
-
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write(s.indexContent)
+ s.serveIndex(w, r)
return
}
@@ -142,20 +178,15 @@ func (s *Server) ServeSPA(w http.ResponseWriter, r *http.Request) {
}
if info.IsDir() {
+ s.serveIndex(w, r)
+ return
+ }
+
+ if renderer, ok := s.fileRenderers[path]; ok {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.Header().Set("ETag", `"`+s.indexETag+`"`)
-
- if r.Header.Get("If-None-Match") == `"`+s.indexETag+`"` {
- w.WriteHeader(http.StatusNotModified)
- return
- }
-
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
- w.Header().Set("Pragma", "no-cache")
- w.Header().Set("Expires", "0")
-
w.WriteHeader(http.StatusOK)
- _, _ = w.Write(s.indexContent)
+ _ = renderer(w, r)
return
}
diff --git a/pkg/server/trust/trust.go b/pkg/server/trust/trust.go
index ef6a4714a..3ef85a239 100644
--- a/pkg/server/trust/trust.go
+++ b/pkg/server/trust/trust.go
@@ -16,29 +16,72 @@
package trust
import (
+ "fmt"
+ "html/template"
+ "io"
+ "io/fs"
"net/http"
truststatics "go.probo.inc/probo/apps/trust"
"go.probo.inc/probo/pkg/server/statichandler"
)
-type Server struct {
- *statichandler.Server
-}
+type (
+ HeadData struct {
+ Title string
+ Description string
+ OGURL string
+ }
+
+ HeadDataFunc func(r *http.Request) HeadData
+
+ Server struct {
+ *statichandler.Server
+ }
+)
+
+func NewServer(headDataFunc HeadDataFunc) (*Server, error) {
+ renderer, err := buildIndexRenderer(headDataFunc)
+ if err != nil {
+ return nil, err
+ }
-func NewServer() (*Server, error) {
gzipOptions := statichandler.GzipOptions{
EnableFileTypeCheck: true,
FileTypes: []string{".js", ".css", ".html"},
}
- spaServer, err := statichandler.NewServer(truststatics.StaticFiles, "dist", gzipOptions)
+ spaServer, err := statichandler.NewServer(
+ truststatics.StaticFiles,
+ "dist",
+ gzipOptions,
+ statichandler.WithFileRenderer("/index.html", renderer),
+ )
if err != nil {
return nil, err
}
- return &Server{
- Server: spaServer,
+ return &Server{Server: spaServer}, nil
+}
+
+func buildIndexRenderer(headDataFunc HeadDataFunc) (statichandler.FileRenderer, error) {
+ subFS, err := fs.Sub(truststatics.StaticFiles, "dist")
+ if err != nil {
+ return nil, fmt.Errorf("cannot open dist: %w", err)
+ }
+
+ indexBytes, err := fs.ReadFile(subFS, "index.html")
+ if err != nil {
+ return nil, fmt.Errorf("cannot read index.html: %w", err)
+ }
+
+ tmpl, err := template.New("index").Parse(string(indexBytes))
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse index.html template: %w", err)
+ }
+
+ return func(w io.Writer, r *http.Request) error {
+ return tmpl.Execute(w, headDataFunc(r))
}, nil
}
diff --git a/pkg/trust/service.go b/pkg/trust/service.go
index 496ac0217..13b39e7be 100644
--- a/pkg/trust/service.go
+++ b/pkg/trust/service.go
@@ -273,6 +273,30 @@ func (s *Service) EmailPresenterConfigByOrganizationID(ctx context.Context, orgI
return s.WithTenant(orgID.TenantID()).TrustCenters.EmailPresenterConfig(ctx, trustCenter.ID)
}
+func (s *Service) GetOrganizationByTrustCenterID(
+ ctx context.Context,
+ trustCenterID gid.GID,
+) (*coredata.Organization, error) {
+ trustCenter, err := s.Get(ctx, trustCenterID)
+ if err != nil {
+ return nil, fmt.Errorf("cannot load trust center: %w", err)
+ }
+
+ org := &coredata.Organization{}
+
+ err = s.pg.WithConn(
+ ctx,
+ func(conn pg.Conn) error {
+ return org.LoadByID(ctx, conn, coredata.NewNoScope(), trustCenter.OrganizationID)
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("cannot load organization: %w", err)
+ }
+
+ return org, nil
+}
+
func (s *Service) GetMembershipByCompliancePageIDAndIdentityID(ctx context.Context, compliancePageID gid.GID, identityID gid.GID) (*coredata.TrustCenterAccess, error) {
membership := &coredata.TrustCenterAccess{}