feat(ECR-013B): AccountLifecycle Closed;启用 Loop 连续执行
UserStatus 迁移、DeviceAuth 拒绝非 active、admin-h5 CTA; Reviewer Closed。Human 授权 LOOP_AUTHORIZATION(免逐闸确认)。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidStatusEdge = errString("invalid status transition")
|
||||
ErrReasonRequired = errString("reason required")
|
||||
)
|
||||
|
||||
var allowedStatusEdges = map[string]map[string]struct{}{
|
||||
"active": {"disabled": {}, "banned": {}, "suspended": {}},
|
||||
"disabled": {"active": {}, "banned": {}},
|
||||
"suspended": {"active": {}, "banned": {}, "disabled": {}},
|
||||
"banned": {"active": {}, "disabled": {}},
|
||||
}
|
||||
|
||||
// TransitionUserStatus migrates UserStatus with audit.
|
||||
func (s *Service) TransitionUserStatus(ctx context.Context, adminID, userID uuid.UUID, toStatus, reason string) error {
|
||||
toStatus = strings.TrimSpace(toStatus)
|
||||
reason = strings.TrimSpace(reason)
|
||||
if reason == "" {
|
||||
return ErrReasonRequired
|
||||
}
|
||||
ok, err := s.Repo.UserExists(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
from, err := s.Repo.GetUserStatus(ctx, userID)
|
||||
if err != nil || from == "" {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
if from == toStatus {
|
||||
return ErrInvalidStatusEdge
|
||||
}
|
||||
next, okEdge := allowedStatusEdges[from]
|
||||
if !okEdge {
|
||||
return ErrInvalidStatusEdge
|
||||
}
|
||||
if _, okEdge = next[toStatus]; !okEdge {
|
||||
return ErrInvalidStatusEdge
|
||||
}
|
||||
meta, _ := json.Marshal(map[string]string{
|
||||
"from": from, "to": toStatus, "reason": reason,
|
||||
})
|
||||
return s.Repo.TransitionUserStatusWithAudit(ctx, adminID, userID, from, toStatus, reason, meta)
|
||||
}
|
||||
|
||||
// ListStatusTransitions returns recent transitions.
|
||||
func (s *Service) ListStatusTransitions(ctx context.Context, userID uuid.UUID, limit int) ([]repository.AccountTransition, error) {
|
||||
ok, err := s.Repo.UserExists(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
items, err := s.Repo.ListStatusTransitions(ctx, userID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if items == nil {
|
||||
items = []repository.AccountTransition{}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -19,12 +19,14 @@ const (
|
||||
PermContentWrite = "admin.content.write"
|
||||
PermRolesRead = "admin.roles.read"
|
||||
PermRolesWrite = "admin.roles.write"
|
||||
PermUsersStatusWrite = "admin.users.status.write"
|
||||
)
|
||||
|
||||
var knownPermissions = map[string]struct{}{
|
||||
PermUsersRead: {}, PermMembershipGrant: {}, PermAskQuotaGrant: {},
|
||||
PermOrdersRead: {}, PermAuditRead: {}, PermAnalyticsRead: {},
|
||||
PermContentWrite: {}, PermRolesRead: {}, PermRolesWrite: {},
|
||||
PermUsersStatusWrite: {},
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
Reference in New Issue
Block a user