admin.go

v1.1.0
Doc Versions Source
1
package admin
2
3
import (
4
	"embed"
5
	"fmt"
6
	"html/template"
7
	"log"
8
	"net/http"
9
10
	"example.com/curator/internal/store"
11
)
12
13
//go:embed templates/*
14
var content embed.FS
15
16
// Handler serves the admin UI.
17
type Handler struct {
18
	store   *store.Store
19
	pages   map[string]*template.Template
20
	version string
21
}
22
23
var funcs = template.FuncMap{
24
	"boolToYes": func(b bool) string {
25
		if b {
26
			return "Yes"
27
		}
28
		return "No"
29
	},
30
}
31
32
// New creates a new admin handler.
33
func New(st *store.Store, version string) (*Handler, error) {
34
	pages := map[string]*template.Template{}
35
	for _, name := range []string{"dashboard.html", "modules.html", "patterns.html"} {
36
		t, err := template.New("").Funcs(funcs).ParseFS(content,
37
			"templates/base_admin.html", "templates/"+name)
38
		if err != nil {
39
			return nil, fmt.Errorf("parse admin templates: %w", err)
40
		}
41
		pages[name] = t
42
	}
43
44
	return &Handler{store: st, pages: pages, version: version}, nil
45
}
46
47
// ServeHTTP routes admin requests.
48
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
49
	switch {
50
	case r.URL.Path == "/-/admin/" || r.URL.Path == "/-/admin":
51
		h.dashboard(w, r)
52
	case r.URL.Path == "/-/admin/modules" && r.Method == http.MethodGet:
53
		h.listModules(w, r)
54
	case r.URL.Path == "/-/admin/modules" && r.Method == http.MethodPost:
55
		h.addModule(w, r)
56
	case r.URL.Path == "/-/admin/modules/delete" && r.Method == http.MethodPost:
57
		h.deleteModule(w, r)
58
	case r.URL.Path == "/-/admin/patterns" && r.Method == http.MethodGet:
59
		h.listPatterns(w, r)
60
	case r.URL.Path == "/-/admin/patterns" && r.Method == http.MethodPost:
61
		h.addPattern(w, r)
62
	case r.URL.Path == "/-/admin/patterns/delete" && r.Method == http.MethodPost:
63
		h.deletePattern(w, r)
64
	default:
65
		http.NotFound(w, r)
66
	}
