admin.go

v1.3.6
Doc Versions Source
1
package admin
2
3
import (
4
	"embed"
5
	"encoding/json"
6
	"fmt"
7
	"html/template"
8
	"log"
9
	"net/http"
10
	"strconv"
11
12
	"github.com/go-git/go-git/v6/plumbing/transport"
13
14
	"go.bigb.es/curator/internal/git"
15
	"go.bigb.es/curator/internal/store"
16
)
17
18
//go:embed templates/*
19
var content embed.FS
20
21
// Handler serves the admin UI.
22
type Handler struct {
23
	store   *store.Store
24
	pages   map[string]*template.Template
25
	version string
26
}
27
28
var funcs = template.FuncMap{
29
	"boolToYes": func(b bool) string {
30
		if b {
31
			return "Yes"
32
		}
33
		return "No"
34
	},
35
}
36
37
// New creates a new admin handler.
38
func New(st *store.Store, version string) (*Handler, error) {
39
	pages := map[string]*template.Template{}
40
	for _, name := range []string{"dashboard.html", "modules.html", "patterns.html", "credentials.html"} {
41
		t, err := template.New("").Funcs(funcs).ParseFS(content,
42
			"templates/base_admin.html", "templates/"+name)
43
		if err != nil {
44
			return nil, fmt.Errorf("parse admin templates: %w", err)
45
		}
46
		pages[name] = t
47
	}
48
49
	return &Handler{store: st, pages: pages, version: version}, nil
50
}
51
52
// ServeHTTP routes admin requests.
53
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
54
	switch {
55
	case r.URL.Path == "/-/admin/" || r.URL.Path == "/-/admin":
56
		h.dashboard(w, r)
57
	case r.URL.Path == "/-/admin/modules" && r.Method == http.MethodGet:
58
		h.listModules(w, r)
59
	case r.URL.Path == "/-/admin/modules" && r.Method == http.MethodPost:
60
		h.addModule(w, r)
61
	case r.URL.Path == "/-/admin/modules/update" && r.Method == http.MethodPost:
62
		h.updateModule(w, r)
63
	case r.URL.Path == "/-/admin/modules/delete" && r.Method == http.MethodPost:
64
		h.deleteModule(w, r)
65
	case r.URL.Path == "/-/admin/patterns" && r.Method == http.MethodGet:
66
		h.listPatterns(w, r)
67
	case r.URL.Path == "/-/admin/patterns" && r.Method == http.MethodPost:
68
		h.addPattern(w, r)
69
	case r.URL.Path == "/-/admin/patterns/update" && r.Method == http.MethodPost:
70
		h.updatePattern(w, r)
71
	case r.URL.Path == "/-/admin/patterns/delete" && r.Method == http.MethodPost:
72
		h.deletePattern(w, r)
73
	case r.URL.Path == "/-/admin/credentials" && r.Method == http.MethodGet:
74
		h.listCredentials(w, r)
75
	case r.URL.Path == "/-/admin/credentials" && r.Method == http.MethodPost:
76
		h.addCredential(w, r)
77
	case r.URL.Path == "/-/admin/credentials/delete" && r.Method == http.MethodPost:
78
		h.deleteCredential(w, r)
79
	default:
80
		http.NotFound(w, r)
81
	}
