feat(ui): add identity management for tenants and users
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:
2026-07-20 22:50:55 +02:00
parent 3d4e6d0939
commit 196b543bee
64 changed files with 3363 additions and 615 deletions
+3 -3
View File
@@ -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)))
-1
View File
@@ -475,4 +475,3 @@ func canonicalPlanPair(a, b int64) (int64, int64) {
}
return b, a
}
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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)))