Files
styx/admin/admin.go
2025-10-06 22:25:23 +02:00

147 lines
3.4 KiB
Go

package admin
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"strconv"
"sync"
"git.maze.io/maze/styx/dataset"
"git.maze.io/maze/styx/logger"
"git.maze.io/maze/styx/proxy"
)
type Admin struct {
Storage dataset.Storage
setupOnce sync.Once
mux *http.ServeMux
api *http.ServeMux
}
type apiError struct {
Code int
Err error
}
func (err apiError) Error() string {
return err.Err.Error()
}
func (a *Admin) setup() {
a.mux = http.NewServeMux()
a.api = http.NewServeMux()
a.api.HandleFunc("GET /groups", a.apiGroups)
a.api.HandleFunc("POST /group", a.apiGroupCreate)
a.api.HandleFunc("GET /group/{id}", a.apiGroup)
a.api.HandleFunc("PATCH /group/{id}", a.apiGroupUpdate)
a.api.HandleFunc("DELETE /group/{id}", a.apiGroupDelete)
a.api.HandleFunc("GET /clients", a.apiClients)
a.api.HandleFunc("GET /client/{id}", a.apiClient)
a.api.HandleFunc("POST /client", a.apiClientCreate)
a.api.HandleFunc("PATCH /client/{id}", a.apiClientUpdate)
a.api.HandleFunc("DELETE /client/{id}", a.apiClientDelete)
a.api.HandleFunc("GET /lists", a.apiLists)
a.api.HandleFunc("POST /list", a.apiListCreate)
a.api.HandleFunc("GET /list/{id}", a.apiList)
a.api.HandleFunc("DELETE /list/{id}", a.apiListDelete)
}
type Handler interface {
Handle(pattern string, handler http.Handler)
}
func (a *Admin) Install(handler Handler) {
a.setupOnce.Do(a.setup)
handler.Handle("/api/v1/", http.StripPrefix("/api/v1", a.api))
}
func (a *Admin) handleAPIError(w http.ResponseWriter, r *http.Request, err error) {
code := http.StatusBadRequest
switch {
case dataset.IsNotExist(err):
code = http.StatusNotFound
case os.IsPermission(err):
code = http.StatusForbidden
case errors.Is(err, apiError{}):
if c := err.(apiError).Code; c > 0 {
code = c
}
}
logger.StandardLog.Err(err).Values(logger.Values{
"code": code,
"client": r.RemoteAddr,
"method": r.Method,
"path": r.URL.Path,
}).Warn("Unexpected API error encountered")
var data []byte
if err, ok := err.(apiError); ok {
data, _ = json.Marshal(struct {
Code int `json:"code"`
Error string `json:"error"`
}{code, err.Error()})
} else {
data, _ = json.Marshal(struct {
Code int `json:"code"`
Error string `json:"error"`
}{code, http.StatusText(code)})
}
res := proxy.NewResponse(code, io.NopCloser(bytes.NewReader(data)), r)
res.Header.Set(proxy.HeaderContentType, "application/json")
for k, vv := range res.Header {
if len(vv) >= 1 {
w.Header().Set(k, vv[0])
for _, v := range vv[1:] {
w.Header().Add(k, v)
}
}
}
w.WriteHeader(code)
io.Copy(w, res.Body)
}
func (a *Admin) jsonResponse(w http.ResponseWriter, r *http.Request, value any, codes ...int) {
var (
code = http.StatusNoContent
body io.ReadCloser
size int64
)
if value != nil {
data, err := json.Marshal(value)
if err != nil {
a.handleAPIError(w, r, err)
return
}
code = http.StatusOK
body = io.NopCloser(bytes.NewReader(data))
size = int64(len(data))
}
if len(codes) > 0 {
code = codes[0]
}
res := proxy.NewResponse(code, body, r)
res.Close = true
res.Header.Set(proxy.HeaderContentLength, strconv.FormatInt(size, 10))
res.Header.Set(proxy.HeaderContentType, "application/json")
for k, vv := range res.Header {
if len(vv) >= 1 {
w.Header().Set(k, vv[0])
for _, v := range vv[1:] {
w.Header().Add(k, v)
}
}
}
w.WriteHeader(code)
io.Copy(w, res.Body)
}