Skip to content

Commit 8eac6fd

Browse files
DAcodedBEATArun Philip
authored andcommitted
feat: show version on dashboard and add --version CLI flag
Display release version in dashboard footer and add --version CLI output. Includes template optimizations and goreleaser integration. Closes #157 Signed-off-by: Arun Philip <arun@corkinc.com>
1 parent c2c1d8d commit 8eac6fd

9 files changed

Lines changed: 169 additions & 96 deletions

File tree

.goreleaser.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ version: 2
33
builds:
44
- env:
55
- CGO_ENABLED=0
6+
ldflags:
7+
- -s -w
8+
- -X github.com/tailscale/tsidp/server.version={{.Version}}
69
goos:
710
- linux
811
- darwin

server/ui-base.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>{{block "title" .}}Tailscale OIDC Identity Provider{{end}}</title>
5+
<link rel="stylesheet" type="text/css" href="/style.css" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
</head>
8+
9+
<body>
10+
{{template "header"}}
11+
12+
{{block "content" .}}{{end}}
13+
14+
{{template "footer" .}}
15+
</body>
16+
</html>

server/ui-edit.html

Lines changed: 34 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,4 @@
1-
<!DOCTYPE html>
2-
<html>
3-
<head>
4-
<title>
5-
{{if .IsNew}}Add New Client{{else}}Edit Client{{end}} - Tailscale OIDC Identity Provider
6-
</title>
7-
<link rel="stylesheet" type="text/css" href="/style.css" />
8-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
9-
</head>
10-
11-
<body>
12-
{{template "header"}}
13-
1+
{{define "content"}}
142
<main>
153
<div class="form-container">
164
<div class="form-header">
@@ -36,20 +24,20 @@ <h2>
3624
<div class="client-info">
3725
<h3>Client Created Successfully!</h3>
3826
<p class="warning">⚠️ Save both the Client ID and Secret now! The secret will not be shown again.</p>
39-
40-
<div class="form-group">
27+
28+
<div class="form-group">
4129
<label>Client ID</label>
4230
<div class="secret-field">
43-
<input type="text" value="{{.ID}}" readonly class="secret-input" id="client-id">
44-
<button type="button" onclick="copyClientId(event)" class="btn btn-secondary btn-small">Copy</button>
31+
<input type="text" value="{{.ID}}" readonly class="secret-input" aria-label="Client ID">
32+
<button type="button" onclick="copyValue(this.previousElementSibling, this)" class="btn btn-secondary btn-small">Copy</button>
4533
</div>
4634
</div>
47-
35+
4836
<div class="form-group">
4937
<label>Client Secret</label>
5038
<div class="secret-field">
51-
<input type="text" value="{{.Secret}}" readonly class="secret-input" id="client-secret">
52-
<button type="button" onclick="copySecret(event)" class="btn btn-secondary btn-small">Copy</button>
39+
<input type="text" value="{{.Secret}}" readonly class="secret-input" aria-label="Client Secret">
40+
<button type="button" onclick="copyValue(this.previousElementSibling, this)" class="btn btn-secondary btn-small">Copy</button>
5341
</div>
5442
</div>
5543
</div>
@@ -60,20 +48,20 @@ <h3>Client Created Successfully!</h3>
6048
<h3>New Client Secret</h3>
6149
<p class="warning">⚠️ Save this secret now! It will not be shown again.</p>
6250
<div class="secret-field">
63-
<input type="text" value="{{.Secret}}" readonly class="secret-input" id="client-secret">
64-
<button type="button" onclick="copySecret(event)" class="btn btn-secondary btn-small">Copy</button>
51+
<input type="text" value="{{.Secret}}" readonly class="secret-input" aria-label="Client Secret">
52+
<button type="button" onclick="copyValue(this.previousElementSibling, this)" class="btn btn-secondary btn-small">Copy</button>
6553
</div>
6654
</div>
6755
{{end}}
6856

6957
<form method="POST" class="client-form">
7058
<div class="form-group">
7159
<label for="name">Client Name</label>
72-
<input
73-
type="text"
74-
id="name"
75-
name="name"
76-
value="{{.Name}}"
60+
<input
61+
type="text"
62+
id="name"
63+
name="name"
64+
value="{{.Name}}"
7765
placeholder="e.g., My Application"
7866
class="form-input"
7967
>
@@ -84,9 +72,9 @@ <h3>New Client Secret</h3>
8472

