admin.go

v1.0.1
Doc Versions Source
1
package admin
2
3
import (
4
	"embed"
5
	"fmt"
6
	"html/template"
7
	"log"
8
	"net/http"
9
	"time"
10
11
	"example.com/curator/internal/store"
12
)
13
14
//go:embed templates/*
15
var content embed.FS
16
17
// Handler serves the admin UI.
18
type Handler struct {
19
	store *store.Store
20
	pages map[string]*template.Template
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) (*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}, 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
	}
77
78
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
79
	if err := h.pages["dashboard.html"].ExecuteTemplate(w, "base", data); err != nil {
80
		log.Printf("admin: render dashboard: %v", err)
81
	}
82
}
83
84
func (h *Handler) listModules(w http.ResponseWriter, r *http.Request) {
85
	modules, err := h.store.ListModules()
86
	if err != nil {
87
		http.Error(w, "internal error", http.StatusInternalServerError)
88
		return
89
	}
90
91
	data := map[string]any{
92
		"Modules": modules,
93
		"Flash":   r.URL.Query().Get("msg"),
94
		"Error":   r.URL.Query().Get("err"),
95
	}
96
97
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
98
	if err := h.pages["modules.html"].ExecuteTemplate(w, "base", data); err != nil {
99
		log.Printf("admin: render modules: %v", err)
100
	}
101
}
102
103
func (h *Handler) addModule(w http.ResponseWriter, r *http.Request) {
104
	if err := r.ParseForm(); err != nil {
105
		http.Error(w, "bad request", http.StatusBadRequest)
106
		return
107
	}
108
109
	m := store.ModuleRow{
110
		Name:    r.FormValue("name"),
111
		VCS:     r.FormValue("vcs"),
112
		Repo:    r.FormValue("repo"),
113
		Web:     r.FormValue("web"),
114
		Private: r.FormValue("private") == "on",
115
	}
116
117
	if m.Name == "" || m.Repo == "" {
118
		http.Redirect(w, r, "/-/admin/modules?err=Name+and+repo+are+required", http.StatusSeeOther)
119
		return
120
	}
121
122
	if m.VCS == "" {
123
		m.VCS = "git"
124
	}
125
126
	// Verify the repository is accessible.
127
	if err := store.VerifyModule(m.Repo, 10*time.Second); err != nil {
128
		log.Printf("admin: verify module %s: %v", m.Name, err)
129
		http.Redirect(w, r, "/-/admin/modules?err=Repository+verification+failed:+"+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
130
		return
131
	}
132
133
	if err := h.store.AddModule(m); err != nil {
134
		log.Printf("admin: add module %s: %v", m.Name, err)
135
		http.Redirect(w, r, "/-/admin/modules?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
136
		return
137
	}
138
139
	http.Redirect(w, r, "/-/admin/modules?msg=Module+"+m.Name+"+added", http.StatusSeeOther)
140
}
141
142
func (h *Handler) deleteModule(w http.ResponseWriter, r *http.Request) {
143
	name := r.FormValue("name")
144
	if name == "" {
145
		http.Redirect(w, r, "/-/admin/modules?err=Name+required", http.StatusSeeOther)
146
		return
147
	}
148
149
	if err := h.store.DeleteModule(name); err != nil {
150
		log.Printf("admin: delete module %s: %v", name, err)
151
		http.Redirect(w, r, "/-/admin/modules?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
152
		return
153
	}
154
155
	http.Redirect(w, r, "/-/admin/modules?msg=Module+"+name+"+deleted", http.StatusSeeOther)
156
}
157
158
func (h *Handler) listPatterns(w http.ResponseWriter, r *http.Request) {
159
	patterns, err := h.store.ListPatterns()
160
	if err != nil {
161
		http.Error(w, "internal error", http.StatusInternalServerError)
162
		return
163
	}
164
165
	data := map[string]any{
166
		"Patterns": patterns,
167
		"Flash":    r.URL.Query().Get("msg"),
168
		"Error":    r.URL.Query().Get("err"),
169
	}
170
171
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
172
	if err := h.pages["patterns.html"].ExecuteTemplate(w, "base", data); err != nil {
173
		log.Printf("admin: render patterns: %v", err)
174
	}
175
}
176
177
func (h *Handler) addPattern(w http.ResponseWriter, r *http.Request) {
178
	if err := r.ParseForm(); err != nil {
179
		http.Error(w, "bad request", http.StatusBadRequest)
180
		return
181
	}
182
183
	p := store.PatternRow{
184
		Pattern: r.FormValue("pattern"),
185
		VCS:     r.FormValue("vcs"),
186
		Repo:    r.FormValue("repo"),
187
		Web:     r.FormValue("web"),
188
		Private: r.FormValue("private") == "on",
189
	}
190
191
	if p.Pattern == "" || p.Repo == "" {
192
		http.Redirect(w, r, "/-/admin/patterns?err=Pattern+and+repo+template+are+required", http.StatusSeeOther)
193
		return
194
	}
195
196
	if p.VCS == "" {
197
		p.VCS = "git"
198
	}
199
200
	// Verify the regex compiles.
201
	if err := store.VerifyPattern(p.Pattern); err != nil {
202
		log.Printf("admin: verify pattern %s: %v", p.Pattern, err)
203
		http.Redirect(w, r, "/-/admin/patterns?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
204
		return
205
	}
206
207
	if err := h.store.AddPattern(p); err != nil {
208
		log.Printf("admin: add pattern %s: %v", p.Pattern, err)
209
		http.Redirect(w, r, "/-/admin/patterns?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
210
		return
211
	}
212
213
	http.Redirect(w, r, "/-/admin/patterns?msg=Pattern+added", http.StatusSeeOther)
214
}
215
216
func (h *Handler) deletePattern(w http.ResponseWriter, r *http.Request) {
217
	idStr := r.FormValue("id")
218
	if idStr == "" {
219
		http.Redirect(w, r, "/-/admin/patterns?err=ID+required", http.StatusSeeOther)
220
		return
221
	}
222
223
	var id int64
224
	if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
225
		http.Redirect(w, r, "/-/admin/patterns?err=Invalid+ID", http.StatusSeeOther)
226
		return
227
	}
228
229
	if err := h.store.DeletePattern(id); err != nil {
230
		log.Printf("admin: delete pattern %d: %v", id, err)
231
		http.Redirect(w, r, "/-/admin/patterns?err="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther)
232
		return
233
	}
234
235
	http.Redirect(w, r, "/-/admin/patterns?msg=Pattern+deleted", http.StatusSeeOther)
236
}
237

Source Files