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 (
|
||||
|
||||
@@ -35,6 +35,9 @@ type SessionResult struct {
|
||||
User Me `json:"user"`
|
||||
}
|
||||
|
||||
// ErrAccountRestricted is returned when UserStatus is not active.
|
||||
var ErrAccountRestricted = errors.New("账户已受限")
|
||||
|
||||
// Register upgrades or opens an account (same open rules as Login).
|
||||
func (s *Service) Register(ctx context.Context, userID uuid.UUID, deviceKey, phone, password, nickname string) (*SessionResult, error) {
|
||||
return s.OpenLogin(ctx, userID, deviceKey, phone, password, nickname)
|
||||
@@ -60,6 +63,9 @@ func (s *Service) OpenLogin(ctx context.Context, deviceUserID uuid.UUID, deviceK
|
||||
|
||||
acc, err := s.Repo.GetByPhone(ctx, phone)
|
||||
if err == nil {
|
||||
if acc.Status != "active" {
|
||||
return nil, ErrAccountRestricted
|
||||
}
|
||||
_ = s.Repo.TouchPassword(ctx, acc.ID, hashStr)
|
||||
if deviceKey != "" {
|
||||
_ = s.Repo.BindDevice(ctx, deviceKey, acc.ID)
|
||||
@@ -78,6 +84,9 @@ func (s *Service) OpenLogin(ctx context.Context, deviceUserID uuid.UUID, deviceK
|
||||
cur, curErr := s.Repo.GetAccount(ctx, deviceUserID)
|
||||
uid := deviceUserID
|
||||
if curErr == nil && cur.Phone == "" {
|
||||
if cur.Status != "active" {
|
||||
return nil, ErrAccountRestricted
|
||||
}
|
||||
if err := s.Repo.RegisterOnUser(ctx, deviceUserID, phone, hashStr, nickname); err != nil {
|
||||
return nil, errors.New("登录失败,请重试")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user