8573
<div class="form-group">
8674
<label for="redirect_uris">Redirect URIs <span class="required">*</span></label>
87-
<textarea
88-
id="redirect_uris"
89-
name="redirect_uris"
75+
<textarea
76+
id="redirect_uris"
77+
name="redirect_uris"
9078
placeholder="https://example.com/auth/callback&#10;https://example.com/oauth/callback"
9179
class="form-input"
9280
rows="4"
@@ -100,10 +88,10 @@ <h3>New Client Secret</h3>
10088
{{if .IsEdit}}
10189
<div class="form-group">
10290
<label>Client ID</label>
103-
<input
104-
type="text"
105-
value="{{.ID}}"
106-
readonly
91+
<input
92+
type="text"
93+
value="{{.ID}}"
94+
readonly
10795
class="form-input form-input-readonly"
10896
>
10997
<div class="form-help">
@@ -116,14 +104,14 @@ <h3>New Client Secret</h3>
116104
<button type="submit" class="btn btn-primary">
117105
{{if .IsNew}}Create Client{{else}}Update Client{{end}}
118106
</button>
119-
107+
120108
{{if .IsEdit}}
121-
<button type="submit" name="action" value="regenerate_secret" class="btn btn-warning"
109+
<button type="submit" name="action" value="regenerate_secret" class="btn btn-warning"
122110
onclick="return confirm('Are you sure you want to regenerate the client secret? The old secret will stop working immediately.')">
123111
Regenerate Secret
124112
</button>
125-
126-
<button type="submit" name="action" value="delete" class="btn btn-danger"
113+
114+
<button type="submit" name="action" value="delete" class="btn btn-danger"
127115
onclick="return confirm('Are you sure you want to delete this client? This cannot be undone.')">
128116
Delete Client
129117
</button>
@@ -152,47 +140,22 @@ <h3>Client Information</h3>
152140
</main>
153141

