feat(ui): add identity management for tenants and users
CI / build-and-test (push) Successful in 1m47s
CI / build-and-test (push) Successful in 1m47s
* Implement tenant and user creation in IdentityPage * Add API calls for managing tenants and users * Introduce tenant-scoped API requests * Update sidebar to include identity navigation * Create BooleanStatusBadge component for key status
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/auth"
|
||||
)
|
||||
|
||||
type identityAdmin struct {
|
||||
pool *pgxpool.Pool
|
||||
keyring *auth.Keyring
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func newIdentityAdmin(pool *pgxpool.Pool, keyring *auth.Keyring, logger *slog.Logger) *identityAdmin {
|
||||
return &identityAdmin{pool: pool, keyring: keyring, logger: logger}
|
||||
}
|
||||
|
||||
func loadIdentityKeyring(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error {
|
||||
if keyring == nil {
|
||||
return nil
|
||||
}
|
||||
rows, err := pool.Query(ctx, `select a.key_id, a.tenant_id, a.enabled, coalesce(m.secret_hash, '') from api_key_assignments a left join managed_api_keys m on m.key_id = a.key_id`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var keyID, tenantID, hash string
|
||||
var enabled bool
|
||||
if err := rows.Scan(&keyID, &tenantID, &enabled, &hash); err != nil {
|
||||
return err
|
||||
}
|
||||
keyring.AssignTenant(keyID, tenantID)
|
||||
if hash != "" {
|
||||
keyring.AddManaged(keyID, hash, enabled)
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// ensureConfiguredTenants makes a tenant referenced in static YAML visible to
|
||||
// the admin UI as well as to the authentication middleware.
|
||||
func ensureConfiguredTenants(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error {
|
||||
if keyring == nil {
|
||||
return nil
|
||||
}
|
||||
for _, key := range keyring.ConfiguredKeys() {
|
||||
tenantID := strings.TrimSpace(key.TenantID)
|
||||
if tenantID == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `insert into tenants (id, name) values ($1, $1) on conflict (id) do nothing`, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *identityAdmin) handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
keyID, ok := auth.KeyIDFromContext(r.Context())
|
||||
if !ok || a.keyring == nil || !a.keyring.IsSuperadmin(keyID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "superadmin API key required"})
|
||||
return
|
||||
}
|
||||
}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/admin/identity")
|
||||
switch {
|
||||
case r.Method == http.MethodGet && path == "":
|
||||
a.list(w, r)
|
||||
case r.Method == http.MethodPost && path == "/tenants":
|
||||
a.createTenant(w, r)
|
||||
case r.Method == http.MethodPost && path == "/users":
|
||||
a.createUser(w, r)
|
||||
case r.Method == http.MethodPost && path == "/keys":
|
||||
a.createKey(w, r)
|
||||
case r.Method == http.MethodPatch && strings.HasPrefix(path, "/keys/"):
|
||||
a.updateKey(w, r, strings.TrimPrefix(path, "/keys/"))
|
||||
case r.Method == http.MethodPost && strings.HasPrefix(path, "/tenants/") && strings.HasSuffix(path, "/adopt-legacy"):
|
||||
a.adoptLegacy(w, r, strings.TrimSuffix(strings.TrimPrefix(path, "/tenants/"), "/adopt-legacy"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type tenantDTO struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
type userDTO struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
type keyDTO struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID *string `json:"user_id,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (a *identityAdmin) list(w http.ResponseWriter, r *http.Request) {
|
||||
result := struct {
|
||||
Tenants []tenantDTO `json:"tenants"`
|
||||
Users []userDTO `json:"users"`
|
||||
Keys []keyDTO `json:"keys"`
|
||||
}{Tenants: []tenantDTO{}, Users: []userDTO{}, Keys: []keyDTO{}}
|
||||
rows, err := a.pool.Query(r.Context(), `select id, name, created_at from tenants order by name`)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var x tenantDTO
|
||||
if err := rows.Scan(&x.ID, &x.Name, &x.CreatedAt); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
result.Tenants = append(result.Tenants, x)
|
||||
}
|
||||
rows, err = a.pool.Query(r.Context(), `select id, tenant_id, name, email, created_at from tenant_users order by name`)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var x userDTO
|
||||
if err := rows.Scan(&x.ID, &x.TenantID, &x.Name, &x.Email, &x.CreatedAt); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
result.Users = append(result.Users, x)
|
||||
}
|
||||
configured := make(map[string]authKey)
|
||||
if a.keyring != nil {
|
||||
for _, key := range a.keyring.ConfiguredKeys() {
|
||||
configured[key.ID] = authKey{description: key.Description}
|
||||
}
|
||||
}
|
||||
rows, err = a.pool.Query(r.Context(), `select key_id, tenant_id, user_id, description, source, enabled, created_at from api_key_assignments order by key_id`)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var x keyDTO
|
||||
if err := rows.Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
result.Keys = append(result.Keys, x)
|
||||
delete(configured, x.ID)
|
||||
}
|
||||
for id, key := range configured {
|
||||
result.Keys = append(result.Keys, keyDTO{ID: id, Description: key.description, Source: "configured", Enabled: true})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
type authKey struct{ description string }
|
||||
|
||||
func (a *identityAdmin) createTenant(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.Name = strings.TrimSpace(body.Name)
|
||||
if body.Name == "" {
|
||||
badRequest(w, "name is required")
|
||||
return
|
||||
}
|
||||
x := tenantDTO{ID: newIdentityID(), Name: body.Name}
|
||||
err := a.pool.QueryRow(r.Context(), `insert into tenants (id,name) values ($1,$2) returning created_at`, x.ID, x.Name).Scan(&x.CreatedAt)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, x)
|
||||
}
|
||||
func (a *identityAdmin) createUser(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
TenantID, Name string
|
||||
Email *string `json:"email"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.TenantID = strings.TrimSpace(body.TenantID)
|
||||
body.Name = strings.TrimSpace(body.Name)
|
||||
if body.TenantID == "" || body.Name == "" {
|
||||
badRequest(w, "tenant_id and name are required")
|
||||
return
|
||||
}
|
||||
x := userDTO{ID: newIdentityID(), TenantID: body.TenantID, Name: body.Name, Email: body.Email}
|
||||
err := a.pool.QueryRow(r.Context(), `insert into tenant_users (id,tenant_id,name,email) values ($1,$2,$3,$4) returning created_at`, x.ID, x.TenantID, x.Name, x.Email).Scan(&x.CreatedAt)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, x)
|
||||
}
|
||||
func (a *identityAdmin) createKey(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID *string `json:"user_id"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.TenantID = strings.TrimSpace(body.TenantID)
|
||||
if body.TenantID == "" {
|
||||
badRequest(w, "tenant_id is required")
|
||||
return
|
||||
}
|
||||
secret, hash, err := auth.GenerateSecret()
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
x := keyDTO{ID: newIdentityID(), TenantID: body.TenantID, UserID: body.UserID, Description: strings.TrimSpace(body.Description), Source: "managed", Enabled: true}
|
||||
tx, err := a.pool.Begin(r.Context())
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
if err = tx.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,$4,'managed',true) returning created_at`, x.ID, x.TenantID, x.UserID, x.Description).Scan(&x.CreatedAt); err == nil {
|
||||
_, err = tx.Exec(r.Context(), `insert into managed_api_keys (key_id,secret_hash) values ($1,$2)`, x.ID, hash)
|
||||
}
|
||||
if err == nil {
|
||||
err = tx.Commit(r.Context())
|
||||
}
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
a.keyring.AddManaged(x.ID, hash, true)
|
||||
a.keyring.AssignTenant(x.ID, x.TenantID)
|
||||
writeJSON(w, http.StatusCreated, struct {
|
||||
Key keyDTO `json:"key"`
|
||||
Secret string `json:"secret"`
|
||||
}{x, secret})
|
||||
}
|
||||
func (a *identityAdmin) updateKey(w http.ResponseWriter, r *http.Request, keyID string) {
|
||||
var body struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID *string `json:"user_id"`
|
||||
Description *string `json:"description"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.TenantID = strings.TrimSpace(body.TenantID)
|
||||
if body.TenantID == "" {
|
||||
badRequest(w, "tenant_id is required")
|
||||
return
|
||||
}
|
||||
if !a.keyring.IsConfigured(keyID) {
|
||||
var exists bool
|
||||
if err := a.pool.QueryRow(r.Context(), `select exists(select 1 from managed_api_keys where key_id=$1)`, keyID).Scan(&exists); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
badRequest(w, "unknown key id")
|
||||
return
|
||||
}
|
||||
}
|
||||
var x keyDTO
|
||||
err := a.pool.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,coalesce($4,''),case when $6 then 'configured' else 'managed' end,coalesce($5,true)) on conflict (key_id) do update set tenant_id=excluded.tenant_id,user_id=excluded.user_id,description=coalesce($4,api_key_assignments.description),enabled=coalesce($5,api_key_assignments.enabled) returning key_id,tenant_id,user_id,description,source,enabled,created_at`, keyID, body.TenantID, body.UserID, body.Description, body.Enabled, a.keyring.IsConfigured(keyID)).Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
a.keyring.AssignTenant(x.ID, x.TenantID)
|
||||
a.keyring.SetManagedEnabled(x.ID, x.Enabled)
|
||||
writeJSON(w, http.StatusOK, x)
|
||||
}
|
||||
func (a *identityAdmin) adoptLegacy(w http.ResponseWriter, r *http.Request, tenantID string) {
|
||||
tenantID = strings.TrimSpace(tenantID)
|
||||
if tenantID == "" {
|
||||
badRequest(w, "tenant id is required")
|
||||
return
|
||||
}
|
||||
tables := []string{"projects", "thoughts", "stored_files", "learnings", "plans", "chat_histories"}
|
||||
tx, err := a.pool.Begin(r.Context())
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
var exists bool
|
||||
if err = tx.QueryRow(r.Context(), `select exists(select 1 from tenants where id=$1)`, tenantID).Scan(&exists); err == nil && !exists {
|
||||
badRequest(w, "tenant does not exist")
|
||||
return
|
||||
}
|
||||
for _, table := range tables {
|
||||
if _, err = tx.Exec(r.Context(), fmt.Sprintf("update %s set tenant_id=$1 where tenant_id is null", table), tenantID); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(r.Context()); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
func newIdentityID() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, v any) bool {
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
badRequest(w, "invalid JSON")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
func badRequest(w http.ResponseWriter, message string) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": message})
|
||||
}
|
||||
func identityError(w http.ResponseWriter, err error) {
|
||||
if a, ok := err.(interface{ SQLState() string }); ok && a.SQLState() == "23505" {
|
||||
badRequest(w, "that value already exists")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "identity operation failed"})
|
||||
}
|
||||
@@ -91,6 +91,14 @@ func Run(ctx context.Context, configPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
keyring = auth.NewManagedKeyring()
|
||||
}
|
||||
if err := ensureConfiguredTenants(ctx, db.Pool(), keyring); err != nil {
|
||||
return fmt.Errorf("create configured tenants: %w", err)
|
||||
}
|
||||
if err := loadIdentityKeyring(ctx, db.Pool(), keyring); err != nil {
|
||||
return fmt.Errorf("load identity key assignments: %w", err)
|
||||
}
|
||||
tokenStore = auth.NewTokenStore(0)
|
||||
if len(cfg.Auth.OAuth.Clients) > 0 {
|
||||
@@ -192,6 +200,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
||||
enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger)
|
||||
backfillTool := tools.NewBackfillTool(db, bgEmbeddings, activeProjects, logger)
|
||||
adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)
|
||||
identityAdmin := newIdentityAdmin(db.Pool(), keyring, logger)
|
||||
|
||||
toolSet := mcpserver.ToolSet{
|
||||
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
|
||||
@@ -246,6 +255,8 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
||||
mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger))
|
||||
mux.Handle("/api/admin/actions/backfill", authMiddleware(adminActions.backfillHandler()))
|
||||
mux.Handle("/api/admin/actions/retry-metadata", authMiddleware(adminActions.retryMetadataHandler()))
|
||||
mux.Handle("/api/admin/identity", authMiddleware(identityAdmin.handler()))
|
||||
mux.Handle("/api/admin/identity/", authMiddleware(identityAdmin.handler()))
|
||||
mux.HandleFunc("/favicon.ico", serveFavicon)
|
||||
mux.HandleFunc("/images/project.jpg", serveHomeImage)
|
||||
mux.HandleFunc("/images/icon.png", serveIcon)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/uptrace/bunrouter"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
"git.warky.dev/wdevs/amcs/internal/tenancy"
|
||||
)
|
||||
|
||||
func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware func(http.Handler) http.Handler, logger *slog.Logger) error {
|
||||
@@ -45,7 +46,12 @@ func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware
|
||||
rsMount.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
middleware(rsMount).ServeHTTP(w, r)
|
||||
middleware(http.HandlerFunc(func(w http.ResponseWriter, authenticated *http.Request) {
|
||||
if tenantID := strings.TrimSpace(authenticated.Header.Get("X-AMCS-Tenant-ID")); tenantID != "" {
|
||||
authenticated = authenticated.WithContext(tenancy.WithTenantKey(authenticated.Context(), tenantID))
|
||||
}
|
||||
rsMount.ServeHTTP(w, authenticated)
|
||||
})).ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
mux.Handle("/api/rs/", protectedRSMount)
|
||||
|
||||
@@ -14,12 +14,14 @@ func resolveSpecModels() []resolveSpecModel {
|
||||
{schema: "public", entity: "agent_personas", model: generatedmodels.ModelPublicAgentPersonas{}},
|
||||
{schema: "public", entity: "agent_skills", model: generatedmodels.ModelPublicAgentSkills{}},
|
||||
{schema: "public", entity: "agent_traits", model: generatedmodels.ModelPublicAgentTraits{}},
|
||||
{schema: "public", entity: "api_key_assignments", model: generatedmodels.ModelPublicAPIKeyAssignments{}},
|
||||
{schema: "public", entity: "arc_stage_parts", model: generatedmodels.ModelPublicArcStageParts{}},
|
||||
{schema: "public", entity: "arc_stages", model: generatedmodels.ModelPublicArcStages{}},
|
||||
{schema: "public", entity: "character_arcs", model: generatedmodels.ModelPublicCharacterArcs{}},
|
||||
{schema: "public", entity: "chat_histories", model: generatedmodels.ModelPublicChatHistories{}},
|
||||
{schema: "public", entity: "embeddings", model: generatedmodels.ModelPublicEmbeddings{}},
|
||||
{schema: "public", entity: "learnings", model: generatedmodels.ModelPublicLearnings{}},
|
||||
{schema: "public", entity: "managed_api_keys", model: generatedmodels.ModelPublicManagedAPIKeys{}},
|
||||
{schema: "public", entity: "oauth_clients", model: generatedmodels.ModelPublicOauthClients{}},
|
||||
{schema: "public", entity: "persona_arc", model: generatedmodels.ModelPublicPersonaArc{}},
|
||||
{schema: "public", entity: "plan_dependencies", model: generatedmodels.ModelPublicPlanDependencies{}},
|
||||
@@ -32,6 +34,8 @@ func resolveSpecModels() []resolveSpecModel {
|
||||
{schema: "public", entity: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}},
|
||||
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
|
||||
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
|
||||
{schema: "public", entity: "tenant_users", model: generatedmodels.ModelPublicTenantUsers{}},
|
||||
{schema: "public", entity: "tenants", model: generatedmodels.ModelPublicTenants{}},
|
||||
{schema: "public", entity: "thought_learning_links", model: generatedmodels.ModelPublicThoughtLearningLinks{}},
|
||||
{schema: "public", entity: "thought_links", model: generatedmodels.ModelPublicThoughtLinks{}},
|
||||
{schema: "public", entity: "thoughts", model: generatedmodels.ModelPublicThoughts{}},
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
var (
|
||||
//go:embed ui/dist
|
||||
uiFiles embed.FS
|
||||
uiDistFS fs.FS
|
||||
uiFiles embed.FS
|
||||
uiDistFS fs.FS
|
||||
indexHTML []byte
|
||||
)
|
||||
|
||||
|
||||
+113
-2
@@ -1,14 +1,27 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
)
|
||||
|
||||
type Keyring struct {
|
||||
keys []config.APIKey
|
||||
mu sync.RWMutex
|
||||
keys []config.APIKey
|
||||
tenantsByKeyID map[string]string
|
||||
managed map[string]managedKey
|
||||
}
|
||||
|
||||
type managedKey struct {
|
||||
secretHash string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func NewKeyring(keys []config.APIKey) (*Keyring, error) {
|
||||
@@ -16,14 +29,112 @@ func NewKeyring(keys []config.APIKey) (*Keyring, error) {
|
||||
return nil, fmt.Errorf("keyring requires at least one key")
|
||||
}
|
||||
|
||||
return &Keyring{keys: append([]config.APIKey(nil), keys...)}, nil
|
||||
tenantsByKeyID := make(map[string]string)
|
||||
for _, key := range keys {
|
||||
if tenantID := strings.TrimSpace(key.TenantID); tenantID != "" {
|
||||
tenantsByKeyID[key.ID] = tenantID
|
||||
}
|
||||
}
|
||||
return &Keyring{keys: append([]config.APIKey(nil), keys...), tenantsByKeyID: tenantsByKeyID, managed: make(map[string]managedKey)}, nil
|
||||
}
|
||||
|
||||
// NewManagedKeyring is used when API credentials are administered in the
|
||||
// database rather than supplied through static configuration.
|
||||
func NewManagedKeyring() *Keyring {
|
||||
return &Keyring{tenantsByKeyID: make(map[string]string), managed: make(map[string]managedKey)}
|
||||
}
|
||||
|
||||
func (k *Keyring) Lookup(value string) (string, bool) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
for _, key := range k.keys {
|
||||
if subtle.ConstantTimeCompare([]byte(key.Value), []byte(value)) == 1 {
|
||||
return key.ID, true
|
||||
}
|
||||
}
|
||||
hash := secretHash(value)
|
||||
for keyID, key := range k.managed {
|
||||
if key.enabled && subtle.ConstantTimeCompare([]byte(key.secretHash), []byte(hash)) == 1 {
|
||||
return keyID, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// TenantForKey returns the tenant boundary assigned to keyID. Unassigned
|
||||
// configured keys retain the historical key-ID boundary for compatibility.
|
||||
func (k *Keyring) TenantForKey(keyID string) string {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
if tenantID := k.tenantsByKeyID[keyID]; tenantID != "" {
|
||||
return tenantID
|
||||
}
|
||||
return keyID
|
||||
}
|
||||
|
||||
func (k *Keyring) AssignTenant(keyID, tenantID string) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
if tenantID == "" {
|
||||
delete(k.tenantsByKeyID, keyID)
|
||||
return
|
||||
}
|
||||
k.tenantsByKeyID[keyID] = tenantID
|
||||
}
|
||||
|
||||
func (k *Keyring) AddManaged(keyID, secretHash string, enabled bool) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.managed[keyID] = managedKey{secretHash: secretHash, enabled: enabled}
|
||||
}
|
||||
|
||||
func (k *Keyring) ConfiguredKeys() []config.APIKey {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return append([]config.APIKey(nil), k.keys...)
|
||||
}
|
||||
|
||||
func (k *Keyring) IsConfigured(keyID string) bool {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
for _, key := range k.keys {
|
||||
if key.ID == keyID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keyring) IsSuperadmin(keyID string) bool {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
for _, key := range k.keys {
|
||||
if key.ID == keyID {
|
||||
return key.Superadmin
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keyring) SetManagedEnabled(keyID string, enabled bool) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
if key, ok := k.managed[keyID]; ok {
|
||||
key.enabled = enabled
|
||||
k.managed[keyID] = key
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateSecret() (string, string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
secret := "amcs_" + hex.EncodeToString(b)
|
||||
return secret, secretHash(secret), nil
|
||||
}
|
||||
|
||||
func secretHash(secret string) string {
|
||||
sum := sha256.Sum256([]byte(secret))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
@@ -36,6 +36,26 @@ func TestNewKeyringAndLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredKeyUsesTenantID(t *testing.T) {
|
||||
keyring, err := NewKeyring([]config.APIKey{{ID: "agent-key", Value: "secret", TenantID: "acme"}})
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeyring() error = %v", err)
|
||||
}
|
||||
if got := keyring.TenantForKey("agent-key"); got != "acme" {
|
||||
t.Fatalf("TenantForKey() = %q, want acme", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredKeySuperadmin(t *testing.T) {
|
||||
keyring, err := NewKeyring([]config.APIKey{{ID: "operator", Value: "secret", Superadmin: true}})
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeyring() error = %v", err)
|
||||
}
|
||||
if !keyring.IsSuperadmin("operator") || keyring.IsSuperadmin("missing") {
|
||||
t.Fatal("IsSuperadmin() did not return the configured role")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareAllowsHeaderAuthAndSetsContext(t *testing.T) {
|
||||
keyring, err := NewKeyring([]config.APIKey{{ID: "client-a", Value: "secret"}})
|
||||
if err != nil {
|
||||
|
||||
@@ -53,7 +53,11 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
||||
}
|
||||
withTenant := func(ctx context.Context, keyID string) context.Context {
|
||||
ctx = context.WithValue(ctx, keyIDContextKey, keyID)
|
||||
return tenancy.WithTenantKey(ctx, keyID)
|
||||
tenantID := keyID
|
||||
if keyring != nil {
|
||||
tenantID = keyring.TenantForKey(keyID)
|
||||
}
|
||||
return tenancy.WithTenantKey(ctx, tenantID)
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -53,6 +53,8 @@ type AuthConfig struct {
|
||||
type APIKey struct {
|
||||
ID string `yaml:"id"`
|
||||
Value string `yaml:"value"`
|
||||
TenantID string `yaml:"tenant_id"`
|
||||
Superadmin bool `yaml:"superadmin"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ type ModelPublicAgentGuardrails struct {
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Severity sql_types.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelGuardrailIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
|
||||
RelGuardrailIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
|
||||
RelGuardrailIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
|
||||
|
||||
@@ -18,7 +18,9 @@ type ModelPublicAgentParts struct {
|
||||
PartType sql_types.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelPartIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
|
||||
RelPartIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ type ModelPublicAgentPersonas struct {
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelPersonaIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
|
||||
RelPersonaIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
|
||||
RelPersonaIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
|
||||
|
||||
@@ -3,6 +3,7 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
@@ -19,8 +20,10 @@ type ModelPublicAgentSkills struct {
|
||||
LanguageTags sql_types.SqlStringArray `bun:"language_tags,type:text[],default:'{}',notnull," json:"language_tags"`
|
||||
LibraryTags sql_types.SqlStringArray `bun:"library_tags,type:text[],default:'{}',notnull," json:"library_tags"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,array,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelSkillIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
|
||||
RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||
RelSkillIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
||||
|
||||
@@ -16,8 +16,10 @@ type ModelPublicAgentTraits struct {
|
||||
Instruction sql_types.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
TraitType sql_types.SqlString `bun:"trait_type,type:text,notnull," json:"trait_type"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelTraitIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=trait_id" json:"reltraitidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAPIKeyAssignments struct {
|
||||
bun.BaseModel `bun:"table:public.api_key_assignments,alias:api_key_assignments"`
|
||||
KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Enabled bool `bun:"enabled,type:boolean,default:true,notnull," json:"enabled"`
|
||||
Source sql_types.SqlString `bun:"source,type:text,notnull," json:"source"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull," json:"tenant_id"`
|
||||
UserID sql_types.SqlString `bun:"user_id,type:text,nullzero," json:"user_id"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelUserID *ModelPublicTenantUsers `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicTenantUsers
|
||||
RelKeyIDPublicManagedAPIKeys []*ModelPublicManagedAPIKeys `bun:"rel:has-many,join:key_id=key_id" json:"relkeyidpublicmanagedapikeys,omitempty"` // Has many ModelPublicManagedAPIKeys
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAPIKeyAssignments
|
||||
func (m ModelPublicAPIKeyAssignments) TableName() string {
|
||||
return "public.api_key_assignments"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicAPIKeyAssignments
|
||||
func (m ModelPublicAPIKeyAssignments) TableNameOnly() string {
|
||||
return "api_key_assignments"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicAPIKeyAssignments
|
||||
func (m ModelPublicAPIKeyAssignments) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicAPIKeyAssignments) GetID() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAPIKeyAssignments) GetIDStr() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicAPIKeyAssignments) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicAPIKeyAssignments) UpdateID(newid string) {
|
||||
m.KeyID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicAPIKeyAssignments) GetIDName() string {
|
||||
return "key_id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicAPIKeyAssignments) GetPrefix() string {
|
||||
return "AKA"
|
||||
}
|
||||
@@ -14,7 +14,9 @@ type ModelPublicCharacterArcs struct {
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,default:'',notnull," json:"summary"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelArcIDPublicArcStages []*ModelPublicArcStages `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicarcstages,omitempty"` // Has many ModelPublicArcStages
|
||||
RelArcIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ type ModelPublicChatHistories struct {
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
SessionID sql_types.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,nullzero," json:"summary"`
|
||||
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
Title sql_types.SqlString `bun:"title,type:text,nullzero," json:"title"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicChatHistories
|
||||
|
||||
@@ -30,13 +30,14 @@ type ModelPublicLearnings struct {
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
SupersedesLearningID sql_types.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelRelatedSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:related_skill_id=id" json:"relrelatedskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
RelRelatedThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:related_thought_id=id" json:"relrelatedthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
RelSupersedesLearningID *ModelPublicLearnings `bun:"rel:has-one,join:supersedes_learning_id=id" json:"relsupersedeslearningid,omitempty"` // Has one ModelPublicLearnings
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelLearningIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=learning_id" json:"rellearningidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicManagedAPIKeys struct {
|
||||
bun.BaseModel `bun:"table:public.managed_api_keys,alias:managed_api_keys"`
|
||||
KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"`
|
||||
SecretHash sql_types.SqlString `bun:"secret_hash,type:text,notnull," json:"secret_hash"`
|
||||
RelKeyID *ModelPublicAPIKeyAssignments `bun:"rel:has-one,join:key_id=key_id" json:"relkeyid,omitempty"` // Has one ModelPublicAPIKeyAssignments
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicManagedAPIKeys
|
||||
func (m ModelPublicManagedAPIKeys) TableName() string {
|
||||
return "public.managed_api_keys"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicManagedAPIKeys
|
||||
func (m ModelPublicManagedAPIKeys) TableNameOnly() string {
|
||||
return "managed_api_keys"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicManagedAPIKeys
|
||||
func (m ModelPublicManagedAPIKeys) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicManagedAPIKeys) GetID() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicManagedAPIKeys) GetIDStr() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicManagedAPIKeys) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicManagedAPIKeys) UpdateID(newid string) {
|
||||
m.KeyID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicManagedAPIKeys) GetIDName() string {
|
||||
return "key_id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicManagedAPIKeys) GetPrefix() string {
|
||||
return "MAK"
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
type ModelPublicPersonaArc struct {
|
||||
bun.BaseModel `bun:"table:public.persona_arc,alias:persona_arc"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,pk," json:"persona_id"`
|
||||
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
|
||||
CurrentStageID int64 `bun:"current_stage_id,type:bigint,notnull," json:"current_stage_id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs
|
||||
RelCurrentStageID *ModelPublicArcStages `bun:"rel:has-one,join:current_stage_id=id" json:"relcurrentstageid,omitempty"` // Has one ModelPublicArcStages
|
||||
|
||||
@@ -23,11 +23,12 @@ type ModelPublicPlans struct {
|
||||
Status sql_types.SqlString `bun:"status,type:text,default:'draft',notnull," json:"status"` // draft, active, blocked, completed, cancelled, superseded
|
||||
SupersedesPlanID sql_types.SqlInt64 `bun:"supersedes_plan_id,type:bigint,nullzero," json:"supersedes_plan_id"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
Title sql_types.SqlString `bun:"title,type:text,notnull," json:"title"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||
RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||
RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||
|
||||
@@ -14,8 +14,9 @@ type ModelPublicProjects struct {
|
||||
Description sql_types.SqlString `bun:"description,type:text,nullzero," json:"description"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
LastActiveAt sql_types.SqlTimeStamp `bun:"last_active_at,type:timestamptz,default:now(),nullzero," json:"last_active_at"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull,unique:uidx_projects_tenant_key_name," json:"name"`
|
||||
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero,unique:uidx_projects_tenant_key_name," json:"tenant_key"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull,unique:uidx_projects_tenant_id_name," json:"name"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero,unique:uidx_projects_tenant_id_name," json:"tenant_id"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelProjectIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
|
||||
RelProjectIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicthoughts,omitempty"` // Has many ModelPublicThoughts
|
||||
RelProjectIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
||||
|
||||
@@ -20,10 +20,11 @@ type ModelPublicStoredFiles struct {
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
Sha256 sql_types.SqlString `bun:"sha256,type:text,notnull," json:"sha256"`
|
||||
SizeBytes int64 `bun:"size_bytes,type:bigint,notnull," json:"size_bytes"`
|
||||
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
ThoughtID sql_types.SqlInt64 `bun:"thought_id,type:bigint,nullzero," json:"thought_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicTenantUsers struct {
|
||||
bun.BaseModel `bun:"table:public.tenant_users,alias:tenant_users"`
|
||||
ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Email sql_types.SqlString `bun:"email,type:text,nullzero,unique:uidx_tenant_users_tenant_id_email," json:"email"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull,unique:uidx_tenant_users_tenant_id_email," json:"tenant_id"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelUserIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicTenantUsers
|
||||
func (m ModelPublicTenantUsers) TableName() string {
|
||||
return "public.tenant_users"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicTenantUsers
|
||||
func (m ModelPublicTenantUsers) TableNameOnly() string {
|
||||
return "tenant_users"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicTenantUsers
|
||||
func (m ModelPublicTenantUsers) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicTenantUsers) GetID() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicTenantUsers) GetIDStr() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicTenantUsers) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicTenantUsers) UpdateID(newid string) {
|
||||
m.ID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicTenantUsers) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicTenantUsers) GetPrefix() string {
|
||||
return "TUE"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicTenants struct {
|
||||
bun.BaseModel `bun:"table:public.tenants,alias:tenants"`
|
||||
ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
RelTenantIDPublicAgentPersonas []*ModelPublicAgentPersonas `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentpersonas,omitempty"` // Has many ModelPublicAgentPersonas
|
||||
RelTenantIDPublicAgentParts []*ModelPublicAgentParts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentparts,omitempty"` // Has many ModelPublicAgentParts
|
||||
RelTenantIDPublicAgentTraits []*ModelPublicAgentTraits `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagenttraits,omitempty"` // Has many ModelPublicAgentTraits
|
||||
RelTenantIDPublicCharacterArcs []*ModelPublicCharacterArcs `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliccharacterarcs,omitempty"` // Has many ModelPublicCharacterArcs
|
||||
RelTenantIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicthoughts,omitempty"` // Has many ModelPublicThoughts
|
||||
RelTenantIDPublicProjects []*ModelPublicProjects `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicprojects,omitempty"` // Has many ModelPublicProjects
|
||||
RelTenantIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
||||
RelTenantIDPublicTenantUsers []*ModelPublicTenantUsers `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublictenantusers,omitempty"` // Has many ModelPublicTenantUsers
|
||||
RelTenantIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments
|
||||
RelTenantIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
|
||||
RelTenantIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||
RelTenantIDPublicPlans []*ModelPublicPlans `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicplans,omitempty"` // Has many ModelPublicPlans
|
||||
RelTenantIDPublicAgentSkills []*ModelPublicAgentSkills `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentskills,omitempty"` // Has many ModelPublicAgentSkills
|
||||
RelTenantIDPublicAgentGuardrails []*ModelPublicAgentGuardrails `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentguardrails,omitempty"` // Has many ModelPublicAgentGuardrails
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicTenants
|
||||
func (m ModelPublicTenants) TableName() string {
|
||||
return "public.tenants"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicTenants
|
||||
func (m ModelPublicTenants) TableNameOnly() string {
|
||||
return "tenants"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicTenants
|
||||
func (m ModelPublicTenants) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicTenants) GetID() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicTenants) GetIDStr() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicTenants) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicTenants) UpdateID(newid string) {
|
||||
m.ID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicTenants) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicTenants) GetPrefix() string {
|
||||
return "TEN"
|
||||
}
|
||||
@@ -16,9 +16,10 @@ type ModelPublicThoughts struct {
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:{}::jsonb,nullzero," json:"metadata"`
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelFromIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=from_id" json:"relfromidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||
RelToIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=to_id" json:"reltoidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||
RelThoughtIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile) (thoughttypes.StoredFile, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into stored_files (thought_id, project_id, tenant_key, name, media_type, kind, encoding, size_bytes, sha256, content)
|
||||
insert into stored_files (thought_id, project_id, tenant_id, name, media_type, kind, encoding, size_bytes, sha256, content)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
returning id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
|
||||
`, file.ThoughtID, file.ProjectID, tenantKeyPtr(ctx), file.Name, file.MediaType, file.Kind, file.Encoding, file.SizeBytes, file.SHA256, file.Content)
|
||||
@@ -46,7 +46,7 @@ func (db *DB) GetStoredFile(ctx context.Context, id uuid.UUID) (thoughttypes.Sto
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content, created_at, updated_at
|
||||
from stored_files
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicStoredFiles
|
||||
if err := row.Scan(
|
||||
@@ -77,7 +77,7 @@ func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFil
|
||||
args := make([]any, 0, 4)
|
||||
conditions := make([]string, 0, 3)
|
||||
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_key")
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
if filter.ThoughtID != nil {
|
||||
args = append(args, *filter.ThoughtID)
|
||||
conditions = append(conditions, fmt.Sprintf("thought_id = $%d", len(args)))
|
||||
|
||||
@@ -475,4 +475,3 @@ func canonicalPlanPair(a, b int64) (int64, int64) {
|
||||
}
|
||||
return b, a
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func (db *DB) CreateProject(ctx context.Context, name, description string) (thoughttypes.Project, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into projects (name, description, tenant_key)
|
||||
insert into projects (name, description, tenant_id)
|
||||
values ($1, $2, $3)
|
||||
returning id, guid, name, description, created_at, last_active_at
|
||||
`, name, description, tenantKeyPtr(ctx))
|
||||
@@ -49,7 +49,7 @@ func (db *DB) getProjectByGUID(ctx context.Context, id uuid.UUID) (thoughttypes.
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, name, description, created_at, last_active_at
|
||||
from projects
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
return scanProject(row)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (db *DB) getProjectByName(ctx context.Context, name string) (thoughttypes.P
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, name, description, created_at, last_active_at
|
||||
from projects
|
||||
where name = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
return scanProject(row)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary,
|
||||
where := ""
|
||||
if key, ok := tenantKey(ctx); ok {
|
||||
args = append(args, key)
|
||||
where = "where p.tenant_key = $1"
|
||||
where = "where p.tenant_id = $1"
|
||||
}
|
||||
rows, err := db.pool.Query(ctx, `
|
||||
select p.id, p.guid, p.name, p.description, p.created_at, p.last_active_at, count(t.id) as thought_count
|
||||
@@ -113,7 +113,7 @@ func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary,
|
||||
|
||||
func (db *DB) TouchProject(ctx context.Context, id int64) error {
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("touch project: %w", err)
|
||||
}
|
||||
|
||||
+25
-15
@@ -28,10 +28,10 @@ func (db *DB) AddSkill(ctx context.Context, skill ext.AgentSkill) (ext.AgentSkil
|
||||
skill.DomainTags = []string{}
|
||||
}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into agent_skills (name, description, content, tags, language_tags, library_tags, framework_tags, domain_tags)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
insert into agent_skills (name, description, content, tenant_id, tags, language_tags, library_tags, framework_tags, domain_tags)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
returning id, guid, created_at, updated_at
|
||||
`, skill.Name, skill.Description, skill.Content, skill.Tags,
|
||||
`, skill.Name, skill.Description, skill.Content, tenantKeyPtr(ctx), skill.Tags,
|
||||
skill.LanguageTags, skill.LibraryTags, skill.FrameworkTags, skill.DomainTags)
|
||||
|
||||
created := skill
|
||||
@@ -47,7 +47,8 @@ func (db *DB) AddSkill(ctx context.Context, skill ext.AgentSkill) (ext.AgentSkil
|
||||
}
|
||||
|
||||
func (db *DB) RemoveSkill(ctx context.Context, id int64) error {
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_skills where id = $1`, id)
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_skills where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete agent skill: %w", err)
|
||||
}
|
||||
@@ -60,9 +61,14 @@ func (db *DB) RemoveSkill(ctx context.Context, id int64) error {
|
||||
func (db *DB) ListSkills(ctx context.Context, tag string) ([]ext.AgentSkill, error) {
|
||||
q := `select id, name, description, content, tags::text[], language_tags::text[], library_tags::text[], framework_tags::text[], domain_tags::text[], created_at, updated_at from agent_skills`
|
||||
args := []any{}
|
||||
conditions := []string{}
|
||||
if t := strings.TrimSpace(tag); t != "" {
|
||||
args = append(args, t)
|
||||
q += fmt.Sprintf(" where $%d = any(tags) or $%d = any(language_tags) or $%d = any(library_tags) or $%d = any(framework_tags) or $%d = any(domain_tags)", len(args), len(args), len(args), len(args), len(args))
|
||||
conditions = append(conditions, fmt.Sprintf("($%d = any(tags) or $%d = any(language_tags) or $%d = any(library_tags) or $%d = any(framework_tags) or $%d = any(domain_tags))", len(args), len(args), len(args), len(args), len(args)))
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
if len(conditions) > 0 {
|
||||
q += " where " + strings.Join(conditions, " and ")
|
||||
}
|
||||
q += " order by name"
|
||||
|
||||
@@ -135,7 +141,8 @@ func normalizeSkillSlices(skill *ext.AgentSkill) {
|
||||
}
|
||||
|
||||
func (db *DB) GetSkill(ctx context.Context, id int64) (ext.AgentSkill, error) {
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`, id)
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
s, err := scanSkill(row)
|
||||
if err != nil {
|
||||
return ext.AgentSkill{}, fmt.Errorf("get agent skill: %w", err)
|
||||
@@ -144,7 +151,8 @@ func (db *DB) GetSkill(ctx context.Context, id int64) (ext.AgentSkill, error) {
|
||||
}
|
||||
|
||||
func (db *DB) GetSkillByName(ctx context.Context, name string) (ext.AgentSkill, error) {
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where name = $1`, name)
|
||||
args := []any{name}
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
s, err := scanSkill(row)
|
||||
if err != nil {
|
||||
return ext.AgentSkill{}, fmt.Errorf("get agent skill by name: %w", err)
|
||||
@@ -153,10 +161,10 @@ func (db *DB) GetSkillByName(ctx context.Context, name string) (ext.AgentSkill,
|
||||
}
|
||||
|
||||
func (db *DB) GetGuardrailByName(ctx context.Context, name string) (ext.AgentGuardrail, error) {
|
||||
args := []any{name}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, name, description, content, severity, tags::text[], created_at, updated_at
|
||||
from agent_guardrails where name = $1
|
||||
`, name)
|
||||
from agent_guardrails where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicAgentGuardrails
|
||||
var tags []string
|
||||
@@ -189,10 +197,10 @@ func (db *DB) AddGuardrail(ctx context.Context, g ext.AgentGuardrail) (ext.Agent
|
||||
g.Severity = "medium"
|
||||
}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into agent_guardrails (name, description, content, severity, tags)
|
||||
values ($1, $2, $3, $4, $5)
|
||||
insert into agent_guardrails (name, description, content, severity, tenant_id, tags)
|
||||
values ($1, $2, $3, $4, $5, $6)
|
||||
returning id, guid, created_at, updated_at
|
||||
`, g.Name, g.Description, g.Content, g.Severity, g.Tags)
|
||||
`, g.Name, g.Description, g.Content, g.Severity, tenantKeyPtr(ctx), g.Tags)
|
||||
|
||||
created := g
|
||||
var model generatedmodels.ModelPublicAgentGuardrails
|
||||
@@ -207,7 +215,8 @@ func (db *DB) AddGuardrail(ctx context.Context, g ext.AgentGuardrail) (ext.Agent
|
||||
}
|
||||
|
||||
func (db *DB) RemoveGuardrail(ctx context.Context, id int64) error {
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_guardrails where id = $1`, id)
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_guardrails where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete agent guardrail: %w", err)
|
||||
}
|
||||
@@ -229,6 +238,7 @@ func (db *DB) ListGuardrails(ctx context.Context, tag, severity string) ([]ext.A
|
||||
args = append(args, s)
|
||||
conditions = append(conditions, fmt.Sprintf("severity = $%d", len(args)))
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
|
||||
q := `select id, name, description, content, severity, tags::text[], created_at, updated_at from agent_guardrails`
|
||||
if len(conditions) > 0 {
|
||||
@@ -268,10 +278,10 @@ func (db *DB) ListGuardrails(ctx context.Context, tag, severity string) ([]ext.A
|
||||
}
|
||||
|
||||
func (db *DB) GetGuardrail(ctx context.Context, id int64) (ext.AgentGuardrail, error) {
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, name, description, content, severity, tags::text[], created_at, updated_at
|
||||
from agent_guardrails where id = $1
|
||||
`, id)
|
||||
from agent_guardrails where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicAgentGuardrails
|
||||
var tags []string
|
||||
|
||||
+14
-14
@@ -31,7 +31,7 @@ func (db *DB) InsertThought(ctx context.Context, thought thoughttypes.Thought, e
|
||||
}()
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
insert into thoughts (content, metadata, project_id, tenant_key)
|
||||
insert into thoughts (content, metadata, project_id, tenant_id)
|
||||
values ($1, $2::jsonb, $3, $4)
|
||||
returning id, guid, created_at, updated_at
|
||||
`, thought.Content, metadata, thought.ProjectID, tenantKeyPtr(ctx))
|
||||
@@ -123,7 +123,7 @@ func (db *DB) ListThoughts(ctx context.Context, filter thoughttypes.ListFilter)
|
||||
args := make([]any, 0, 6)
|
||||
conditions := []string{}
|
||||
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_key")
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
if !filter.IncludeArchived {
|
||||
conditions = append(conditions, "archived_at is null")
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func (db *DB) Stats(ctx context.Context) (thoughttypes.ThoughtStats, error) {
|
||||
var total int
|
||||
statsArgs := []any{}
|
||||
statsConditions := []string{"archived_at is null"}
|
||||
addTenantCondition(ctx, &statsArgs, &statsConditions, "tenant_key")
|
||||
addTenantCondition(ctx, &statsArgs, &statsConditions, "tenant_id")
|
||||
if err := db.pool.QueryRow(ctx, `select count(*) from thoughts where `+strings.Join(statsConditions, " and "), statsArgs...).Scan(&total); err != nil {
|
||||
return thoughttypes.ThoughtStats{}, fmt.Errorf("count thoughts: %w", err)
|
||||
}
|
||||
@@ -241,7 +241,7 @@ func (db *DB) GetThought(ctx context.Context, id uuid.UUID) (thoughttypes.Though
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||
from thoughts
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicThoughts
|
||||
if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
@@ -264,7 +264,7 @@ func (db *DB) GetThoughtByID(ctx context.Context, id int64) (thoughttypes.Though
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||
from thoughts
|
||||
where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicThoughts
|
||||
if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
@@ -303,7 +303,7 @@ func (db *DB) UpdateThought(ctx context.Context, id uuid.UUID, content string, e
|
||||
metadata = $3::jsonb,
|
||||
project_id = $4,
|
||||
updated_at = now()
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return thoughttypes.Thought{}, fmt.Errorf("update thought: %w", err)
|
||||
}
|
||||
@@ -342,7 +342,7 @@ func (db *DB) UpdateThoughtMetadata(ctx context.Context, id int64, metadata thou
|
||||
update thoughts
|
||||
set metadata = $2::jsonb,
|
||||
updated_at = now()
|
||||
where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return thoughttypes.Thought{}, fmt.Errorf("update thought metadata: %w", err)
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func (db *DB) UpdateThoughtMetadata(ctx context.Context, id int64, metadata thou
|
||||
|
||||
func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error {
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete thought: %w", err)
|
||||
}
|
||||
@@ -367,7 +367,7 @@ func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error {
|
||||
|
||||
func (db *DB) ArchiveThought(ctx context.Context, id uuid.UUID) error {
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||
tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("archive thought: %w", err)
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func (db *DB) SearchSimilarThoughts(ctx context.Context, embedding []float32, em
|
||||
"1 - (e.embedding <=> $1) > $2",
|
||||
"e.model = $3",
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
if projectID != nil {
|
||||
args = append(args, *projectID)
|
||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
||||
@@ -495,7 +495,7 @@ func (db *DB) HasEmbeddingsForModel(ctx context.Context, model string, projectID
|
||||
"e.model = $1",
|
||||
"t.archived_at is null",
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
if projectID != nil {
|
||||
args = append(args, *projectID)
|
||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
||||
@@ -514,7 +514,7 @@ func (db *DB) HasEmbeddingsForModel(ctx context.Context, model string, projectID
|
||||
func (db *DB) ListThoughtsMissingEmbedding(ctx context.Context, model string, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) {
|
||||
args := []any{model}
|
||||
conditions := []string{"e.id is null"}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
|
||||
if !includeArchived {
|
||||
conditions = append(conditions, "t.archived_at is null")
|
||||
@@ -564,7 +564,7 @@ func (db *DB) ListThoughtsMissingEmbedding(ctx context.Context, model string, li
|
||||
func (db *DB) ListThoughtsForMetadataReparse(ctx context.Context, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) {
|
||||
args := make([]any, 0, 3)
|
||||
conditions := make([]string, 0, 4)
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_key")
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
|
||||
if !includeArchived {
|
||||
conditions = append(conditions, "archived_at is null")
|
||||
@@ -634,7 +634,7 @@ func (db *DB) SearchThoughtsText(ctx context.Context, query string, limit int, p
|
||||
"t.archived_at is null",
|
||||
"(to_tsvector('simple', t.content) || to_tsvector('simple', coalesce(p.name, ''))) @@ websearch_to_tsquery('simple', $1)",
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
if projectID != nil {
|
||||
args = append(args, *projectID)
|
||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
||||
|
||||
Reference in New Issue
Block a user