package httpapi import ( "encoding/json" "net/http" "strings" "time" "evobgp/internal/store" ) func (s *Server) registerAPIKeyRoutes(m *http.ServeMux) { m.HandleFunc("GET /auth/session", s.handleAuthSession) m.HandleFunc("GET /api-keys", s.handleListAPIKeys) m.HandleFunc("POST /api-keys", s.handlePostAPIKey) m.HandleFunc("GET /api-keys/{id}", s.handleGetAPIKey) m.HandleFunc("PATCH /api-keys/{id}", s.handlePatchAPIKey) m.HandleFunc("DELETE /api-keys/{id}", s.handleDeleteAPIKey) m.HandleFunc("POST /api-keys/{id}/rotate", s.handleRotateAPIKey) } func (s *Server) handleAuthSession(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requireAtLeast(w, a, "viewer") { return } writeJSON(w, http.StatusOK, map[string]any{ "tenant_id": a.TenantID, "role": a.Role, }) } func apiKeyJSON(k *store.APIKey) map[string]any { m := map[string]any{ "id": k.ID, "name": k.Name, "role": k.Role, "prefix": k.Prefix, "created_at": k.CreatedAt.UTC().Format(time.RFC3339), "updated_at": k.UpdatedAt.UTC().Format(time.RFC3339), } if k.ExpiresAt != nil { m["expires_at"] = k.ExpiresAt.UTC().Format(time.RFC3339) } else { m["expires_at"] = nil } if k.RevokedAt != nil { m["revoked_at"] = k.RevokedAt.UTC().Format(time.RFC3339) } else { m["revoked_at"] = nil } if k.LastUsedAt != nil { m["last_used_at"] = k.LastUsedAt.UTC().Format(time.RFC3339) } else { m["last_used_at"] = nil } return m } func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requireAtLeast(w, a, "operator") { return } list, err := s.store.ListAPIKeys(a.TenantID) if err != nil { writeStoreErr(w, err) return } writePaginatedListJSON(w, r, list, func(k *store.APIKey) map[string]any { return apiKeyJSON(k) }) } func (s *Server) handleGetAPIKey(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requireAtLeast(w, a, "operator") { return } k, err := s.store.GetAPIKey(a.TenantID, r.PathValue("id")) if err != nil { writeStoreErr(w, err) return } writeJSON(w, http.StatusOK, apiKeyJSON(k)) } func (s *Server) handlePostAPIKey(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requireAtLeast(w, a, "operator") { return } var body struct { Name string `json:"name"` Role string `json:"role"` ExpiresAt *string `json:"expires_at"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json") return } in := &store.APIKeyCreate{ Name: strings.TrimSpace(body.Name), Role: strings.TrimSpace(body.Role), } if body.ExpiresAt != nil && strings.TrimSpace(*body.ExpiresAt) != "" { t, err := time.Parse(time.RFC3339, strings.TrimSpace(*body.ExpiresAt)) if err != nil { writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid expires_at") return } in.ExpiresAt = &t } created, err := s.store.CreateAPIKey(a.TenantID, in) if err != nil { writeStoreErr(w, err) return } if err := s.keyResolver.Reload(s.store); err != nil { writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys") return } out := apiKeyJSON(&created.APIKey) out["token"] = created.Token writeJSON(w, http.StatusCreated, out) } func (s *Server) handlePatchAPIKey(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requireAtLeast(w, a, "operator") { return } var raw map[string]json.RawMessage if err := json.NewDecoder(r.Body).Decode(&raw); err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json") return } patch := &store.APIKeyPatch{} if v, ok := raw["name"]; ok { var name string if err := json.Unmarshal(v, &name); err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid name") return } patch.Name = &name } if v, ok := raw["role"]; ok { var role string if err := json.Unmarshal(v, &role); err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid role") return } patch.Role = &role } if v, ok := raw["expires_at"]; ok { if string(v) == "null" { patch.ClearExpiresAt = true } else { var s string if err := json.Unmarshal(v, &s); err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid expires_at") return } t, err := time.Parse(time.RFC3339, strings.TrimSpace(s)) if err != nil { writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid expires_at") return } patch.ExpiresAt = &t } } k, err := s.store.UpdateAPIKey(a.TenantID, r.PathValue("id"), patch) if err != nil { writeStoreErr(w, err) return } if err := s.keyResolver.Reload(s.store); err != nil { writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys") return } writeJSON(w, http.StatusOK, apiKeyJSON(k)) } func (s *Server) handleDeleteAPIKey(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requireAtLeast(w, a, "operator") { return } if err := s.store.RevokeAPIKey(a.TenantID, r.PathValue("id")); err != nil { writeStoreErr(w, err) return } if err := s.keyResolver.Reload(s.store); err != nil { writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys") return } w.WriteHeader(http.StatusNoContent) } func (s *Server) handleRotateAPIKey(w http.ResponseWriter, r *http.Request) { a, ok := authFromContext(r.Context()) if !ok || !s.requireAtLeast(w, a, "operator") { return } rotated, err := s.store.RotateAPIKey(a.TenantID, r.PathValue("id")) if err != nil { writeStoreErr(w, err) return } if err := s.keyResolver.Reload(s.store); err != nil { writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys") return } out := apiKeyJSON(&rotated.APIKey) out["token"] = rotated.Token writeJSON(w, http.StatusOK, out) }