Add platform name note to password gate login page
Show where password protection is coming from by displaying the platform name derived from CARLOS_PLATFORM_DOMAIN environment variable. Known mappings: - carloku.com -> Carloku - tito.io -> Tito Platform - oncarlos.com -> CARLOS For unknown domains, capitalizes the second-level domain name.
3 files changed,
+135
−5
internal/gate/gate_test.go+83 −0internal/gate/login.html+2 −0internal/gate/page.go+50 −5
CI failed — run details
diff --git a/internal/gate/gate_test.go b/internal/gate/gate_test.go| index 8c78c3551..824f7d366 100644 |
| --- a/internal/gate/gate_test.go |
| +++ b/internal/gate/gate_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "net/http" |
| "net/http/httptest" |
| "net/url" |
| + "os" |
| "strings" |
| "testing" |
| "time" |
| @@ -117,6 +118,88 @@ func TestAdmitNoCookieIsTheLoginPage(t *testing.T) { |
| } |
| } |
| +func TestProviderName(t *testing.T) { |
| + // Clean up first |
| + os.Unsetenv("CARLOS_PLATFORM_DOMAIN") |
| + |
| + // Test with no environment variable set |
| + if got := providerName(); got != "" { |
| + t.Errorf("providerName() = %q, want empty string", got) |
| + } |
| + |
| + // Test Carloku |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", "carloku.com") |
| + if got := providerName(); got != "Carloku" { |
| + t.Errorf("providerName() = %q, want %q", got, "Carloku") |
| + } |
| + |
| + // Test Tito Platform |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", "tito.io") |
| + if got := providerName(); got != "Tito Platform" { |
| + t.Errorf("providerName() = %q, want %q", got, "Tito Platform") |
| + } |
| + |
| + // Test CARLOS |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", "oncarlos.com") |
| + if got := providerName(); got != "CARLOS" { |
| + t.Errorf("providerName() = %q, want %q", got, "CARLOS") |
| + } |
| + |
| + // Test with subdomain (should still work) |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", "platform.tito.io") |
| + if got := providerName(); got != "Tito Platform" { |
| + t.Errorf("providerName() = %q, want %q", got, "Tito Platform") |
| + } |
| + |
| + // Test with whitespace |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", " carloku.com ") |
| + if got := providerName(); got != "Carloku" { |
| + t.Errorf("providerName() = %q, want %q", got, "Carloku") |
| + } |
| + |
| + // Test unknown domain (should capitalize first letter of second-level domain) |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", "custom.example.com") |
| + if got := providerName(); got != "Example" { |
| + t.Errorf("providerName() = %q, want %q", got, "Example") |
| + } |
| + |
| + // Clean up |
| + os.Unsetenv("CARLOS_PLATFORM_DOMAIN") |
| +} |
| + |
| +func TestLoginPageShowsProviderWhenConfigured(t *testing.T) { |
| + // Set the platform domain |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", "carloku.com") |
| + defer os.Unsetenv("CARLOS_PLATFORM_DOMAIN") |
| + |
| + g := New() |
| + hash, _ := Hash("test-password") |
| + |
| + rec := httptest.NewRecorder() |
| + req := httptest.NewRequest(http.MethodGet, "https://site.example/", nil) |
| + g.Admit(rec, req, "site.example", hash) |
| + |
| + body := rec.Body.String() |
| + if !strings.Contains(body, "This page is password protected by Carloku") { |
| + t.Fatalf("login page missing Carloku provider note: %s", body) |
| + } |
| + |
| + // Test Tito Platform |
| + os.Setenv("CARLOS_PLATFORM_DOMAIN", "tito.io") |
| + g2 := New() |
| + rec2 := httptest.NewRecorder() |
| + req2 := httptest.NewRequest(http.MethodGet, "https://site.example/", nil) |
| + g2.Admit(rec2, req2, "site.example", hash) |
| + |
| + body2 := rec2.Body.String() |
| + if !strings.Contains(body2, "This page is password protected by Tito Platform") { |
| + t.Fatalf("login page missing Tito Platform provider note: %s", body2) |
| + } |
| + |
| + // Clean up |
| + os.Unsetenv("CARLOS_PLATFORM_DOMAIN") |
| +} |
| + |
| func postLogin(g *Gate, host, hash, password, next string) *httptest.ResponseRecorder { |
| rec := httptest.NewRecorder() |
| form := url.Values{"password": {password}, "next": {next}} |
diff --git a/internal/gate/login.html b/internal/gate/login.html| index 2fa757543..82b1313ea 100644 |
| --- a/internal/gate/login.html |
| +++ b/internal/gate/login.html |
| @@ -37,6 +37,7 @@ button { |
| border: 0; border-radius: 0.4rem; padding: 0.6rem 0.9rem; cursor: pointer; |
| } |
| .error { color: var(--bad); margin: 0 0 0.9rem; } |
| +.note { color: var(--muted); font-size: 0.875rem; margin-top: 1rem; } |
| </style> |
| </head> |
| <body> |
| @@ -50,6 +51,7 @@ button { |
| <input type="password" id="password" name="password" autocomplete="current-password" autofocus required> |
| <button type="submit">Continue</button> |
| </form> |
| + {{if .Provider}}<p class="note">This page is password protected by {{.Provider}}</p>{{end}} |
| </main> |
| </body> |
| </html> |
diff --git a/internal/gate/page.go b/internal/gate/page.go| index 973c28c36..fa8762795 100644 |
| --- a/internal/gate/page.go |
| +++ b/internal/gate/page.go |
| @@ -5,6 +5,8 @@ import ( |
| "html/template" |
| "log" |
| "net/http" |
| + "os" |
| + "strings" |
| ) |
| //go:embed login.html |
| @@ -18,10 +20,53 @@ const ( |
| ) |
| type pageData struct { |
| - Host string |
| - Error string |
| - Next string |
| - Action string |
| + Host string |
| + Error string |
| + Next string |
| + Action string |
| + Provider string |
| +} |
| + |
| +// providerName returns the platform name derived from the deployment's |
| +// platform domain (CARLOS_PLATFORM_DOMAIN), or an empty string if not configured. |
| +// Examples: |
| +// - "carloku.com" → "Carloku" |
| +// - "tito.io" → "Tito Platform" |
| +// - "oncarlos.com" → "CARLOS" |
| +func providerName() string { |
| + domain := strings.TrimSpace(os.Getenv("CARLOS_PLATFORM_DOMAIN")) |
| + if domain == "" { |
| + return "" |
| + } |
| + |
| + // Extract the main domain name (remove any subdomains) |
| + // For example: "platform.tito.io" → "tito.io", "carloku.com" → "carloku.com" |
| + parts := strings.Split(domain, ".") |
| + if len(parts) < 2 { |
| + return "" |
| + } |
| + |
| + // Get the last two parts (main domain + TLD) |
| + mainDomain := strings.Join(parts[len(parts)-2:], ".") |
| + |
| + // Map known domains to their platform names |
| + switch strings.ToLower(mainDomain) { |
| + case "carloku.com": |
| + return "Carloku" |
| + case "tito.io": |
| + return "Tito Platform" |
| + case "oncarlos.com": |
| + return "CARLOS" |
| + default: |
| + // For unknown domains, capitalize the first letter of the second-level domain |
| + if len(parts) >= 2 { |
| + name := parts[len(parts)-2] // Get the second-level domain |
| + if len(name) > 0 { |
| + return strings.ToUpper(string(name[0])) + strings.ToLower(name[1:]) |
| + } |
| + } |
| + return "" |
| + } |
| } |
| // writePage renders the login page at status. Every page this package |
| @@ -35,7 +80,7 @@ func writePage(w http.ResponseWriter, r *http.Request, host string, status int, |
| if r.Method == http.MethodHead { |
| return |
| } |
| - if err := loginTmpl.Execute(w, pageData{Host: host, Error: msg, Next: next, Action: LoginPath}); err != nil { |
| + if err := loginTmpl.Execute(w, pageData{Host: host, Error: msg, Next: next, Action: LoginPath, Provider: providerName()}); err != nil { |
| log.Printf("gate: render login page for %s: %v", host, err) |
| } |
| } |