carlos / platform Public

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.
Paul Campbell pushed by paul@keymail.dev 0e4c9e5028c10358316400dd641e414411c0061f parent 16593de
3 files changed, +135 −5
  • internal/gate/gate_test.go +83 −0
  • internal/gate/login.html +2 −0
  • internal/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)
}
}