api.go

v1.3.2
Doc Versions Source
1
package admin
2
3
import (
4
	"database/sql"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"log"
9
	"net/http"
10
	"strconv"
11
	"strings"
12
13
	"github.com/go-git/go-git/v6/plumbing/transport"
14
15
	"go.bigb.es/curator/internal/git"
16
	"go.bigb.es/curator/internal/store"
17
)
18
19
// APIHandler serves the REST API for module management.
20
type APIHandler struct {
21
	store *store.Store
22
}
23
24
// NewAPI creates a new API handler.
25
func NewAPI(st *store.Store) *APIHandler {
26
	return &APIHandler{store: st}
27
}
28
29
// ServeHTTP routes API requests.
30
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
31
	path := strings.TrimPrefix(r.URL.Path, "/-/api")
32
33
	switch {
34
	case path == "/modules" && r.Method == http.MethodGet:
35
		h.listModules(w, r)
36
	case path == "/modules" && r.Method == http.MethodPost:
37
		h.addModule(w, r)
38
	case strings.HasPrefix(path, "/modules/") && r.Method == http.MethodDelete:
39
		name := strings.TrimPrefix(path, "/modules/")
40
		h.deleteModule(w, r, name)
41
	case path == "/patterns" && r.Method == http.MethodGet:
42
		h.listPatterns(w, r)
43
	case path == "/patterns" && r.Method == http.MethodPost:
44
		h.addPattern(w, r)
45
	case strings.HasPrefix(path, "/patterns/") && r.Method == http.MethodDelete:
46
		idStr := strings.TrimPrefix(path, "/patterns/")
47
		h.deletePattern(w, r, idStr)
48
	case path == "/credentials" && r.Method == http.MethodGet:
49
		h.apiListCredentials(w, r)
50
	case path == "/credentials" && r.Method == http.MethodPost:
51
		h.apiAddCredential(w, r)
52
	case strings.HasPrefix(path, "/credentials/") && r.Method == http.MethodPut:
53
		name := strings.TrimPrefix(path, "/credentials/")
54
		h.apiUpdateCredential(w, r, name)
55
	case strings.HasPrefix(path, "/credentials/") && r.Method == http.MethodDelete:
56
		name := strings.TrimPrefix(path, "/credentials/")
57
		h.apiDeleteCredential(w, r, name)
58
	default:
59
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
60
	}
61
}
62
63
func (h *APIHandler) listModules(w http.ResponseWriter, r *http.Request) {
64
	modules, err := h.store.ListModules()
65
	if err != nil {
66
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
67
		return
68
	}
69
	if modules == nil {
70
		modules = []store.ModuleRow{}
71
	}
72
	writeJSON(w, http.StatusOK, modules)
73
}
74
75
func (h *APIHandler) addModule(w http.ResponseWriter, r *http.Request) {
76
	var m store.ModuleRow
77
	if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
78
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
79
		return
80
	}
81
82
	if m.Name == "" || m.Repo == "" {
83
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "name and repo are required"})
84
		return
85
	}
86
87
	if m.VCS == "" {
88
		m.VCS = "git"
89
	}
90
91
	// Verify the repository.
92
	auth := h.authForCredential(m.CredentialName)
93
	if err := store.VerifyModule(m.Repo, auth); err != nil {
94
		log.Printf("api: verify module %s: %v", m.Name, err)
95
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{
96
			"error": fmt.Sprintf("repository verification failed: %v", err),
97
		})
98
		return
99
	}
100
101
	if err := h.store.AddModule(m); err != nil {
102
		code := http.StatusInternalServerError
103
		if strings.Contains(err.Error(), "UNIQUE constraint") {
104
			code = http.StatusConflict
105
		}
106
		writeJSON(w, code, map[string]string{"error": err.Error()})
107
		return
108
	}
109
110
	writeJSON(w, http.StatusCreated, m)
111
}
112
113
func (h *APIHandler) deleteModule(w http.ResponseWriter, r *http.Request, name string) {
114
	if name == "" {
115
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
116
		return
117
	}
118
119
	if err := h.store.DeleteModule(name); err != nil {
120
		if errors.Is(err, sql.ErrNoRows) {
121
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
122
			return
123
		}
124
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
125
		return
126
	}
127
128
	w.WriteHeader(http.StatusNoContent)
129
}
130
131
func (h *APIHandler) listPatterns(w http.ResponseWriter, r *http.Request) {
132
	patterns, err := h.store.ListPatterns()
133
	if err != nil {
134
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
135
		return
136
	}
137
	if patterns == nil {
138
		patterns = []store.PatternRow{}
139
	}
140
	writeJSON(w, http.StatusOK, patterns)
141
}
142
143
func (h *APIHandler) addPattern(w http.ResponseWriter, r *http.Request) {
144
	var p store.PatternRow
145
	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
146
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
147
		return
148
	}