82
}
83
84
func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) {
85
	modules, _ := h.store.ListModules()
86
	patterns, _ := h.store.ListPatterns()
87
88
	data := map[string]any{
89
		"ModuleCount":     len(modules),
90
		"PatternCount":    len(patterns),
91
		"CredentialCount": h.store.CredentialCount(),
92
		"Version":         h.version,
93
	}
94
95
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
96
	if err := h.pages["dashboard.html"].ExecuteTemplate(w, "base", data); err != nil {
97
		log.Printf("admin: render dashboard: %v", err)
98
	}
99
}
100
101
func (h *Handler) listModules(w http.ResponseWriter, r *http.Request) {
102
	modules, err := h.store.ListModules()
103
	if err != nil {
104
		http.Error(w, "internal error", http.StatusInternalServerError)
105
		return
106
	}
107
108
	creds, _ := h.store.ListCredentials()
109
110
	data := map[string]any{
111
		"Modules":     modules,
112
		"Credentials": creds,
113
		"Flash":       r.URL.Query().Get("msg"),
114
		"Error":       r.URL.Query().Get("err"),
115
		"Version":     h.version,
116
	}
117
118
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
119
	if err := h.pages["modules.html"].ExecuteTemplate(w, "base", data); err != nil {
120
		log.Printf("admin: render modules: %v", err)
121
	}
122
}
123
124
func (h *Handler) renderModulesWithError(w http.ResponseWriter, errMsg string, form *store.ModuleRow) {
125
	modules, _ := h.store.ListModules()
126
	creds, _ := h.store.ListCredentials()
127
128
	data := map[string]any{
129
		"Modules":     modules,
130
		"Credentials": creds,
131
		"Error":       errMsg,
132
		"Version":     h.version,
133
	}
134
	if form != nil {
135
		data["Form"] = form
136
	}
137
138
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
139
	if err := h.pages["modules.html"].ExecuteTemplate(w, "base", data); err != nil {
140
		log.Printf("admin: render modules: %v", err)
141
	}
142
}
143
144
func (h *Handler) addModule(w http.ResponseWriter, r *http.Request) {
145
	if err := r.ParseForm(); err != nil {
146
		http.Error(w, "bad request", http.StatusBadRequest)
147
		return
148
	}
149
150
	m := store.ModuleRow{
151
		Name:           r.FormValue("name"),
152
		VCS:            r.FormValue("vcs"),
153
		Repo:           r.FormValue("repo"),
154
		Web:            r.FormValue("web"),
155
		Private:        r.FormValue("private") == "on",
156
		CredentialName: r.FormValue("credential_name"),
157
	}
158
159
	if m.Name == "" || m.Repo == "" {
160
		h.renderModulesWithError(w, "Name and repo are required", &m)
161
		return
162
	}
163
164
	if m.VCS == "" {
165
		m.VCS = "git"
166
	}
167
168
	// Verify the repository is accessible.
169
	auth := h.authForCredential(m.CredentialName)
170
	if err := store.VerifyModule(m.Repo, auth); err != nil {
171
		log.Printf("admin: verify module %s: %v", m.Name, err)
172
		h.renderModulesWithError(w, "Repository verification failed: "+err.Error(), &m)
173
		return
174
	}
175
176
	if err := h.store.AddModule(m); err != nil {
177
		log.Printf("admin: add module %s: %v", m.Name, err)
178
		h.renderModulesWithError(w, err.Error(), &m)
179
		return
180
	}
181
182
	http.Redirect(w, r, "/-/admin/modules?msg=Module+"+m.Name+"+added", http.StatusSeeOther)
183
}
184
185
func (h *Handler) updateModule(w http.ResponseWriter, r *http.Request) {
186
	if err := r.ParseForm(); err != nil {
187
		http.Error(w, "bad request", http.StatusBadRequest)
188
		return
189
	}
190
191
	m := store.ModuleRow{
192
		Name:           r.FormValue("name"),
193
		VCS:            r.FormValue("vcs"),
194
		Repo:           r.FormValue("repo"),
195
		Web:            r.FormValue("web"),
196
		Private:        r.FormValue("private") == "on",
197
		CredentialName: r.FormValue("credential_name"),
198
	}
199
200
	if m.Name == "" || m.Repo == "" {
201
		h.renderModulesWithError(w, "Name and repo are required", &m)
202
		return
203
	}
204
205
	if m.VCS == "" {
206
		m.VCS = "git"
207
	}
208
209
	// Verify the repository is accessible.
210
	auth := h.authForCredential(m.CredentialName)
211
	if err := store.VerifyModule(m.Repo, auth); err != nil {
212
		log.Printf("admin: verify module %s: %v", m.Name, err)
213
		h.renderModulesWithError(w, "Repository verification failed: "+err.Error(), &m)
214
		return
215
	}
216
217
	if err := h.store.UpdateModule(m); err != nil {
218
		log.Printf("admin: update module %s: %v", m.Name, err)
219
		h.renderModulesWithError(w, err.Error(), &m)
220
		return
221
	}
222
223
	http.Redirect(w, r, "/-/admin/modules?msg=Module+"+m.Name+"+updated", http.StatusSeeOther)
224
}
225
226
func (h *Handler) deleteModule(w http.ResponseWriter, r *http.Request) {
227
	name := r.FormValue("name")
228
	if name == "" {
229
		http.Redirect(w, r, "/-/admin/modules?err=Name+required", http.StatusSeeOther)
230
		return
231
	}
232
233
	if err := h.store.DeleteModule(name); err != nil {
234
		log.Printf("admin: delete module %s: %v", name, err)
235
		http.Redirect(w, r, "/-/admin/modules?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
236
		return
237
	}
238
239
	http.Redirect(w, r, "/-/admin/modules?msg=Module+"+name+"+deleted", http.StatusSeeOther)
240
}
241
242
func (h *Handler) listPatterns(w http.ResponseWriter, r *http.Request) {
243
	patterns, err := h.store.ListPatterns()
244
	if err != nil {
245
		http.Error(w, "internal error", http.StatusInternalServerError)
246
		return
247
	}
248
249
	creds, _ := h.store.ListCredentials()
250
251
	data := map[string]any{
252
		"Patterns":    patterns,
253
		"Credentials": creds,
254
		"Flash":       r.URL.Query().Get("msg"),
255
		"Error":       r.URL.Query().Get("err"),
256
		"Version":     h.version,
257
	}
258
259
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
260
	if err := h.pages["patterns.html"].ExecuteTemplate(w, "base", data); err != nil {
261
		log.Printf("admin: render patterns: %v", err)
262
	}
263
}
264
265
func (h *Handler) renderPatternsWithError(w http.ResponseWriter, errMsg string, form *store.PatternRow) {
266
	patterns, _ := h.store.ListPatterns()
267
	creds, _ := h.store.ListCredentials()
268
269
	data := map[string]any{
270
		"Patterns":    patterns,
271
		"Credentials": creds,
272
		"Error":       errMsg,
273
		"Version":     h.version,
274
	}
275
	if form != nil {
276
		data["Form"] = form
277
	}
278
279
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
280
	if err := h.pages["patterns.html"].ExecuteTemplate(w, "base", data); err != nil {
281
		log.Printf("admin: render patterns: %v", err)
282
	}
283
}
284
285
func (h *Handler) addPattern(w http.ResponseWriter, r *http.Request) {
286
	if err := r.ParseForm(); err != nil {
287
		http.Error(w, "bad request", http.StatusBadRequest)
288
		return
289
	}
290
291
	priority, _ := strconv.Atoi(r.FormValue("priority"))
292
293
	p := store.PatternRow{
294
		Pattern:        r.FormValue("pattern"),
295
		VCS:            r.FormValue("vcs"),
296
		Repo:           r.FormValue("repo"),
297
		Web:            r.FormValue("web"),
298
		Private:        r.FormValue("private") == "on",
299
		Priority:       priority,
300
		CredentialName: r.FormValue("credential_name"),
301
	}
302
303
	if p.Pattern == "" || p.Repo == "" {
304
		h.renderPatternsWithError(w, "Pattern and repo template are required", &p)
305
		return
306
	}
307
308
	if p.VCS == "" {
309
		p.VCS = "git"
310
	}
311
312
	// Verify the regex compiles.
313
	if err := store.VerifyPattern(p.Pattern); err != nil {
314
		log.Printf("admin: verify pattern %s: %v", p.Pattern, err)
315
		h.renderPatternsWithError(w, err.Error(), &p)
316
		return
317
	}
318
319
	if err := h.store.AddPattern(p); err != nil {
320
		log.Printf("admin: add pattern %s: %v", p.Pattern, err)
321
		h.renderPatternsWithError(w, err.Error(), &p)
322
		return
323
	}
324
325
	http.Redirect(w, r, "/-/admin/patterns?msg=Pattern+added", http.StatusSeeOther)
326
}
327
328
func (h *Handler) updatePattern(w http.ResponseWriter, r *http.Request) {
329
	if err := r.ParseForm(); err != nil {
330
		http.Error(w, "bad request", http.StatusBadRequest)
331
		return
332
	}
333
334
	priority, _ := strconv.Atoi(r.FormValue("priority"))
335
336
	p := store.PatternRow{
337
		Pattern:        r.FormValue("pattern"),
338
		VCS:            r.FormValue("vcs"),
339
		Repo:           r.FormValue("repo"),
340
		Web:            r.FormValue("web"),
341
		Private:        r.FormValue("private") == "on",
342
		Priority:       priority,
343
		CredentialName: r.FormValue("credential_name"),
344
	}
345
346
	if p.Pattern == "" || p.Repo == "" {
347
		h.renderPatternsWithError(w, "Pattern and repo template are required", &p)
348
		return
349
	}
350
351
	if p.VCS == "" {
352
		p.VCS = "git"
353
	}
354
355
	idStr := r.FormValue("edit_id")
356
	if idStr == "" {
357
		h.renderPatternsWithError(w, "Pattern ID required for update", &p)
358
		return
359
	}
360
	if _, err := fmt.Sscanf(idStr, "%d", &p.ID); err != nil {
361
		h.renderPatternsWithError(w, "Invalid pattern ID", &p)
362
		return
363
	}
364
365
	// Verify the regex compiles.
366
	if err := store.VerifyPattern(p.Pattern); err != nil {
367
		log.Printf("admin: verify pattern %s: %v", p.Pattern, err)
368
		h.renderPatternsWithError(w, err.Error(), &p)
369
		return
370
	}
371
372
	if err := h.store.UpdatePattern(p); err != nil {
373
		log.Printf("admin: update pattern %d: %v", p.ID, err)
374
		h.renderPatternsWithError(w, err.Error(), &p)
375
		return
376
	}
377
378
	http.Redirect(w, r, "/-/admin/patterns?msg=Pattern+updated", http.StatusSeeOther)
379
}
380
381
func (h *Handler) deletePattern(w http.ResponseWriter, r *http.Request) {
382
	idStr := r.FormValue("id")
383
	if idStr == "" {
384
		http.Redirect(w, r, "/-/admin/patterns?err=ID+required", http.StatusSeeOther)
385
		return
386
	}
387
388
	var id int64
389
	if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
390
		http.Redirect(w, r, "/-/admin/patterns?err=Invalid+ID", http.StatusSeeOther)
391
		return
392
	}
393
394
	if err := h.store.DeletePattern(id); err != nil {
395
		log.Printf("admin: delete pattern %d: %v", id, err)
396
		http.Redirect(w, r, "/-/admin/patterns?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
397
		return
398
	}
399
400
	http.Redirect(w, r, "/-/admin/patterns?msg=Pattern+deleted", http.StatusSeeOther)
401
}
402
403
func (h *Handler) listCredentials(w http.ResponseWriter, r *http.Request) {
404
	creds, err := h.store.ListCredentials()
405
	if err != nil {
406
		http.Error(w, "internal error", http.StatusInternalServerError)
407
		return
408
	}
409
410
	data := map[string]any{
411
		"Credentials": creds,
412
		"Flash":       r.URL.Query().Get("msg"),
413
		"Error":       r.URL.Query().Get("err"),
414
		"Version":     h.version,
415
	}
416
417
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
418
	if err := h.pages["credentials.html"].ExecuteTemplate(w, "base", data); err != nil {
419
		log.Printf("admin: render credentials: %v", err)
420
	}
421
}
422
423
func (h *Handler) addCredential(w http.ResponseWriter, r *http.Request) {
424
	if err := r.ParseForm(); err != nil {
425
		http.Error(w, "bad request", http.StatusBadRequest)
426
		return
427
	}
428
429
	name := r.FormValue("name")
430
	credType := r.FormValue("type")
431
432
	if name == "" || credType == "" {
433
		http.Redirect(w, r, "/-/admin/credentials?err=Name+and+type+are+required", http.StatusSeeOther)
434
		return
435
	}
436
437
	// Build JSON data from form fields based on credential type.
438
	var data string
439
	switch credType {
440
	case "basic":
441
		d, _ := json.Marshal(map[string]string{
442
			"username": r.FormValue("username"),
443
			"password": r.FormValue("password"),
444
		})
445
		data = string(d)
446
	case "token":
447
		d, _ := json.Marshal(map[string]string{
448
			"token": r.FormValue("token"),
449
		})
450
		data = string(d)
451
	case "ssh":
452
		d, _ := json.Marshal(map[string]string{
453
			"key":      r.FormValue("key"),
454
			"password": r.FormValue("key_password"),
455
		})
456
		data = string(d)
457
	default:
458
		http.Redirect(w, r, "/-/admin/credentials?err=Invalid+credential+type", http.StatusSeeOther)
459
		return
460
	}
461
462
	c := store.CredentialRow{
463
		Name: name,
464
		Type: credType,
465
		Data: data,
466
	}
467
468
	if err := h.store.AddCredential(c); err != nil {
469
		log.Printf("admin: add credential %s: %v", name, err)
470
		http.Redirect(w, r, "/-/admin/credentials?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
471
		return
472
	}
473
474
	http.Redirect(w, r, "/-/admin/credentials?msg=Credential+"+name+"+added", http.StatusSeeOther)
475
}
476
477
func (h *Handler) deleteCredential(w http.ResponseWriter, r *http.Request) {
478
	name := r.FormValue("name")
479
	if name == "" {
480
		http.Redirect(w, r, "/-/admin/credentials?err=Name+required", http.StatusSeeOther)
481
		return
482
	}
483
484
	if err := h.store.DeleteCredential(name); err != nil {
485
		log.Printf("admin: delete credential %s: %v", name, err)
486
		http.Redirect(w, r, "/-/admin/credentials?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
487
		return
488
	}
489
490
	http.Redirect(w, r, "/-/admin/credentials?msg=Credential+"+name+"+deleted", http.StatusSeeOther)
491
}
492
493
// authForCredential looks up a credential by name and builds a transport.AuthMethod.
494
// Returns nil if name is empty or the credential cannot be found/built.
495
func (h *Handler) authForCredential(name string) transport.AuthMethod {
496
	if name == "" {
497
		return nil
498
	}
499
	cred, err := h.store.GetCredential(name)
500
	if err != nil {
501
		return nil
502
	}
503
	auth, err := git.AuthMethodFromCredential(cred.Type, cred.Data)
504
	if err != nil {
505
		return nil
506
	}
507
	return auth
508
}
509

Source Files