package app import ( "fmt" "log/slog" "net/http" "strings" "github.com/bitechdev/ResolveSpec/pkg/resolvespec" "github.com/uptrace/bunrouter" "git.warky.dev/wdevs/amcs/internal/generatedmodels" "git.warky.dev/wdevs/amcs/internal/store" ) func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware func(http.Handler) http.Handler, logger *slog.Logger) error { rs := resolvespec.NewHandlerWithBun(db.Bun()) registerResolveSpecGuards(rs) for _, model := range resolveSpecModels() { if err := rs.RegisterModel(model.schema, model.entity, model.model); err != nil { return fmt.Errorf("register resolvespec model %s.%s: %w", model.schema, model.entity, err) } } rsRouter := bunrouter.New() resolvespec.SetupBunRouterRoutes(rsRouter, rs, nil) rsMount := http.StripPrefix("/api/rs", rsRouter) protectedRSMount := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") { trimmed := strings.TrimRight(r.URL.Path, "/") if trimmed == "" { trimmed = "/" } clone := r.Clone(r.Context()) clone.URL.Path = trimmed if clone.URL.RawPath != "" { clone.URL.RawPath = strings.TrimRight(clone.URL.RawPath, "/") if clone.URL.RawPath == "" { clone.URL.RawPath = "/" } } r = clone } if r.Method == http.MethodOptions { rsMount.ServeHTTP(w, r) return } middleware(rsMount).ServeHTTP(w, r) }) mux.Handle("/api/rs/", protectedRSMount) mux.Handle("/api/rs", http.RedirectHandler("/api/rs/openapi", http.StatusTemporaryRedirect)) if logger != nil { logger.Info("resolvespec admin api enabled", slog.String("prefix", "/api/rs"), slog.Int("models", len(resolveSpecModels())), ) } return nil } func registerResolveSpecGuards(rs *resolvespec.Handler) { mutableByEntity := map[string]map[string]struct{}{ "projects": { "create": {}, "update": {}, "delete": {}, }, "thoughts": { "create": {}, "update": {}, "delete": {}, }, "plans": { "create": {}, "update": {}, "delete": {}, }, "learnings": { "create": {}, "update": {}, "delete": {}, }, "agent_skills": { "create": {}, "update": {}, "delete": {}, }, "agent_guardrails": { "create": {}, "update": {}, "delete": {}, }, "stored_files": { "update": {}, "delete": {}, }, } rs.Hooks().Register(resolvespec.BeforeHandle, func(hookCtx *resolvespec.HookContext) error { switch hookCtx.Operation { case "read", "meta": return nil case "create", "update", "delete": allowedOps, ok := mutableByEntity[hookCtx.Entity] if !ok { hookCtx.Abort = true hookCtx.AbortCode = http.StatusForbidden hookCtx.AbortMessage = fmt.Sprintf("operation %q is not allowed for %s.%s", hookCtx.Operation, hookCtx.Schema, hookCtx.Entity) return fmt.Errorf("forbidden operation") } if _, ok := allowedOps[hookCtx.Operation]; !ok { hookCtx.Abort = true hookCtx.AbortCode = http.StatusForbidden hookCtx.AbortMessage = fmt.Sprintf("operation %q is not allowed for %s.%s", hookCtx.Operation, hookCtx.Schema, hookCtx.Entity) return fmt.Errorf("forbidden operation") } return nil default: hookCtx.Abort = true hookCtx.AbortCode = http.StatusBadRequest hookCtx.AbortMessage = fmt.Sprintf("unsupported operation %q", hookCtx.Operation) return fmt.Errorf("unsupported operation") } }) } type resolveSpecModel struct { schema string entity string model any } func resolveSpecModels() []resolveSpecModel { //This must be generated with relspec to include all models //Use the relspec command with template generation. It supprot templ. return []resolveSpecModel{ // {schema: "public", entity: "activities", model: generatedmodels.ModelPublicActivities{}}, {schema: "public", entity: "agent_guardrails", model: generatedmodels.ModelPublicAgentGuardrails{}}, {schema: "public", entity: "agent_skills", model: generatedmodels.ModelPublicAgentSkills{}}, {schema: "public", entity: "chat_histories", model: generatedmodels.ModelPublicChatHistories{}}, // {schema: "public", entity: "contact_interactions", model: generatedmodels.ModelPublicContactInteractions{}}, {schema: "public", entity: "embeddings", model: generatedmodels.ModelPublicEmbeddings{}}, // {schema: "public", entity: "family_members", model: generatedmodels.ModelPublicFamilyMembers{}}, // {schema: "public", entity: "household_items", model: generatedmodels.ModelPublicHouseholdItems{}}, // {schema: "public", entity: "household_vendors", model: generatedmodels.ModelPublicHouseholdVendors{}}, // {schema: "public", entity: "important_dates", model: generatedmodels.ModelPublicImportantDates{}}, {schema: "public", entity: "learnings", model: generatedmodels.ModelPublicLearnings{}}, // {schema: "public", entity: "maintenance_logs", model: generatedmodels.ModelPublicMaintenanceLogs{}}, // {schema: "public", entity: "maintenance_tasks", model: generatedmodels.ModelPublicMaintenanceTasks{}}, // {schema: "public", entity: "meal_plans", model: generatedmodels.ModelPublicMealPlans{}}, // {schema: "public", entity: "opportunities", model: generatedmodels.ModelPublicOpportunities{}}, {schema: "public", entity: "plan_dependencies", model: generatedmodels.ModelPublicPlanDependencies{}}, {schema: "public", entity: "plan_guardrails", model: generatedmodels.ModelPublicPlanGuardrails{}}, {schema: "public", entity: "plan_related_plans", model: generatedmodels.ModelPublicPlanRelatedPlans{}}, {schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}}, {schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}}, // {schema: "public", entity: "professional_contacts", model: generatedmodels.ModelPublicProfessionalContacts{}}, {schema: "public", entity: "project_guardrails", model: generatedmodels.ModelPublicProjectGuardrails{}}, {schema: "public", entity: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}}, {schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}}, // {schema: "public", entity: "recipes", model: generatedmodels.ModelPublicRecipes{}}, // {schema: "public", entity: "shopping_lists", model: generatedmodels.ModelPublicShoppingLists{}}, {schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}}, {schema: "public", entity: "thought_links", model: generatedmodels.ModelPublicThoughtLinks{}}, {schema: "public", entity: "thoughts", model: generatedmodels.ModelPublicThoughts{}}, {schema: "public", entity: "tool_annotations", model: generatedmodels.ModelPublicToolAnnotations{}}, } }