From e5e59fdcbf82e99ef1776829c47c59f0e313884a Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sun, 4 Jan 2026 23:12:14 -0600 Subject: [PATCH] add tos and privacy policy --- cmd/appview/serve.go | 29 +-- pkg/appview/handlers/legal.go | 44 ++++ pkg/appview/routes/routes.go | 15 ++ pkg/appview/static/css/style.css | 131 +++++++++++ pkg/appview/templates/pages/privacy.html | 266 +++++++++++++++++++++++ pkg/appview/templates/pages/terms.html | 204 +++++++++++++++++ 6 files changed, 670 insertions(+), 19 deletions(-) create mode 100644 pkg/appview/handlers/legal.go create mode 100644 pkg/appview/templates/pages/privacy.html create mode 100644 pkg/appview/templates/pages/terms.html diff --git a/cmd/appview/serve.go b/cmd/appview/serve.go index 15b0006..68fa0c8 100644 --- a/cmd/appview/serve.go +++ b/cmd/appview/serve.go @@ -380,7 +380,16 @@ func serveRegistry(cmd *cobra.Command, args []string) error { // OAuth client metadata endpoint mainRouter.Get("/oauth-client-metadata.json", func(w http.ResponseWriter, r *http.Request) { config := oauthClientApp.Config + logoURI := cfg.Server.BaseURL + "/web-app-manifest-192x192.png" + policyURI := cfg.Server.BaseURL + "/privacy" + tosURI := cfg.Server.BaseURL + "/terms" + metadata := config.ClientMetadata() + metadata.ClientName = &cfg.Server.ClientName + metadata.ClientURI = &cfg.Server.BaseURL + metadata.LogoURI = &logoURI + metadata.PolicyURI = &policyURI + metadata.TosURI = &tosURI // For confidential clients, ensure JWKS is included // The indigo library should populate this automatically, but we explicitly set it here @@ -390,30 +399,12 @@ func serveRegistry(cmd *cobra.Command, args []string) error { metadata.JWKS = &jwks } - // Convert indigo's metadata to map so we can add custom fields - metadataBytes, err := json.Marshal(metadata) - if err != nil { - http.Error(w, "Failed to marshal metadata", http.StatusInternalServerError) - return - } - - var metadataMap map[string]any - if err := json.Unmarshal(metadataBytes, &metadataMap); err != nil { - http.Error(w, "Failed to unmarshal metadata", http.StatusInternalServerError) - return - } - - // Add custom fields - metadataMap["client_name"] = cfg.Server.ClientName - metadataMap["client_uri"] = cfg.Server.BaseURL - metadataMap["logo_uri"] = cfg.Server.BaseURL + "/web-app-manifest-192x192.png" - w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // Limit caching to allow scope changes to propagate quickly // PDS servers cache client metadata, so short max-age helps with updates w.Header().Set("Cache-Control", "public, max-age=300") - if err := json.NewEncoder(w).Encode(metadataMap); err != nil { + if err := json.NewEncoder(w).Encode(metadata); err != nil { http.Error(w, "Failed to encode metadata", http.StatusInternalServerError) } }) diff --git a/pkg/appview/handlers/legal.go b/pkg/appview/handlers/legal.go new file mode 100644 index 0000000..51722c0 --- /dev/null +++ b/pkg/appview/handlers/legal.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +// PrivacyPolicyHandler handles the /privacy page +type PrivacyPolicyHandler struct { + Templates *template.Template + RegistryURL string +} + +func (h *PrivacyPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + data := struct { + PageData + }{ + PageData: NewPageData(r, h.RegistryURL), + } + + if err := h.Templates.ExecuteTemplate(w, "privacy", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// TermsOfServiceHandler handles the /terms page +type TermsOfServiceHandler struct { + Templates *template.Template + RegistryURL string +} + +func (h *TermsOfServiceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + data := struct { + PageData + }{ + PageData: NewPageData(r, h.RegistryURL), + } + + if err := h.Templates.ExecuteTemplate(w, "terms", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/pkg/appview/routes/routes.go b/pkg/appview/routes/routes.go index 89a59cf..d56efec 100644 --- a/pkg/appview/routes/routes.go +++ b/pkg/appview/routes/routes.go @@ -87,6 +87,21 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) { }, ).ServeHTTP) + // Legal pages (public) + router.Get("/privacy", middleware.OptionalAuth(deps.SessionStore, deps.Database)( + &uihandlers.PrivacyPolicyHandler{ + Templates: deps.Templates, + RegistryURL: registryURL, + }, + ).ServeHTTP) + + router.Get("/terms", middleware.OptionalAuth(deps.SessionStore, deps.Database)( + &uihandlers.TermsOfServiceHandler{ + Templates: deps.Templates, + RegistryURL: registryURL, + }, + ).ServeHTTP) + // API route for repository stats (public, read-only) router.Get("/api/stats/{handle}/{repository}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( &uihandlers.GetStatsHandler{ diff --git a/pkg/appview/static/css/style.css b/pkg/appview/static/css/style.css index 9bf79c7..fcfff11 100644 --- a/pkg/appview/static/css/style.css +++ b/pkg/appview/static/css/style.css @@ -2477,3 +2477,134 @@ a.license-badge:hover { background-color: rgba(13, 108, 191, 0.15); color: #0d6cbf; } + +/* Legal Pages (Privacy Policy, Terms of Service) */ +.legal-page { + max-width: 800px; + margin: 0 auto; + padding: 2rem 1rem; +} + +.legal-page h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: var(--fg); +} + +.legal-updated { + color: var(--secondary); + margin-bottom: 2rem; +} + +.legal-section { + margin: 2rem 0; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border); +} + +.legal-section:last-child { + border-bottom: none; +} + +.legal-section h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: var(--fg); +} + +.legal-section h3 { + font-size: 1.15rem; + margin: 1.5rem 0 0.75rem; + color: var(--fg); +} + +.legal-section p { + margin-bottom: 1rem; + line-height: 1.7; +} + +.legal-section ul, +.legal-section ol { + margin-bottom: 1rem; + padding-left: 2rem; +} + +.legal-section li { + margin-bottom: 0.5rem; + line-height: 1.6; +} + +.legal-section ul ul { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.legal-section code { + background: var(--code-bg); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: "Monaco", "Menlo", monospace; + font-size: 0.9em; +} + +.legal-section a { + color: var(--primary); + text-decoration: underline; +} + +.legal-section a:hover { + color: var(--primary-dark); +} + +.legal-section table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; +} + +.legal-section table th, +.legal-section table td { + padding: 0.75rem 1rem; + border: 1px solid var(--border); + text-align: left; +} + +.legal-section table th { + background: var(--code-bg); + font-weight: 600; +} + +.legal-section table tr:nth-child(even) { + background: var(--hover-bg); +} + +.legal-disclaimer { + background: var(--code-bg); + padding: 1rem; + border-radius: 4px; + font-size: 0.95rem; + margin: 1rem 0; +} + +@media (max-width: 768px) { + .legal-page { + padding: 1rem 0.5rem; + } + + .legal-page h1 { + font-size: 1.5rem; + } + + .legal-section h2 { + font-size: 1.25rem; + } + + .legal-section table { + font-size: 0.85rem; + } + + .legal-section table th, + .legal-section table td { + padding: 0.5rem; + } +} diff --git a/pkg/appview/templates/pages/privacy.html b/pkg/appview/templates/pages/privacy.html new file mode 100644 index 0000000..12dfcd9 --- /dev/null +++ b/pkg/appview/templates/pages/privacy.html @@ -0,0 +1,266 @@ +{{ define "privacy" }} + + + + Privacy Policy - ATCR + {{ template "head" . }} + + + {{ template "nav" . }} + +
+ +
+ + + + +{{ end }} diff --git a/pkg/appview/templates/pages/terms.html b/pkg/appview/templates/pages/terms.html new file mode 100644 index 0000000..4af0771 --- /dev/null +++ b/pkg/appview/templates/pages/terms.html @@ -0,0 +1,204 @@ +{{ define "terms" }} + + + + Terms of Service - ATCR + {{ template "head" . }} + + + {{ template "nav" . }} + +
+ +
+ + + + +{{ end }}