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) }