Some checks failed
CI / build-and-test (push) Failing after -31m24s
* Implement ContentEditorField for inline editing of content * Create ContentEditorModal for editing content in a modal * Introduce FormerShell for managing forms related to skills and thoughts * Enhance SkillsPage and ThoughtsPage with new components for better content management
173 lines
6.5 KiB
Go
173 lines
6.5 KiB
Go
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{}},
|
|
}
|
|
}
|