package serve import ( "axolotl/models" "axolotl/service" "axolotl/store" "encoding/json" "net/http" "strconv" "strings" ) // New returns an HTTP handler that exposes NodeService as a JSON API. // When oidcCfg is non-nil, every request must carry a valid Bearer token; // the authenticated username is derived from the token claim configured in // OIDCConfig.UserClaim. Without OIDC, the X-Ax-User header is used instead. func New(newSvc func(user string) (service.NodeService, error), oidcCfg *store.OIDCConfig) (http.Handler, error) { s := &server{newSvc: newSvc} mux := http.NewServeMux() mux.HandleFunc("GET /nodes", s.listNodes) mux.HandleFunc("POST /nodes", s.addNode) mux.HandleFunc("GET /nodes/{id}", s.getNode) mux.HandleFunc("PATCH /nodes/{id}", s.updateNode) mux.HandleFunc("DELETE /nodes/{id}", s.deleteNode) mux.HandleFunc("GET /users", s.listUsers) mux.HandleFunc("POST /users", s.addUser) if oidcCfg != nil { ah, err := newAuthHandler(*oidcCfg) if err != nil { return nil, err } mux.HandleFunc("POST /auth/start", ah.start) mux.HandleFunc("GET /auth/callback", ah.callback) mux.HandleFunc("GET /auth/poll", ah.poll) return withSessionAuth(ah, mux), nil } return mux, nil } type server struct { newSvc func(user string) (service.NodeService, error) } func (s *server) svc(w http.ResponseWriter, r *http.Request) (service.NodeService, bool) { user := userFromContext(r) if user == "" { user = r.Header.Get("X-Ax-User") } if user == "" { writeError(w, http.StatusUnauthorized, "X-Ax-User header required") return nil, false } svc, err := s.newSvc(user) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return nil, false } return svc, true } func (s *server) listNodes(w http.ResponseWriter, r *http.Request) { svc, ok := s.svc(w, r) if !ok { return } q := r.URL.Query() var filter service.ListFilter for _, tag := range q["tag"] { filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelType(tag)}) } for _, rel := range q["rel"] { filter.Rels = append(filter.Rels, parseRel(rel)) } for k, prefix := range map[string]string{"type": "_type::", "status": "_status::", "prio": "_prio::"} { if v := q.Get(k); v != "" { filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelType(prefix + v)}) } } if v := q.Get("namespace"); v != "" { filter.Namespace = v } if v := q.Get("assignee"); v != "" { filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelAssignee, Target: v}) } if v := q.Get("mention"); v != "" { filter.Rels = append(filter.Rels, service.RelInput{Type: models.RelMentions, Target: v}) } if q.Get("has_due_date") == "true" { filter.HasDueDate = true } if v := q.Get("due_within"); v != "" { if n, err := strconv.Atoi(v); err == nil { filter.DueWithin = &n } } nodes, err := svc.List(filter) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, nodes) } func (s *server) addNode(w http.ResponseWriter, r *http.Request) { svc, ok := s.svc(w, r) if !ok { return } var input service.AddInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } n, err := svc.Add(input) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } w.WriteHeader(http.StatusCreated) writeJSON(w, n) } func (s *server) getNode(w http.ResponseWriter, r *http.Request) { svc, ok := s.svc(w, r) if !ok { return } n, err := svc.GetByID(r.PathValue("id")) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, n) } func (s *server) updateNode(w http.ResponseWriter, r *http.Request) { svc, ok := s.svc(w, r) if !ok { return } var input service.UpdateInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } n, err := svc.Update(r.PathValue("id"), input) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, n) } func (s *server) deleteNode(w http.ResponseWriter, r *http.Request) { svc, ok := s.svc(w, r) if !ok { return } if err := svc.Delete(r.PathValue("id")); err != nil { writeError(w, http.StatusNotFound, err.Error()) return } w.WriteHeader(http.StatusNoContent) } func (s *server) listUsers(w http.ResponseWriter, r *http.Request) { svc, ok := s.svc(w, r) if !ok { return } users, err := svc.ListUsers() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, users) } func (s *server) addUser(w http.ResponseWriter, r *http.Request) { svc, ok := s.svc(w, r) if !ok { return } var body struct { Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } n, err := svc.AddUser(body.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } w.WriteHeader(http.StatusCreated) writeJSON(w, n) } func parseRel(s string) service.RelInput { if strings.Contains(s, "::") { return service.RelInput{Type: models.RelType(s)} } if before, after, found := strings.Cut(s, ":"); found { return service.RelInput{Type: models.RelType(before), Target: after} } return service.RelInput{Type: models.RelType(s)} } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, code int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) json.NewEncoder(w).Encode(map[string]string{"error": msg}) }