154142
<script>
155-
function copySecret(event) {
156-
const secretInput = document.getElementById('client-secret');
157-
secretInput.select();
158-
secretInput.setSelectionRange(0, 99999); // For mobile devices
159-
160-
navigator.clipboard.writeText(secretInput.value).then(function() {
161-
const button = event.target;
162-
const originalText = button.textContent;
163-
button.textContent = 'Copied!';
164-
button.classList.add('btn-success');
165-
166-
setTimeout(function() {
167-
button.textContent = originalText;
168-
button.classList.remove('btn-success');
169-
}, 2000);
170-
}).catch(function(err) {
171-
console.error('Failed to copy: ', err);
172-
alert('Failed to copy to clipboard. Please copy manually.');
173-
});
174-
}
175-
176-
function copyClientId(event) {
177-
const clientIdInput = document.getElementById('client-id');
178-
clientIdInput.select();
179-
clientIdInput.setSelectionRange(0, 99999); // For mobile devices
180-
181-
navigator.clipboard.writeText(clientIdInput.value).then(function() {
182-
const button = event.target;
143+
function copyValue(input, button) {
144+
input.select();
145+
input.setSelectionRange(0, 99999); // For mobile devices
146+
147+
navigator.clipboard.writeText(input.value).then(function() {
183148
const originalText = button.textContent;
184149
button.textContent = 'Copied!';
185150
button.classList.add('btn-success');
186-
151+
187152
setTimeout(function() {
188153
button.textContent = originalText;
189154
button.classList.remove('btn-success');
190155
}, 2000);
191-
}).catch(function(err) {
192-
console.error('Failed to copy: ', err);
156+
}).catch(function() {
193157
alert('Failed to copy to clipboard. Please copy manually.');
194158
});
195159
}
196160
</script>
197-
</body>
198-
</html>
161+
{{end}}

server/ui-footer.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<footer class="app-footer">
2+
<div class="footer-content">
3+
<span class="version-info">tsidp {{.Version}}</span>
4+
</div>
5+
</footer>

server/ui-list.html

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
1-
<!DOCTYPE html>
2-
<html>
3-
<head>
4-
<title>Tailscale OIDC Identity Provider</title>
5-
<link rel="stylesheet" type="text/css" href="/style.css" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
</head>
8-
9-
<body>
10-
{{template "header"}}
11-
1+
{{define "content"}}
122
<main>
133
<div class="header-actions">
144
<div>
155
<h2>OIDC Clients</h2>
16-
{{if .}}
17-
<p class="client-count">{{len .}} client{{if ne (len .) 1}}s{{end}} configured</p>
6+
{{if .Clients}}
7+
<p class="client-count">{{len .Clients}} client{{if ne (len .Clients) 1}}s{{end}} configured</p>
188
{{end}}
199
</div>
2010
<a href="/new" class="btn btn-primary">Add New Client</a>
2111
</div>
2212

23-
{{if .}}
13+
{{if .Clients}}
2414
<table>
2515
<thead>
2616
<tr>
@@ -32,7 +22,7 @@ <h2>OIDC Clients</h2>
3222
</tr>
3323
</thead>
3424
<tbody>
35-
{{range .}}
25+
{{range .Clients}}
3626
<tr>
3727
<td>
3828
{{if .Name}}
@@ -79,5 +69,4 @@ <h3>No OIDC clients configured</h3>
7969
</div>
8070
{{end}}
8171
</main>
82-
</body>
83-
</html>
72+
{{end}}

server/ui-style.css

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,4 +450,23 @@ tbody tr:hover {
450450
.client-id {
451451
font-size: 10px;
452452
}
453-
}
453+
}
454+
455+
/* Footer */
456+
.app-footer {
457+
margin-top: 60px;
458+
padding: 24px 0;
459+
border-top: 1px solid rgb(var(--color-gray-800));
460+
}
461+
462+
.footer-content {
463+
max-width: 1120px;
464+
margin: 0 auto;
465+
padding: 0 20px;
466+
text-align: center;
467+
}
468+
469+
.version-info {
470+
color: rgb(var(--color-gray-500));
471+
font-size: 14px;
472+
}

server/ui.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import (
1818
"tailscale.com/util/rands"
1919
)
2020

21+
//go:embed ui-base.html
22+
var baseHTML string
23+
2124
//go:embed ui-header.html
2225
var headerHTML string
2326

@@ -27,16 +30,38 @@ var listHTML string
2730
//go:embed ui-edit.html
2831
var editHTML string
2932

33+
//go:embed ui-footer.html
34+
var footerHTML string
35+
3036
//go:embed ui-style.css
3137
var styleCSS string
3238

3339
var tmplFuncs = template.FuncMap{
3440
"joinRedirectURIs": joinRedirectURIs,
3541
}
3642

37-
var headerTmpl = template.Must(template.New("header").Funcs(tmplFuncs).Parse(headerHTML))
38-
var listTmpl = template.Must(headerTmpl.New("list").Parse(listHTML))
39-
var editTmpl = template.Must(headerTmpl.New("edit").Parse(editHTML))
43+
var (
44+
listTmpl *template.Template
45+
editTmpl *template.Template
46+
)
47+
48+
func init() {
49+
// Each page gets its own template set so their {{define "content"}} blocks
50+
// don't collide in a shared set.
51+
newBase := func() *template.Template {
52+
t := template.Must(template.New("base").Funcs(tmplFuncs).Parse(baseHTML))
53+
template.Must(t.New("header").Parse(headerHTML))
54+
template.Must(t.New("footer").Parse(footerHTML))
55+
return t
56+
}
57+
l := newBase()
58+
template.Must(l.New("list").Parse(listHTML))
59+
listTmpl = l
60+
61+
e := newBase()
62+
template.Must(e.New("edit").Parse(editHTML))
63+
editTmpl = e
64+
}
4065

4166
var processStart = time.Now()
4267

@@ -101,12 +126,20 @@ func (s *IDPServer) handleClientsList(w http.ResponseWriter, r *http.Request) {
101126
return clients[i].ID < clients[j].ID
102127
})
103128

129+
data := listPageData{
130+
Clients: clients,
131+
Version: GetVersion(),
132+
}
133+
104134
var buf bytes.Buffer
105-
if err := listTmpl.Execute(&buf, clients); err != nil {
135+
if err := listTmpl.ExecuteTemplate(&buf, "base", data); err != nil {
106136
writeHTTPError(w, r, http.StatusInternalServerError, ecServerError, "failed to render client list", err)
107137
return
108138
}
109-
buf.WriteTo(w)
139+
140+
if _, err := buf.WriteTo(w); err != nil {
141+
slog.Error("failed to write client list response", slog.Any("error", err))
142+
}
110143
}
111144

112145
// handleNewClient handles creating a new OAuth/OIDC client
@@ -319,7 +352,7 @@ func (s *IDPServer) handleEditClient(w http.ResponseWriter, r *http.Request) {
319352
writeHTTPError(w, r, http.StatusMethodNotAllowed, ecInvalidRequest, "Method not allowed", nil)
320353
}
321354

322-
// clientDisplayData holds data for rendering client forms and lists
355+
// clientDisplayData holds data for rendering client forms
323356
// Migrated from legacy/ui.go:321-331
324357
type clientDisplayData struct {
325358
ID string
@@ -331,15 +364,26 @@ type clientDisplayData struct {
331364
IsEdit bool
332365
Success string
333366
Error string
367+
Version string // Version for footer display
368+
}
369+
370+
// listPageData holds data for rendering the clients list page
371+
type listPageData struct {
372+
Clients []clientDisplayData
373+
Version string
334374
}
335375

336376
// renderClientForm renders the client edit/create form
337377
// Migrated from legacy/ui.go:333-342
338378
func (s *IDPServer) renderClientForm(w http.ResponseWriter, data clientDisplayData) error {
379+
// Add version to data
380+
data.Version = GetVersion()
381+
339382
var buf bytes.Buffer
340-
if err := editTmpl.Execute(&buf, data); err != nil {
383+
if err := editTmpl.ExecuteTemplate(&buf, "base", data); err != nil {
341384
return err
342385
}
386+
343387
if _, err := buf.WriteTo(w); err != nil {
344388
return err
345389
}

0 commit comments

Comments
 (0)