Some checks failed
CI / build-and-test (push) Failing after -32m0s
* Add OAuth login handling in app and UI components * Create new components for login and dashboard pages * Refactor sidebar and navigation structure * Introduce types for access entries and status responses
269 lines
6.6 KiB
Go
269 lines
6.6 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/amcs/internal/store"
|
|
ext "git.warky.dev/wdevs/amcs/internal/types"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type adminHandlers struct {
|
|
db *store.DB
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func newAdminHandlers(db *store.DB, logger *slog.Logger) *adminHandlers {
|
|
return &adminHandlers{db: db, logger: logger}
|
|
}
|
|
|
|
func (h *adminHandlers) register(mux *http.ServeMux, middleware func(http.Handler) http.Handler) {
|
|
handle := func(pattern string, fn http.HandlerFunc) {
|
|
mux.Handle(pattern, middleware(fn))
|
|
}
|
|
|
|
handle("GET /api/admin/projects", h.listProjects)
|
|
handle("POST /api/admin/projects", h.createProject)
|
|
handle("GET /api/admin/thoughts", h.listThoughts)
|
|
handle("GET /api/admin/thoughts/{id}", h.getThought)
|
|
handle("DELETE /api/admin/thoughts/{id}", h.deleteThought)
|
|
handle("POST /api/admin/thoughts/{id}/archive", h.archiveThought)
|
|
handle("GET /api/admin/skills", h.listSkills)
|
|
handle("DELETE /api/admin/skills/{id}", h.deleteSkill)
|
|
handle("GET /api/admin/guardrails", h.listGuardrails)
|
|
handle("DELETE /api/admin/guardrails/{id}", h.deleteGuardrail)
|
|
handle("GET /api/admin/files", h.listFiles)
|
|
handle("GET /api/admin/stats", h.stats)
|
|
}
|
|
|
|
// --- Projects ---
|
|
|
|
func (h *adminHandlers) listProjects(w http.ResponseWriter, r *http.Request) {
|
|
projects, err := h.db.ListProjects(r.Context())
|
|
if err != nil {
|
|
h.internalError(w, "list projects", err)
|
|
return
|
|
}
|
|
writeJSON(w, projects)
|
|
}
|
|
|
|
func (h *adminHandlers) createProject(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if strings.TrimSpace(body.Name) == "" {
|
|
http.Error(w, "name is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
project, err := h.db.CreateProject(r.Context(), body.Name, body.Description)
|
|
if err != nil {
|
|
h.internalError(w, "create project", err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
writeJSON(w, project)
|
|
}
|
|
|
|
// --- Thoughts ---
|
|
|
|
func (h *adminHandlers) listThoughts(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
limit := 50
|
|
if l := q.Get("limit"); l != "" {
|
|
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
|
limit = min(n, 200)
|
|
}
|
|
}
|
|
|
|
query := strings.TrimSpace(q.Get("q"))
|
|
includeArchived := q.Get("include_archived") == "true"
|
|
|
|
var projectID *uuid.UUID
|
|
if pid := q.Get("project_id"); pid != "" {
|
|
if id, err := uuid.Parse(pid); err == nil {
|
|
projectID = &id
|
|
}
|
|
}
|
|
|
|
if query != "" {
|
|
results, err := h.db.SearchThoughtsText(r.Context(), query, limit, projectID, nil)
|
|
if err != nil {
|
|
h.internalError(w, "search thoughts", err)
|
|
return
|
|
}
|
|
writeJSON(w, results)
|
|
return
|
|
}
|
|
|
|
thoughts, err := h.db.ListThoughts(r.Context(), ext.ListFilter{
|
|
Limit: limit,
|
|
ProjectID: projectID,
|
|
IncludeArchived: includeArchived,
|
|
})
|
|
if err != nil {
|
|
h.internalError(w, "list thoughts", err)
|
|
return
|
|
}
|
|
writeJSON(w, thoughts)
|
|
}
|
|
|
|
func (h *adminHandlers) getThought(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseUUID(w, r.PathValue("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
thought, err := h.db.GetThought(r.Context(), id)
|
|
if err != nil {
|
|
h.internalError(w, "get thought", err)
|
|
return
|
|
}
|
|
writeJSON(w, thought)
|
|
}
|
|
|
|
func (h *adminHandlers) deleteThought(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseUUID(w, r.PathValue("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.db.DeleteThought(r.Context(), id); err != nil {
|
|
h.internalError(w, "delete thought", err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *adminHandlers) archiveThought(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseUUID(w, r.PathValue("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.db.ArchiveThought(r.Context(), id); err != nil {
|
|
h.internalError(w, "archive thought", err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// --- Skills ---
|
|
|
|
func (h *adminHandlers) listSkills(w http.ResponseWriter, r *http.Request) {
|
|
tag := r.URL.Query().Get("tag")
|
|
skills, err := h.db.ListSkills(r.Context(), tag)
|
|
if err != nil {
|
|
h.internalError(w, "list skills", err)
|
|
return
|
|
}
|
|
writeJSON(w, skills)
|
|
}
|
|
|
|
func (h *adminHandlers) deleteSkill(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseUUID(w, r.PathValue("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.db.RemoveSkill(r.Context(), id); err != nil {
|
|
h.internalError(w, "delete skill", err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// --- Guardrails ---
|
|
|
|
func (h *adminHandlers) listGuardrails(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
guardrails, err := h.db.ListGuardrails(r.Context(), q.Get("tag"), q.Get("severity"))
|
|
if err != nil {
|
|
h.internalError(w, "list guardrails", err)
|
|
return
|
|
}
|
|
writeJSON(w, guardrails)
|
|
}
|
|
|
|
func (h *adminHandlers) deleteGuardrail(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseUUID(w, r.PathValue("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.db.RemoveGuardrail(r.Context(), id); err != nil {
|
|
h.internalError(w, "delete guardrail", err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// --- Files ---
|
|
|
|
func (h *adminHandlers) listFiles(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
limit := 100
|
|
if l := q.Get("limit"); l != "" {
|
|
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
|
limit = min(n, 500)
|
|
}
|
|
}
|
|
|
|
filter := ext.StoredFileFilter{Limit: limit}
|
|
if pid := q.Get("project_id"); pid != "" {
|
|
if id, err := uuid.Parse(pid); err == nil {
|
|
filter.ProjectID = &id
|
|
}
|
|
}
|
|
if tid := q.Get("thought_id"); tid != "" {
|
|
if id, err := uuid.Parse(tid); err == nil {
|
|
filter.ThoughtID = &id
|
|
}
|
|
}
|
|
filter.Kind = q.Get("kind")
|
|
|
|
files, err := h.db.ListStoredFiles(r.Context(), filter)
|
|
if err != nil {
|
|
h.internalError(w, "list files", err)
|
|
return
|
|
}
|
|
writeJSON(w, files)
|
|
}
|
|
|
|
// --- Stats ---
|
|
|
|
func (h *adminHandlers) stats(w http.ResponseWriter, r *http.Request) {
|
|
stats, err := h.db.Stats(r.Context())
|
|
if err != nil {
|
|
h.internalError(w, "stats", err)
|
|
return
|
|
}
|
|
writeJSON(w, stats)
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func (h *adminHandlers) internalError(w http.ResponseWriter, op string, err error) {
|
|
h.logger.Error("admin handler error", slog.String("op", op), slog.String("error", err.Error()))
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func parseUUID(w http.ResponseWriter, s string) (uuid.UUID, bool) {
|
|
id, err := uuid.Parse(s)
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return uuid.UUID{}, false
|
|
}
|
|
return id, true
|
|
}
|
|
|