diff --git a/server/ui-base.html b/server/ui-base.html new file mode 100644 index 000000000..726f51062 --- /dev/null +++ b/server/ui-base.html @@ -0,0 +1,16 @@ + + + + {{block "title" .}}Tailscale OIDC Identity Provider{{end}} + + + + + +{{template "header"}} + +{{block "content" .}}{{end}} + +{{template "footer" .}} + + diff --git a/server/ui-edit.html b/server/ui-edit.html index 126f35f15..8627d1968 100644 --- a/server/ui-edit.html +++ b/server/ui-edit.html @@ -1,198 +1,167 @@ - - - - - {{if .IsNew}}Add New Client{{else}}Edit Client{{end}} - Tailscale OIDC Identity Provider - - - - - - - {{template "header"}} - -
-
+{{define "content"}} +
+
-

- {{if .IsNew}}Add New OIDC Client{{else}}Edit OIDC Client{{end}} -

- ← Back to Clients +

+ {{if .IsNew}}Add New OIDC Client{{else}}Edit OIDC Client{{end}} +

+ ← Back to Clients
{{if .Success}}
- {{.Success}} + {{.Success}}
{{end}} {{if .Error}}
- {{.Error}} + {{.Error}}
{{end}} {{if and .Secret .IsNew}}
-

Client Created Successfully!

-

⚠️ Save both the Client ID and Secret now! The secret will not be shown again.

- -
- -
- - +

Client Created Successfully!

+

⚠️ Save both the Client ID and Secret now! The secret will not be shown again.

+ +
+ +
+ + +
-
- -
- -
- - + +
+ +
+ + +
-
{{end}} {{if and .Secret .IsEdit}}
-

New Client Secret

-

⚠️ Save this secret now! It will not be shown again.

-
- - -
+

New Client Secret

+

⚠️ Save this secret now! It will not be shown again.

+
+ + +
{{end}} -
-
- - -
- A descriptive name for this OIDC client (optional). -
-
- -
- - -
- Enter one redirect URI per line. Users will be redirected to one of these URLs after authentication. + +
+ + +
+ A descriptive name for this OIDC client (optional). +
-
- - {{if .IsEdit}} -
- - -
- The client ID cannot be changed. + +
+ + +
+ Enter one redirect URI per line. Users will be redirected to one of these URLs after authentication. +
-
- {{end}} - -
- - + {{if .IsEdit}} - - - +
+ + +
+ The client ID cannot be changed. +
+
{{end}} -
+ +
+ + + {{if .IsEdit}} + + + + {{end}} +
{{if .IsEdit}}
-

Client Information

-
-
Client ID
-
{{.ID}}
-
Secret Status
-
- {{if .HasSecret}} - Secret configured - {{else}} - No secret - {{end}} -
-
+

Client Information

+
+
Client ID
+
{{.ID}}
+
Secret Status
+
+ {{if .HasSecret}} + Secret configured + {{else}} + No secret + {{end}} +
+
{{end}} -
-
- - - - \ No newline at end of file + } + +{{end}} diff --git a/server/ui-footer.html b/server/ui-footer.html new file mode 100644 index 000000000..478f4d712 --- /dev/null +++ b/server/ui-footer.html @@ -0,0 +1,5 @@ +
+ +
diff --git a/server/ui-header.html b/server/ui-header.html index 68e9bc0df..687256aa2 100644 --- a/server/ui-header.html +++ b/server/ui-header.html @@ -1,53 +1,53 @@
- -
\ No newline at end of file + + diff --git a/server/ui-list.html b/server/ui-list.html index cccbeab7b..355620a6a 100644 --- a/server/ui-list.html +++ b/server/ui-list.html @@ -1,83 +1,72 @@ - - - - Tailscale OIDC Identity Provider - - - - - - {{template "header"}} - -
-
+{{define "content"}} +
+
-

OIDC Clients

- {{if .}} -

{{len .}} client{{if ne (len .) 1}}s{{end}} configured

- {{end}} +

OIDC Clients

+ {{if .Clients}} +

{{len .Clients}} client{{if ne (len .Clients) 1}}s{{end}} configured

+ {{end}}
- Add New Client -
+ Add New Client +
- {{if .}} - + {{if .Clients}} +
- + - + - {{range .}} - + {{range .Clients}} + - - {{end}} + + {{end}} -
Name Client ID Redirect URIs Status Actions
- {{if .Name}} + {{if .Name}} {{.Name}} - {{else}} + {{else}} Unnamed Client - {{end}} + {{end}} - {{.ID}} + {{.ID}} - {{if gt (len .RedirectURIs) 1}} + {{if gt (len .RedirectURIs) 1}}
- {{range .RedirectURIs}} + {{range .RedirectURIs}} {{.}} - {{end}} + {{end}}
- {{else if eq (len .RedirectURIs) 1}} + {{else if eq (len .RedirectURIs) 1}} {{index .RedirectURIs 0}} - {{else}} + {{else}} No redirect URIs - {{end}} + {{end}}
- {{if .HasSecret}} + {{if .HasSecret}} Active - {{else}} + {{else}} No Secret - {{end}} + {{end}} - Edit + Edit
- {{else}} -
+ + {{else}} +

