membership_plans 表、admin 套餐页、Grant/CreateOrder 读表; Loop continuous 自动 Approve/Closed。Next:ECR-015 RedemptionCode。 Co-authored-by: Cursor <cursoragent@cursor.com>
140 lines
4.4 KiB
Go
140 lines
4.4 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Permission catalog frozen in ECR-013A Spec.
|
|
const (
|
|
PermUsersRead = "admin.users.read"
|
|
PermMembershipGrant = "admin.users.membership.grant"
|
|
PermAskQuotaGrant = "admin.users.ask_quota.grant"
|
|
PermOrdersRead = "admin.orders.read"
|
|
PermAuditRead = "admin.audit.read"
|
|
PermAnalyticsRead = "admin.analytics.read"
|
|
PermContentWrite = "admin.content.write"
|
|
PermRolesRead = "admin.roles.read"
|
|
PermRolesWrite = "admin.roles.write"
|
|
PermUsersStatusWrite = "admin.users.status.write"
|
|
PermMembershipPlansRead = "admin.membership.plans.read"
|
|
PermMembershipPlansWrite = "admin.membership.plans.write"
|
|
)
|
|
|
|
var knownPermissions = map[string]struct{}{
|
|
PermUsersRead: {}, PermMembershipGrant: {}, PermAskQuotaGrant: {},
|
|
PermOrdersRead: {}, PermAuditRead: {}, PermAnalyticsRead: {},
|
|
PermContentWrite: {}, PermRolesRead: {}, PermRolesWrite: {},
|
|
PermUsersStatusWrite: {}, PermMembershipPlansRead: {}, PermMembershipPlansWrite: {},
|
|
}
|
|
|
|
var (
|
|
ErrForbidden = errString("forbidden")
|
|
ErrRoleNotFound = errString("role not found")
|
|
ErrInvalidPerm = errString("invalid permission code")
|
|
ErrProtectSystem = errString("system role protected")
|
|
)
|
|
|
|
// AdminMe is the public admin profile (with RBAC).
|
|
type AdminMe struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Username string `json:"username"`
|
|
Role string `json:"role,omitempty"`
|
|
Permissions []string `json:"permissions"`
|
|
}
|
|
|
|
// RoleDTO is list/detail payload.
|
|
type RoleDTO struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
System bool `json:"system"`
|
|
Permissions []string `json:"permissions,omitempty"`
|
|
}
|
|
|
|
// HasPermission reports whether admin holds code.
|
|
func (s *Service) HasPermission(ctx context.Context, adminID uuid.UUID, code string) (bool, error) {
|
|
perms, err := s.Repo.ListPermissionsForAdmin(ctx, adminID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, p := range perms {
|
|
if p == code {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// DenyPermission audits a forbidden attempt.
|
|
func (s *Service) DenyPermission(ctx context.Context, adminID uuid.UUID, code, path string) {
|
|
meta, _ := json.Marshal(map[string]string{"permission": code, "path": path})
|
|
_ = s.Repo.InsertAudit(ctx, adminID, "permission.denied", "permission", code, meta)
|
|
}
|
|
|
|
// Me returns the current admin profile with permissions.
|
|
func (s *Service) Me(ctx context.Context, adminID uuid.UUID) (*AdminMe, error) {
|
|
acc, err := s.Repo.FindAccountByID(ctx, adminID)
|
|
if err != nil || acc == nil {
|
|
return nil, errors.New("admin not found")
|
|
}
|
|
perms, err := s.Repo.ListPermissionsForAdmin(ctx, adminID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if perms == nil {
|
|
perms = []string{}
|
|
}
|
|
_, roleName, _ := s.Repo.GetAdminRoleMeta(ctx, adminID)
|
|
return &AdminMe{ID: acc.ID, Username: acc.Username, Role: roleName, Permissions: perms}, nil
|
|
}
|
|
|
|
// ListRoles returns roles without permissions.
|
|
func (s *Service) ListRoles(ctx context.Context) ([]RoleDTO, error) {
|
|
roles, err := s.Repo.ListAdminRoles(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]RoleDTO, 0, len(roles))
|
|
for _, r := range roles {
|
|
out = append(out, RoleDTO{ID: r.ID, Name: r.Name, System: r.System})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// GetRole returns role + permissions.
|
|
func (s *Service) GetRole(ctx context.Context, id uuid.UUID) (*RoleDTO, error) {
|
|
role, err := s.Repo.GetAdminRole(ctx, id)
|
|
if err != nil || role == nil {
|
|
return nil, ErrRoleNotFound
|
|
}
|
|
perms, err := s.Repo.ListRolePermissions(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if perms == nil {
|
|
perms = []string{}
|
|
}
|
|
return &RoleDTO{ID: role.ID, Name: role.Name, System: role.System, Permissions: perms}, nil
|
|
}
|
|
|
|
// ReplaceRolePermissions updates permissions with audit.
|
|
func (s *Service) ReplaceRolePermissions(ctx context.Context, adminID, roleID uuid.UUID, codes []string) error {
|
|
role, err := s.Repo.GetAdminRole(ctx, roleID)
|
|
if err != nil || role == nil {
|
|
return ErrRoleNotFound
|
|
}
|
|
for _, c := range codes {
|
|
if _, ok := knownPermissions[c]; !ok {
|
|
return ErrInvalidPerm
|
|
}
|
|
}
|
|
if err := s.Repo.ReplaceRolePermissions(ctx, roleID, codes); err != nil {
|
|
return err
|
|
}
|
|
meta, _ := json.Marshal(map[string]any{"permissions": codes, "role": role.Name})
|
|
return s.Repo.InsertAudit(ctx, adminID, "roles.permissions.update", "admin_role", roleID.String(), meta)
|
|
}
|