67
}
68
69
func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) {
70
	modules, _ := h.store.ListModules()
71
	patterns, _ := h.store.ListPatterns()
72
73
	data := map[string]any{
74
		"ModuleCount":  len(modules),
75
		"PatternCount": len(patterns),
76
		"Version":      h.version,
77
	}
78
79
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
80
	if err := h.pages["dashboard.html"].ExecuteTemplate(w, "base", data); err != nil {
81
		log.Printf("admin: render dashboard: %v", err)
82
	}
83
}
84
85
func (h *Handler) listModules(w http.ResponseWriter, r *http.Request) {
86
	modules, err := h.store.ListModules()
87
	if err != nil {
88
		http.Error(w, "internal error", http.StatusInternalServerError)
89
		return
90
	}
91
92
	data := map[string]any{
93
		"Modules": modules,
94
		"Flash":   r.URL.Query().Get("msg"),
95
		"Error":   r.URL.Query().Get("err"),
96
		"Version": h.version,
97
	}
98
99
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
100
	if err := h.pages["modules.html"].ExecuteTemplate(w, "base", data); err != nil {
101
		log.Printf("admin: render modules: %v", err)
102
	}
103
}
104
105
func (h *Handler) addModule(w http.ResponseWriter, r *http.Request) {
106
	if err := r.ParseForm(); err != nil {
107
		http.Error(w, "bad request", http.StatusBadRequest)
108
		return
109
	}
110
111
	m := store.ModuleRow{
112
		Name:    r.FormValue("name"),
113
		VCS:     r.FormValue("vcs"),
114
		Repo:    r.FormValue("repo"),
115
		Web:     r.FormValue("web"),
116
		Private: r.FormValue("private") == "on",
117
	}
118
119
	if m.Name == "" || m.Repo == "" {
120
		http.Redirect(w, r, "/-/admin/modules?err=Name+and+repo+are+required", http.StatusSeeOther)
121
		return
122
	}
123
124
	if m.VCS == "" {
125
		m.VCS = "git"
126
	}
127
128
	// Verify the repository is accessible.
129
	if err := store.VerifyModule(m.Repo); err != nil {
130
		log.Printf("admin: verify module %s: %v", m.Name, err)
131
		http.Redirect(w, r, "/-/admin/modules?err=Repository+verification+failed:+"+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
132
		return
133
	}
134
135
	if err := h.store.AddModule(m); err != nil {
136
		log.Printf("admin: add module %s: %v", m.Name, err)
137
		http.Redirect(w, r, "/-/admin/modules?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
138
		return
139
	}
140
141
	http.Redirect(w, r, "/-/admin/modules?msg=Module+"+m.Name+"+added", http.StatusSeeOther)
142
}
143
144
func (h *Handler) deleteModule(w http.ResponseWriter, r *http.Request) {
145
	name := r.FormValue("name")
146
	if name == "" {
147
		http.Redirect(w, r, "/-/admin/modules?err=Name+required", http.StatusSeeOther)
148
		return
149
	}
150
151
	if err := h.store.DeleteModule(name); err != nil {
152
		log.Printf("admin: delete module %s: %v", name, err)
153
		http.Redirect(w, r, "/-/admin/modules?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
154
		return
155
	}
156
157
	http.Redirect(w, r, "/-/admin/modules?msg=Module+"+name+"+deleted", http.StatusSeeOther)
158
}
159
160
func (h *Handler) listPatterns(w http.ResponseWriter, r *http.Request) {
161
	patterns, err := h.store.ListPatterns()
162
	if err != nil {
163
		http.Error(w, "internal error", http.StatusInternalServerError)
164
		return
165
	}
166
167
	data := map[string]any{
168
		"Patterns": patterns,
169
		"Flash":    r.URL.Query().Get("msg"),
170
		"Error":    r.URL.Query().Get("err"),
171
		"Version":  h.version,
172
	}
173
174
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
175
	if err := h.pages["patterns.html"].ExecuteTemplate(w, "base", data); err != nil {
176
		log.Printf("admin: render patterns: %v", err)
177
	}
178
}
179
180
func (h *Handler) addPattern(w http.ResponseWriter, r *http.Request) {
181
	if err := r.ParseForm(); err != nil {
182
		http.Error(w, "bad request", http.StatusBadRequest)
183
		return
184
	}
185
186
	p := store.PatternRow{
187
		Pattern: r.FormValue("pattern"),
188
		VCS:     r.FormValue("vcs"),
189
		Repo:    r.FormValue("repo"),
190
		Web:     r.FormValue("web"),
191
		Private: r.FormValue("private") == "on",
192
	}
193
194
	if p.Pattern == "" || p.Repo == "" {
195
		http.Redirect(w, r, "/-/admin/patterns?err=Pattern+and+repo+template+are+required", http.StatusSeeOther)
196
		return
197
	}
198
199
	if p.VCS == "" {
200
		p.VCS = "git"
201
	}
202
203
	// Verify the regex compiles.
204
	if err := store.VerifyPattern(p.Pattern); err != nil {
205
		log.Printf("admin: verify pattern %s: %v", p.Pattern, err)
206
		http.Redirect(w, r, "/-/admin/patterns?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
207
		return
208
	}
209
210
	if err := h.store.AddPattern(p); err != nil {
211
		log.Printf("admin: add pattern %s: %v", p.Pattern, err)
212
		http.Redirect(w, r, "/-/admin/patterns?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
213
		return
214
	}
215
216
	http.Redirect(w, r, "/-/admin/patterns?msg=Pattern+added", http.StatusSeeOther)
217
}
218
219
func (h *Handler) deletePattern(w http.ResponseWriter, r *http.Request) {
220
	idStr := r.FormValue("id")
221
	if idStr == "" {
222
		http.Redirect(w, r, "/-/admin/patterns?err=ID+required", http.StatusSeeOther)
223
		return
224
	}
225
226
	var id int64
227
	if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
228
		http.Redirect(w, r, "/-/admin/patterns?err=Invalid+ID", http.StatusSeeOther)
229
		return
230
	}
231
232
	if err := h.store.DeletePattern(id); err != nil {
233
		log.Printf("admin: delete pattern %d: %v", id, err)
234
		http.Redirect(w, r, "/-/admin/patterns?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
235
		return
236
	}
237
238
	http.Redirect(w, r, "/-/admin/patterns?msg=Pattern+deleted", http.StatusSeeOther)
239
}
240

Source Files