easyai-ai-gateway/apps/api/internal/store/identity_admin.go

411 lines
16 KiB
Go

package store
import (
"context"
"encoding/json"
"strings"
"github.com/jackc/pgx/v5"
"golang.org/x/crypto/bcrypt"
)
type GatewayTenantInput struct {
TenantKey string `json:"tenantKey"`
Source string `json:"source"`
ExternalTenantID string `json:"externalTenantId"`
Name string `json:"name"`
Description string `json:"description"`
DefaultUserGroupID string `json:"defaultUserGroupId"`
PlanKey string `json:"planKey"`
BillingProfile map[string]any `json:"billingProfile"`
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
AuthPolicy map[string]any `json:"authPolicy"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type GatewayUserInput struct {
UserKey string `json:"userKey"`
Source string `json:"source"`
ExternalUserID string `json:"externalUserId"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
Phone string `json:"phone"`
AvatarURL string `json:"avatarUrl"`
Password string `json:"password"`
GatewayTenantID string `json:"gatewayTenantId"`
TenantID string `json:"tenantId"`
TenantKey string `json:"tenantKey"`
DefaultUserGroupID string `json:"defaultUserGroupId"`
Roles []string `json:"roles"`
AuthProfile map[string]any `json:"authProfile"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type UserGroupInput struct {
GroupKey string `json:"groupKey"`
Name string `json:"name"`
Description string `json:"description"`
Source string `json:"source"`
Priority int `json:"priority"`
RechargeDiscountPolicy map[string]any `json:"rechargeDiscountPolicy"`
BillingDiscountPolicy map[string]any `json:"billingDiscountPolicy"`
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
QuotaPolicy map[string]any `json:"quotaPolicy"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
func (s *Store) CreateTenant(ctx context.Context, input GatewayTenantInput) (GatewayTenant, error) {
input = normalizeTenantInput(input)
billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanTenant(s.pool.QueryRow(ctx, `
INSERT INTO gateway_tenants (
tenant_key, source, external_tenant_id, name, description, default_user_group_id, plan_key,
billing_profile, rate_limit_policy, auth_policy, metadata, status
)
VALUES ($1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, '')::uuid, NULLIF($7, ''), $8, $9, $10, $11, $12)
RETURNING `+tenantColumns,
input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID, input.PlanKey,
billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
))
}
func (s *Store) UpdateTenant(ctx context.Context, id string, input GatewayTenantInput) (GatewayTenant, error) {
input = normalizeTenantInput(input)
billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanTenant(s.pool.QueryRow(ctx, `
UPDATE gateway_tenants
SET tenant_key = $2,
source = $3,
external_tenant_id = NULLIF($4, ''),
name = $5,
description = NULLIF($6, ''),
default_user_group_id = NULLIF($7, '')::uuid,
plan_key = NULLIF($8, ''),
billing_profile = $9,
rate_limit_policy = $10,
auth_policy = $11,
metadata = $12,
status = $13,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL
RETURNING `+tenantColumns,
id, input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID,
input.PlanKey, billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
))
}
func (s *Store) DeleteTenant(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `
UPDATE gateway_tenants
SET deleted_at = now(),
status = 'deleted',
tenant_key = tenant_key || ':deleted:' || left(id::text, 8),
external_tenant_id = NULL,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func (s *Store) CreateGatewayUser(ctx context.Context, input GatewayUserInput) (GatewayUser, error) {
input = normalizeUserInput(input)
passwordHash := ""
if strings.TrimSpace(input.Password) != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
return GatewayUser{}, err
}
passwordHash = string(hash)
}
roles, _ := json.Marshal(input.Roles)
authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUser(s.pool.QueryRow(ctx, `
INSERT INTO gateway_users (
user_key, source, external_user_id, username, display_name, email, phone, avatar_url, password_hash,
gateway_tenant_id, tenant_id, tenant_key, default_user_group_id, roles, auth_profile, metadata, status
)
VALUES (
$1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''), NULLIF($8, ''), NULLIF($9, ''),
NULLIF($10, '')::uuid, NULLIF($11, ''), NULLIF($12, ''), NULLIF($13, '')::uuid, $14, $15, $16, $17
)
RETURNING `+userColumns,
input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone, input.AvatarURL, passwordHash,
input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID, roles, authProfile, metadata, input.Status,
))
}
func (s *Store) UpdateGatewayUser(ctx context.Context, id string, input GatewayUserInput) (GatewayUser, error) {
input = normalizeUserInput(input)
passwordHash := ""
if strings.TrimSpace(input.Password) != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
return GatewayUser{}, err
}
passwordHash = string(hash)
}
roles, _ := json.Marshal(input.Roles)
authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUser(s.pool.QueryRow(ctx, `
UPDATE gateway_users
SET user_key = $2,
source = $3,
external_user_id = NULLIF($4, ''),
username = $5,
display_name = NULLIF($6, ''),
email = NULLIF($7, ''),
phone = NULLIF($8, ''),
avatar_url = NULLIF($9, ''),
password_hash = COALESCE(NULLIF($10, ''), password_hash),
gateway_tenant_id = NULLIF($11, '')::uuid,
tenant_id = NULLIF($12, ''),
tenant_key = NULLIF($13, ''),
default_user_group_id = NULLIF($14, '')::uuid,
roles = $15,
auth_profile = $16,
metadata = $17,
status = $18,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL
RETURNING `+userColumns,
id, input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone,
input.AvatarURL, passwordHash, input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID,
roles, authProfile, metadata, input.Status,
))
}
func (s *Store) DeleteGatewayUser(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `
UPDATE gateway_users
SET deleted_at = now(),
status = 'deleted',
user_key = user_key || ':deleted:' || left(id::text, 8),
external_user_id = NULL,
email = NULL,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func (s *Store) CreateUserGroup(ctx context.Context, input UserGroupInput) (UserGroup, error) {
input = normalizeUserGroupInput(input)
rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUserGroup(s.pool.QueryRow(ctx, `
INSERT INTO gateway_user_groups (
group_key, name, description, source, priority, recharge_discount_policy, billing_discount_policy,
rate_limit_policy, quota_policy, metadata, status
)
VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING `+userGroupColumns,
input.GroupKey, input.Name, input.Description, input.Source, input.Priority, rechargeDiscountPolicy, billingDiscountPolicy,
rateLimitPolicy, quotaPolicy, metadata, input.Status,
))
}
func (s *Store) UpdateUserGroup(ctx context.Context, id string, input UserGroupInput) (UserGroup, error) {
input = normalizeUserGroupInput(input)
rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUserGroup(s.pool.QueryRow(ctx, `
UPDATE gateway_user_groups
SET group_key = $2,
name = $3,
description = NULLIF($4, ''),
source = $5,
priority = $6,
recharge_discount_policy = $7,
billing_discount_policy = $8,
rate_limit_policy = $9,
quota_policy = $10,
metadata = $11,
status = $12,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+userGroupColumns,
id, input.GroupKey, input.Name, input.Description, input.Source, input.Priority,
rechargeDiscountPolicy, billingDiscountPolicy, rateLimitPolicy, quotaPolicy, metadata, input.Status,
))
}
func (s *Store) DeleteUserGroup(ctx context.Context, id string) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = 'user_group' AND subject_id = $1::uuid`, id); err != nil {
return err
}
result, err := tx.Exec(ctx, `DELETE FROM gateway_user_groups WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return tx.Commit(ctx)
}
const tenantColumns = `
id::text, tenant_key, source, COALESCE(external_tenant_id, ''), name, COALESCE(description, ''),
COALESCE(default_user_group_id::text, ''), COALESCE(plan_key, ''), billing_profile, rate_limit_policy,
auth_policy, metadata, status, COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
created_at, updated_at`
const userColumns = `
id::text, user_key, source, COALESCE(external_user_id, ''), username,
COALESCE(display_name, ''), COALESCE(email, ''), COALESCE(phone, ''), COALESCE(avatar_url, ''),
COALESCE(gateway_tenant_id::text, ''), COALESCE(tenant_id, ''), COALESCE(tenant_key, ''),
COALESCE(default_user_group_id::text, ''), roles, auth_profile, metadata,
status, COALESCE(last_login_at::text, ''), COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
created_at, updated_at`
const userGroupColumns = `
id::text, group_key, name, COALESCE(description, ''), source, priority,
recharge_discount_policy, billing_discount_policy, rate_limit_policy, quota_policy, metadata,
status, created_at, updated_at`
type scanner interface {
Scan(dest ...any) error
}
func scanTenant(row scanner) (GatewayTenant, error) {
var item GatewayTenant
var billingProfile []byte
var rateLimitPolicy []byte
var authPolicy []byte
var metadata []byte
if err := row.Scan(
&item.ID, &item.TenantKey, &item.Source, &item.ExternalTenantID, &item.Name, &item.Description,
&item.DefaultUserGroupID, &item.PlanKey, &billingProfile, &rateLimitPolicy, &authPolicy, &metadata,
&item.Status, &item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
); err != nil {
return GatewayTenant{}, err
}
item.BillingProfile = decodeObject(billingProfile)
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
item.AuthPolicy = decodeObject(authPolicy)
item.Metadata = decodeObject(metadata)
return item, nil
}
func scanUser(row scanner) (GatewayUser, error) {
var item GatewayUser
var roles []byte
var authProfile []byte
var metadata []byte
if err := row.Scan(
&item.ID, &item.UserKey, &item.Source, &item.ExternalUserID, &item.Username, &item.DisplayName,
&item.Email, &item.Phone, &item.AvatarURL, &item.GatewayTenantID, &item.TenantID, &item.TenantKey,
&item.DefaultUserGroupID, &roles, &authProfile, &metadata, &item.Status, &item.LastLoginAt,
&item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
); err != nil {
return GatewayUser{}, err
}
item.Roles = decodeStringArray(roles)
item.AuthProfile = decodeObject(authProfile)
item.Metadata = decodeObject(metadata)
return item, nil
}
func scanUserGroup(row scanner) (UserGroup, error) {
var item UserGroup
var rechargeDiscountPolicy []byte
var billingDiscountPolicy []byte
var rateLimitPolicy []byte
var quotaPolicy []byte
var metadata []byte
if err := row.Scan(
&item.ID, &item.GroupKey, &item.Name, &item.Description, &item.Source, &item.Priority,
&rechargeDiscountPolicy, &billingDiscountPolicy, &rateLimitPolicy, &quotaPolicy, &metadata,
&item.Status, &item.CreatedAt, &item.UpdatedAt,
); err != nil {
return UserGroup{}, err
}
item.RechargeDiscountPolicy = decodeObject(rechargeDiscountPolicy)
item.BillingDiscountPolicy = decodeObject(billingDiscountPolicy)
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
item.QuotaPolicy = decodeObject(quotaPolicy)
item.Metadata = decodeObject(metadata)
return item, nil
}
func normalizeTenantInput(input GatewayTenantInput) GatewayTenantInput {
input.TenantKey = strings.TrimSpace(input.TenantKey)
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
input.Name = strings.TrimSpace(input.Name)
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
return input
}
func normalizeUserInput(input GatewayUserInput) GatewayUserInput {
input.Username = strings.TrimSpace(input.Username)
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
input.UserKey = firstNonEmpty(strings.TrimSpace(input.UserKey), input.Source+":"+input.Username)
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
input.Roles = normalizeGatewayRoles(input.Roles)
return input
}
func normalizeGatewayRoles(roles []string) []string {
for _, role := range roles {
switch strings.TrimSpace(role) {
case "manager":
return []string{"manager"}
case "admin":
return []string{"admin"}
case "operator":
return []string{"operator"}
case "creator":
return []string{"creator"}
case "user":
return []string{"user"}
}
}
return []string{"user"}
}
func normalizeUserGroupInput(input UserGroupInput) UserGroupInput {
input.GroupKey = strings.TrimSpace(input.GroupKey)
input.Name = strings.TrimSpace(input.Name)
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
if input.Priority == 0 {
input.Priority = 100
}
return input
}