No OIDC clients configured

Create your first OIDC client to get started with authentication.

- Add New Client -
- {{end}} -
- - \ No newline at end of file + Add New Client +
+ {{end}} +
+{{end}} diff --git a/server/ui-style.css b/server/ui-style.css index 9fe3b6aae..80f8bb3d4 100644 --- a/server/ui-style.css +++ b/server/ui-style.css @@ -450,4 +450,23 @@ tbody tr:hover { .client-id { font-size: 10px; } -} \ No newline at end of file +} + +/* Footer */ +.app-footer { + margin-top: 60px; + padding: 24px 0; + border-top: 1px solid rgb(var(--color-gray-800)); +} + +.footer-content { + max-width: 1120px; + margin: 0 auto; + padding: 0 20px; + text-align: center; +} + +.version-info { + color: rgb(var(--color-gray-500)); + font-size: 14px; +} diff --git a/server/ui.go b/server/ui.go index 566e9d9e8..a44c4504f 100644 --- a/server/ui.go +++ b/server/ui.go @@ -18,6 +18,9 @@ import ( "tailscale.com/util/rands" ) +//go:embed ui-base.html +var baseHTML string + //go:embed ui-header.html var headerHTML string @@ -27,16 +30,39 @@ var listHTML string //go:embed ui-edit.html var editHTML string +//go:embed ui-footer.html +var footerHTML string + //go:embed ui-style.css var styleCSS string var tmplFuncs = template.FuncMap{ "joinRedirectURIs": joinRedirectURIs, + "GetAppVersion": GetVersion, } -var headerTmpl = template.Must(template.New("header").Funcs(tmplFuncs).Parse(headerHTML)) -var listTmpl = template.Must(headerTmpl.New("list").Parse(listHTML)) -var editTmpl = template.Must(headerTmpl.New("edit").Parse(editHTML)) +var ( + listTmpl *template.Template + editTmpl *template.Template +) + +func init() { + // Each page gets its own template set so their {{define "content"}} blocks + // don't collide in a shared set. + newBase := func() *template.Template { + t := template.Must(template.New("base").Funcs(tmplFuncs).Parse(baseHTML)) + template.Must(t.New("header").Parse(headerHTML)) + template.Must(t.New("footer").Parse(footerHTML)) + return t + } + l := newBase() + template.Must(l.New("list").Parse(listHTML)) + listTmpl = l + + e := newBase() + template.Must(e.New("edit").Parse(editHTML)) + editTmpl = e +} var processStart = time.Now() @@ -101,12 +127,19 @@ func (s *IDPServer) handleClientsList(w http.ResponseWriter, r *http.Request) { return clients[i].ID < clients[j].ID }) + data := listPageData{ + Clients: clients, + } + var buf bytes.Buffer - if err := listTmpl.Execute(&buf, clients); err != nil { + if err := listTmpl.ExecuteTemplate(&buf, "base", data); err != nil { writeHTTPError(w, r, http.StatusInternalServerError, ecServerError, "failed to render client list", err) return } - buf.WriteTo(w) + + if _, err := buf.WriteTo(w); err != nil { + slog.Error("failed to write client list response", slog.Any("error", err)) + } } // handleNewClient handles creating a new OAuth/OIDC client @@ -319,7 +352,7 @@ func (s *IDPServer) handleEditClient(w http.ResponseWriter, r *http.Request) { writeHTTPError(w, r, http.StatusMethodNotAllowed, ecInvalidRequest, "Method not allowed", nil) } -// clientDisplayData holds data for rendering client forms and lists +// clientDisplayData holds data for rendering client forms // Migrated from legacy/ui.go:321-331 type clientDisplayData struct { ID string @@ -333,13 +366,19 @@ type clientDisplayData struct { Error string } +// listPageData holds data for rendering the clients list page +type listPageData struct { + Clients []clientDisplayData +} + // renderClientForm renders the client edit/create form // Migrated from legacy/ui.go:333-342 func (s *IDPServer) renderClientForm(w http.ResponseWriter, data clientDisplayData) error { var buf bytes.Buffer - if err := editTmpl.Execute(&buf, data); err != nil { + if err := editTmpl.ExecuteTemplate(&buf, "base", data); err != nil { return err } + if _, err := buf.WriteTo(w); err != nil { return err }