chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
419
platform/internal/rolestore/store.go
Normal file
419
platform/internal/rolestore/store.go
Normal file
@@ -0,0 +1,419 @@
|
||||
package rolestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
RoleID int64 `json:"role_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permissions []string `json:"permissions"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CreateInput struct {
|
||||
Code string
|
||||
Name string
|
||||
Description string
|
||||
Permissions []string
|
||||
}
|
||||
|
||||
type UpdateInput struct {
|
||||
Name *string
|
||||
Description *string
|
||||
Permissions *[]string
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
List(ctx context.Context, tenantID int64) ([]Role, error)
|
||||
Get(ctx context.Context, tenantID, roleID int64) (*Role, error)
|
||||
Create(ctx context.Context, tenantID int64, in CreateInput) (*Role, error)
|
||||
Update(ctx context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error)
|
||||
Delete(ctx context.Context, tenantID, roleID int64) error
|
||||
EnsureDefaults(ctx context.Context, tenantID int64) error
|
||||
}
|
||||
|
||||
var defaultRoles = []CreateInput{
|
||||
{
|
||||
Code: authx.AgentRole生成发布, Name: "生成发布", Description: "生成蓝图、发布应用并导入业务数据(智能体常用最低权限)",
|
||||
Permissions: []string{
|
||||
"读取模块", "发布模块",
|
||||
"导入数据", "查询数据",
|
||||
"上传文件", "下载文件",
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: authx.AgentRole只读, Name: "只读", Description: "读应用与数据,可导出,不可改写",
|
||||
Permissions: []string{"读取模块", "查询数据", "导出数据", "下载文件", "查看审计"},
|
||||
},
|
||||
{
|
||||
Code: authx.AgentRole读写, Name: "读写", Description: "读写业务数据与导入导出,不可发布模块",
|
||||
Permissions: []string{
|
||||
"读取模块", "写入模块",
|
||||
"新增数据", "查询数据", "更新数据", "导出数据", "导入数据",
|
||||
"上传文件", "下载文件",
|
||||
},
|
||||
},
|
||||
{
|
||||
Code: authx.AgentRole运维, Name: "运维", Description: "含发布、删除及全量行操作,适合运维类智能体",
|
||||
Permissions: []string{
|
||||
"读取模块", "写入模块", "发布模块",
|
||||
"新增数据", "查询数据", "更新数据", "删除数据", "导出数据", "导入数据",
|
||||
"上传文件", "下载文件", "查看审计",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type memRole struct{ Role }
|
||||
|
||||
type MemoryStore struct {
|
||||
mu sync.Mutex
|
||||
seq int64
|
||||
by map[int64]*memRole
|
||||
}
|
||||
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
return &MemoryStore{by: map[int64]*memRole{}}
|
||||
}
|
||||
|
||||
func findDefaultRole(byCode map[string]Role, d CreateInput) (Role, bool) {
|
||||
if r, ok := byCode[d.Code]; ok {
|
||||
return r, true
|
||||
}
|
||||
legacy := map[string]string{
|
||||
"publisher": authx.AgentRole生成发布,
|
||||
"viewer": authx.AgentRole只读,
|
||||
"editor": authx.AgentRole读写,
|
||||
"operator": authx.AgentRole运维,
|
||||
}
|
||||
for eng, zh := range legacy {
|
||||
if zh == d.Code {
|
||||
if r, ok := byCode[eng]; ok {
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return Role{}, false
|
||||
}
|
||||
|
||||
func (s *MemoryStore) EnsureDefaults(ctx context.Context, tenantID int64) error {
|
||||
list, err := s.List(ctx, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byCode := map[string]Role{}
|
||||
for _, r := range list {
|
||||
byCode[r.Code] = r
|
||||
}
|
||||
for _, d := range defaultRoles {
|
||||
if existing, ok := findDefaultRole(byCode, d); ok {
|
||||
if d.Code == authx.AgentRole生成发布 {
|
||||
perms := append([]string{}, d.Permissions...)
|
||||
if _, err := s.Update(ctx, tenantID, existing.RoleID, UpdateInput{
|
||||
Name: &d.Name,
|
||||
Description: &d.Description,
|
||||
Permissions: &perms,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := s.Create(ctx, tenantID, d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) List(_ context.Context, tenantID int64) ([]Role, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := []Role{}
|
||||
for _, r := range s.by {
|
||||
if r.TenantID == tenantID {
|
||||
out = append(out, clone(r.Role))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Get(_ context.Context, tenantID, roleID int64) (*Role, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
r, ok := s.by[roleID]
|
||||
if !ok || r.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("role not found")
|
||||
}
|
||||
cp := clone(r.Role)
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Create(_ context.Context, tenantID int64, in CreateInput) (*Role, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
code := strings.TrimSpace(in.Code)
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if code == "" || name == "" {
|
||||
return nil, fmt.Errorf("code and name required")
|
||||
}
|
||||
for _, r := range s.by {
|
||||
if r.TenantID == tenantID && r.Code == code {
|
||||
return nil, fmt.Errorf("role code already exists")
|
||||
}
|
||||
}
|
||||
s.seq++
|
||||
r := &memRole{Role: Role{
|
||||
RoleID: s.seq,
|
||||
TenantID: tenantID,
|
||||
Code: code,
|
||||
Name: name,
|
||||
Description: strings.TrimSpace(in.Description),
|
||||
Permissions: uniq(in.Permissions),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}}
|
||||
s.by[r.RoleID] = r
|
||||
cp := clone(r.Role)
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Update(_ context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
r, ok := s.by[roleID]
|
||||
if !ok || r.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("role not found")
|
||||
}
|
||||
if in.Name != nil {
|
||||
r.Name = strings.TrimSpace(*in.Name)
|
||||
}
|
||||
if in.Description != nil {
|
||||
r.Description = strings.TrimSpace(*in.Description)
|
||||
}
|
||||
if in.Permissions != nil {
|
||||
r.Permissions = uniq(*in.Permissions)
|
||||
}
|
||||
cp := clone(r.Role)
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Delete(_ context.Context, tenantID, roleID int64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
r, ok := s.by[roleID]
|
||||
if !ok || r.TenantID != tenantID {
|
||||
return fmt.Errorf("role not found")
|
||||
}
|
||||
delete(s.by, roleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
type PostgresStore struct{ DB *sql.DB }
|
||||
|
||||
func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} }
|
||||
|
||||
func (s *PostgresStore) EnsureDefaults(ctx context.Context, tenantID int64) error {
|
||||
list, err := s.List(ctx, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byCode := map[string]Role{}
|
||||
for _, r := range list {
|
||||
byCode[r.Code] = r
|
||||
}
|
||||
for _, d := range defaultRoles {
|
||||
if existing, ok := findDefaultRole(byCode, d); ok {
|
||||
// 英文旧编码升级为中文
|
||||
if existing.Code != d.Code {
|
||||
if _, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.roles SET code=$1, name=$2 WHERE role_id=$3 AND tenant_id=$4`,
|
||||
d.Code, d.Name, existing.RoleID, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
existing.Code = d.Code
|
||||
existing.Name = d.Name
|
||||
byCode[d.Code] = existing
|
||||
}
|
||||
if d.Code == authx.AgentRole生成发布 {
|
||||
perms := append([]string{}, d.Permissions...)
|
||||
if _, err := s.Update(ctx, tenantID, existing.RoleID, UpdateInput{
|
||||
Name: &d.Name,
|
||||
Description: &d.Description,
|
||||
Permissions: &perms,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := s.Create(ctx, tenantID, d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) List(ctx context.Context, tenantID int64) ([]Role, error) {
|
||||
rows, err := s.DB.QueryContext(ctx, `
|
||||
SELECT role_id, tenant_id, code, name, description, created_at
|
||||
FROM platform_meta.roles WHERE tenant_id=$1 ORDER BY role_id`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Role
|
||||
for rows.Next() {
|
||||
var r Role
|
||||
if err := rows.Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perms, err := s.loadPerms(ctx, r.RoleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Permissions = perms
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *PostgresStore) Get(ctx context.Context, tenantID, roleID int64) (*Role, error) {
|
||||
var r Role
|
||||
err := s.DB.QueryRowContext(ctx, `
|
||||
SELECT role_id, tenant_id, code, name, description, created_at
|
||||
FROM platform_meta.roles WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID,
|
||||
).Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, fmt.Errorf("role not found")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perms, err := s.loadPerms(ctx, r.RoleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Permissions = perms
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) Create(ctx context.Context, tenantID int64, in CreateInput) (*Role, error) {
|
||||
code := strings.TrimSpace(in.Code)
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if code == "" || name == "" {
|
||||
return nil, fmt.Errorf("code and name required")
|
||||
}
|
||||
var r Role
|
||||
err := s.DB.QueryRowContext(ctx, `
|
||||
INSERT INTO platform_meta.roles(tenant_id, code, name, description)
|
||||
VALUES($1,$2,$3,$4)
|
||||
RETURNING role_id, tenant_id, code, name, description, created_at`,
|
||||
tenantID, code, name, strings.TrimSpace(in.Description),
|
||||
).Scan(&r.RoleID, &r.TenantID, &r.Code, &r.Name, &r.Description, &r.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perms := uniq(in.Permissions)
|
||||
if err := s.replacePerms(ctx, r.RoleID, perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Permissions = perms
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) Update(ctx context.Context, tenantID, roleID int64, in UpdateInput) (*Role, error) {
|
||||
r, err := s.Get(ctx, tenantID, roleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, desc := r.Name, r.Description
|
||||
if in.Name != nil {
|
||||
name = strings.TrimSpace(*in.Name)
|
||||
}
|
||||
if in.Description != nil {
|
||||
desc = strings.TrimSpace(*in.Description)
|
||||
}
|
||||
if _, err := s.DB.ExecContext(ctx, `
|
||||
UPDATE platform_meta.roles SET name=$1, description=$2 WHERE role_id=$3 AND tenant_id=$4`,
|
||||
name, desc, roleID, tenantID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Permissions != nil {
|
||||
if err := s.replacePerms(ctx, roleID, uniq(*in.Permissions)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s.Get(ctx, tenantID, roleID)
|
||||
}
|
||||
|
||||
func (s *PostgresStore) Delete(ctx context.Context, tenantID, roleID int64) error {
|
||||
_, _ = s.DB.ExecContext(ctx, `
|
||||
UPDATE platform_meta.agent_accounts SET role_id=NULL WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID)
|
||||
res, err := s.DB.ExecContext(ctx, `
|
||||
DELETE FROM platform_meta.roles WHERE role_id=$1 AND tenant_id=$2`, roleID, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("role not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) loadPerms(ctx context.Context, roleID int64) ([]string, error) {
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT perm FROM platform_meta.role_permissions WHERE role_id=$1`, roleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []string
|
||||
for rows.Next() {
|
||||
var p string
|
||||
if err := rows.Scan(&p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authx.NormalizePerms(out), nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) replacePerms(ctx context.Context, roleID int64, perms []string) error {
|
||||
tx, err := s.DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.role_permissions WHERE role_id=$1`, roleID); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range authx.NormalizePerms(perms) {
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO platform_meta.role_permissions(role_id, perm) VALUES($1,$2)`, roleID, p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func clone(r Role) Role {
|
||||
cp := r
|
||||
cp.Permissions = append([]string{}, r.Permissions...)
|
||||
return cp
|
||||
}
|
||||
|
||||
func uniq(in []string) []string {
|
||||
return authx.NormalizePerms(in)
|
||||
}
|
||||
Reference in New Issue
Block a user