149
150
	if p.Pattern == "" || p.Repo == "" {
151
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "pattern and repo are required"})
152
		return
153
	}
154
155
	if p.VCS == "" {
156
		p.VCS = "git"
157
	}
158
159
	if err := store.VerifyPattern(p.Pattern); err != nil {
160
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()})
161
		return
162
	}
163
164
	if err := h.store.AddPattern(p); err != nil {
165
		code := http.StatusInternalServerError
166
		if strings.Contains(err.Error(), "UNIQUE constraint") {
167
			code = http.StatusConflict
168
		}
169
		writeJSON(w, code, map[string]string{"error": err.Error()})
170
		return
171
	}
172
173
	writeJSON(w, http.StatusCreated, p)
174
}
175
176
func (h *APIHandler) deletePattern(w http.ResponseWriter, r *http.Request, idStr string) {
177
	id, err := strconv.ParseInt(idStr, 10, 64)
178
	if err != nil {
179
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid ID"})
180
		return
181
	}
182
183
	if err := h.store.DeletePattern(id); err != nil {
184
		if errors.Is(err, sql.ErrNoRows) {
185
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
186
			return
187
		}
188
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
189
		return
190
	}
191
192
	w.WriteHeader(http.StatusNoContent)
193
}
194
195
func (h *APIHandler) apiListCredentials(w http.ResponseWriter, r *http.Request) {
196
	creds, err := h.store.ListCredentials()
197
	if err != nil {
198
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
199
		return
200
	}
201
	if creds == nil {
202
		creds = []store.CredentialRow{}
203
	}
204
	// Strip data from response for safety.
205
	for i := range creds {
206
		creds[i].Data = ""
207
	}
208
	writeJSON(w, http.StatusOK, creds)
209
}
210
211
func (h *APIHandler) apiAddCredential(w http.ResponseWriter, r *http.Request) {
212
	var c store.CredentialRow
213
	if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
214
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
215
		return
216
	}
217
218
	if c.Name == "" || c.Type == "" || c.Data == "" {
219
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "name, type, and data are required"})
220
		return
221
	}
222
223
	switch c.Type {
224
	case "basic", "token", "ssh":
225
		// valid
226
	default:
227
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "type must be basic, token, or ssh"})
228
		return
229
	}
230
231
	if err := h.store.AddCredential(c); err != nil {
232
		code := http.StatusInternalServerError
233
		if strings.Contains(err.Error(), "UNIQUE constraint") {
234
			code = http.StatusConflict
235
		}
236
		writeJSON(w, code, map[string]string{"error": err.Error()})
237
		return
238
	}
239
240
	c.Data = "" // don't echo back
241
	writeJSON(w, http.StatusCreated, c)
242
}
243
244
func (h *APIHandler) apiUpdateCredential(w http.ResponseWriter, r *http.Request, name string) {
245
	if name == "" {
246
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
247
		return
248
	}
249
250
	var c store.CredentialRow
251
	if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
252
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
253
		return
254
	}
255
256
	c.Name = name
257
	if c.Type == "" || c.Data == "" {
258
		writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "type and data are required"})
259
		return
260
	}
261
262
	if err := h.store.UpdateCredential(c); err != nil {
263
		if errors.Is(err, sql.ErrNoRows) {
264
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
265
			return
266
		}
267
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
268
		return
269
	}
270
271
	c.Data = ""
272
	writeJSON(w, http.StatusOK, c)
273
}
274
275
func (h *APIHandler) apiDeleteCredential(w http.ResponseWriter, r *http.Request, name string) {
276
	if name == "" {
277
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name required"})
278
		return
279
	}
280
281
	if err := h.store.DeleteCredential(name); err != nil {
282
		if errors.Is(err, sql.ErrNoRows) {
283
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
284
			return
285
		}
286
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
287
		return
288
	}
289
290
	w.WriteHeader(http.StatusNoContent)
291
}
292
293
// authForCredential looks up a credential by name and builds an AuthMethod.
294
func (h *APIHandler) authForCredential(name string) transport.AuthMethod {
295
	if name == "" {
296
		return nil
297
	}
298
	cred, err := h.store.GetCredential(name)
299
	if err != nil {
300
		return nil
301
	}
302
	auth, err := git.AuthMethodFromCredential(cred.Type, cred.Data)
303
	if err != nil {
304
		return nil
305
	}
306
	return auth
307
}
308
309
func writeJSON(w http.ResponseWriter, code int, v any) {
310
	w.Header().Set("Content-Type", "application/json")
311
	w.WriteHeader(code)
312
	json.NewEncoder(w).Encode(v)
313
}
314

Source Files