admin.go

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

Source Files