400 lines
15 KiB
Go
400 lines
15 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 {
|
|
result, err := s.pool.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 nil
|
|
}
|
|
|
|
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, "aPolicy, &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
|
|
}
|