feat(ECR-010): Ops-E 系统运营;修复登出解绑;P2 Complete
落地管理员 RBAC/封禁/推送任务 stub,logout 解绑 device 并统一各页 ensureAccount,同时收口 P2 生日生成与状态文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
RoleSuper = "super"
|
||||
RoleOps = "ops"
|
||||
|
||||
pushTitleMaxRunes = 128
|
||||
)
|
||||
|
||||
var (
|
||||
ErrForbidden = errString("forbidden")
|
||||
ErrInvalidRole = errString("invalid role")
|
||||
ErrPushNotFound = errString("push job not found")
|
||||
ErrInvalidPush = errString("invalid push job")
|
||||
ErrAdminNotFound = errString("admin not found")
|
||||
ErrLastSuper = errString("cannot demote last super")
|
||||
)
|
||||
|
||||
// RequireSuper returns ErrForbidden unless admin role is super.
|
||||
func (s *Service) RequireSuper(ctx context.Context, adminID uuid.UUID) error {
|
||||
acc, err := s.Repo.FindAccountByID(ctx, adminID)
|
||||
if err != nil || acc == nil {
|
||||
return ErrAdminNotFound
|
||||
}
|
||||
if acc.Role != RoleSuper {
|
||||
return ErrForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BanUser sets users.status=banned.
|
||||
func (s *Service) BanUser(ctx context.Context, adminID, userID uuid.UUID) error {
|
||||
ok, err := s.Repo.UserExists(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{"status": "banned"})
|
||||
err = s.Repo.SetUserStatusWithAudit(ctx, adminID, userID, "banned", "user.ban", meta)
|
||||
if errors.Is(err, repository.ErrUserStatusNotFound) {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UnbanUser sets users.status=active.
|
||||
func (s *Service) UnbanUser(ctx context.Context, adminID, userID uuid.UUID) error {
|
||||
ok, err := s.Repo.UserExists(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{"status": "active"})
|
||||
err = s.Repo.SetUserStatusWithAudit(ctx, adminID, userID, "active", "user.unban", meta)
|
||||
if errors.Is(err, repository.ErrUserStatusNotFound) {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ListAdmins returns admin accounts (super only caller).
|
||||
func (s *Service) ListAdmins(ctx context.Context, actorID uuid.UUID) ([]repository.AdminListItem, error) {
|
||||
if err := s.RequireSuper(ctx, actorID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Repo.ListAdmins(ctx)
|
||||
}
|
||||
|
||||
// UpdateAdminRole changes an admin role (super only).
|
||||
func (s *Service) UpdateAdminRole(ctx context.Context, actorID, targetID uuid.UUID, role string) error {
|
||||
if err := s.RequireSuper(ctx, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
if role != RoleSuper && role != RoleOps {
|
||||
return ErrInvalidRole
|
||||
}
|
||||
target, err := s.Repo.FindAccountByID(ctx, targetID)
|
||||
if err != nil || target == nil {
|
||||
return ErrAdminNotFound
|
||||
}
|
||||
if target.Role == RoleSuper && role != RoleSuper {
|
||||
n, err := s.Repo.CountAdminsByRole(ctx, RoleSuper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n <= 1 {
|
||||
return ErrLastSuper
|
||||
}
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{"role": role})
|
||||
err = s.Repo.UpdateAdminRoleWithAudit(ctx, actorID, targetID, role, meta)
|
||||
if errors.Is(err, repository.ErrAdminNotFound) {
|
||||
return ErrAdminNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ListPushJobs lists push stubs.
|
||||
func (s *Service) ListPushJobs(ctx context.Context, limit, offset int) ([]repository.PushJob, error) {
|
||||
return s.Repo.ListPushJobs(ctx, limit, offset)
|
||||
}
|
||||
|
||||
// CreatePushJob creates a draft push stub.
|
||||
func (s *Service) CreatePushJob(ctx context.Context, adminID uuid.UUID, title, body, audience string) (*repository.PushJob, error) {
|
||||
if err := validatePushTitle(title); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{"title": title})
|
||||
return s.Repo.CreatePushJobWithAudit(ctx, adminID, title, body, audience, meta)
|
||||
}
|
||||
|
||||
// UpdatePushJob updates a push stub.
|
||||
func (s *Service) UpdatePushJob(
|
||||
ctx context.Context,
|
||||
adminID, jobID uuid.UUID,
|
||||
title, body, status *string,
|
||||
) (*repository.PushJob, error) {
|
||||
if status != nil && *status != "draft" && *status != "cancelled" {
|
||||
return nil, ErrInvalidPush
|
||||
}
|
||||
if title != nil {
|
||||
if err := validatePushTitle(*title); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]any{})
|
||||
job, err := s.Repo.UpdatePushJobWithAudit(ctx, adminID, jobID, title, body, status, meta)
|
||||
if errors.Is(err, repository.ErrPushJobNotFound) {
|
||||
return nil, ErrPushNotFound
|
||||
}
|
||||
return job, err
|
||||
}
|
||||
|
||||
func validatePushTitle(title string) error {
|
||||
if title == "" || utf8.RuneCountInString(title) > pushTitleMaxRunes {
|
||||
return ErrInvalidPush
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user