package applogic import ( "errors" "fmt" "net/http" "regexp" "strings" "aijianzhan/platform/internal/agentcap" "aijianzhan/platform/internal/agentstore" "aijianzhan/platform/internal/authx" "aijianzhan/platform/internal/types" "aijianzhan/platform/internal/yuhticket" ) // YuhengRestoreByHostReq Z34b:仅凭宇恒账号 host_key 恢复已有同步绑定。 type YuhengRestoreByHostReq struct { Ticket string `json:"ticket"` HostKey string `json:"host_key"` // 须与票内一致(= 宇恒用户 _id) } // YuhengRestoreByHostResp 与绑定成功对齐,便于宇恒 apply_bind_success。 type YuhengRestoreByHostResp struct { *types.TokenResp OK bool `json:"ok"` SyncBound bool `json:"sync_bound"` ClientID string `json:"client_id,omitempty"` ClientSecret string `json:"client_secret,omitempty"` ChannelID string `json:"channel_id,omitempty"` OnlineDBID string `json:"online_db_id,omitempty"` TenantID int64 `json:"tenant_id,omitempty"` AgentID int64 `json:"agent_id,omitempty"` HostKey string `json:"host_key,omitempty"` PhoneMasked string `json:"phone_masked,omitempty"` Message string `json:"message,omitempty"` } // RestoreBindError 映射 HTTP 状态(404 need_bind / 401 / 403)。 type RestoreBindError struct { HTTPStatus int Message string NeedBind bool } func (e *RestoreBindError) Error() string { if e == nil { return "" } return e.Message } func restoreNeedBind(msg string) error { return &RestoreBindError{HTTPStatus: http.StatusNotFound, Message: msg, NeedBind: true} } func restoreUnauthorized(msg string) error { return &RestoreBindError{HTTPStatus: http.StatusUnauthorized, Message: msg} } func restoreForbidden(msg string) error { return &RestoreBindError{HTTPStatus: http.StatusForbidden, Message: msg} } var onlineUserSuffix = regexp.MustCompile(`_u(\d+)$`) // RestoreByYuhengHost Z34b:验签凭票 → FindByHostKey → 已 sync_bound 则轮换密钥并回传落点。 func (l *AuthLogic) RestoreByYuhengHost(req YuhengRestoreByHostReq) (*YuhengRestoreByHostResp, error) { cfg := l.svcCtx.Config.Agent.YuhengTicket if !cfg.Enabled { return nil, restoreUnauthorized("宇恒凭票未启用(Agent.YuhengTicket.Enabled)") } if strings.TrimSpace(cfg.Secret) == "" { return nil, restoreUnauthorized("宇恒凭票 Secret 未配置") } if l.svcCtx.Agents == nil { return nil, fmt.Errorf("bind service unavailable") } claims, err := yuhticket.Verify(req.Ticket, yuhticket.VerifyOpts{ Secret: cfg.Secret, Issuer: cfg.Issuer, Audience: cfg.Audience, AllowEmptyPhone: true, AllowedScopes: []string{"", "sync_bind", "sync_restore"}, }) if err != nil { msg := err.Error() if strings.Contains(msg, "expired") || strings.Contains(msg, "signature") || strings.Contains(msg, "already used") || strings.Contains(msg, "invalid") || strings.Contains(msg, "not allowed") || strings.Contains(msg, "mismatch") || strings.Contains(msg, "未配置") { return nil, restoreUnauthorized(msg) } return nil, restoreUnauthorized(msg) } hostKey := strings.TrimSpace(req.HostKey) if hostKey == "" { hostKey = strings.TrimSpace(claims.HostKey) } if hostKey == "" || hostKey != strings.TrimSpace(claims.HostKey) { return nil, restoreForbidden("host_key 与凭票不一致") } if yid := strings.TrimSpace(claims.YuhengUserID); yid != "" && yid != hostKey { return nil, restoreForbidden("yuheng_user_id 与 host_key 不一致") } if l.svcCtx.YuhengJTI != nil { if err := l.svcCtx.YuhengJTI.Consume(claims.JTI, claims.Exp); err != nil { return nil, restoreUnauthorized(err.Error()) } } acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey) if err != nil || acc == nil { return nil, restoreNeedBind("无此 host_key 同步绑定;请先绑定码/手机号绑定") } // 通道误删时先自愈再判断 sync_bound if _, hErr := HealTenantSyncBind(l.ctx, l.svcCtx, acc.TenantID); hErr == nil { if refreshed, gErr := l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID); gErr == nil && refreshed != nil { acc = refreshed } } syncBound := strings.TrimSpace(acc.ChannelID) != "" && strings.TrimSpace(acc.OnlineDBID) != "" if acc.Status != agentstore.StatusActive || !syncBound { return nil, restoreNeedBind("host_key 尚未完成同步绑定;请先绑定码/手机号绑定") } if err := l.ensureAgentSyncPerm(acc); err != nil { return nil, err } acc, err = l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID) if err != nil { return nil, err } secret, err := l.svcCtx.Agents.RotateSecret(l.ctx, acc.TenantID, acc.AgentID) if err != nil { return nil, fmt.Errorf("rotate client_secret: %w", err) } token, exp, err := authx.IssueAgentToken(l.svcCtx.JWT, acc.TenantID, acc.AgentID, acc.Perms) if err != nil { return nil, err } _ = l.svcCtx.Agents.TouchToken(l.ctx, acc.AgentID) capSecret := l.svcCtx.Config.Agent.CapsuleSecret if capSecret == "" { capSecret = l.svcCtx.JWT.AccessSecret } tr := &types.TokenResp{ AccessToken: token, TokenType: "Bearer", ExpiresAt: exp, TenantID: acc.TenantID, UserID: acc.AgentID, Username: acc.ClientID, DisplayName: acc.Name, Role: authx.RoleAgent, AgentKey: agentcap.PublicAgentKey(capSecret, acc.TenantID, acc.AgentID), AgentID: acc.AgentID, Permissions: append([]string{}, acc.Perms...), AppSlugs: append([]string{}, acc.AppSlugs...), } fillAgentSyncOnToken(tr, acc) phoneMasked := maskPhoneDigits(strings.TrimSpace(claims.Phone)) if phoneMasked == "" { phoneMasked = l.phoneMaskedForAgent(acc) } _ = l.writeBindAudit("yuheng_restore_by_host", acc.TenantID, acc.AgentID, map[string]any{ "host_key": hostKey, "jti": claims.JTI, "yuheng_user_id": claims.YuhengUserID, }) return &YuhengRestoreByHostResp{ TokenResp: tr, OK: true, SyncBound: true, ClientID: acc.ClientID, ClientSecret: secret, ChannelID: acc.ChannelID, OnlineDBID: acc.OnlineDBID, TenantID: acc.TenantID, AgentID: acc.AgentID, HostKey: hostKey, PhoneMasked: phoneMasked, Message: "已按宇恒账号恢复同步绑定", }, nil } func maskPhoneDigits(phone string) string { phone = strings.TrimSpace(phone) if len(phone) < 7 { return "" } // 11 位国内号:135****1944 if len(phone) == 11 { return phone[:3] + "****" + phone[7:] } keep := 3 if len(phone) < keep*2 { return phone[:1] + "****" } return phone[:keep] + "****" + phone[len(phone)-keep:] } func (l *AuthLogic) phoneMaskedForAgent(acc *agentstore.Account) string { if acc == nil || l.svcCtx.Users == nil { return "" } // 优先 online_db_id 后缀 _u{userID} if m := onlineUserSuffix.FindStringSubmatch(strings.TrimSpace(acc.OnlineDBID)); len(m) == 2 { var uid int64 _, _ = fmt.Sscanf(m[1], "%d", &uid) if uid > 0 { if u, err := l.svcCtx.Users.GetByID(l.ctx, uid); err == nil && u != nil { return maskPhoneDigits(u.Phone) } } } // 回落:同租户 Binding 上挂该 online_db_id 的用户 if l.svcCtx.DBSync == nil { return "" } list, err := l.svcCtx.DBSync.Store().ListBindingsFiltered(acc.TenantID, 0, "") if err != nil { return "" } online := strings.TrimSpace(acc.OnlineDBID) for _, b := range list { if strings.TrimSpace(b.OnlineDBID) != online || b.UserID <= 0 { continue } if u, err := l.svcCtx.Users.GetByID(l.ctx, b.UserID); err == nil && u != nil { if m := maskPhoneDigits(u.Phone); m != "" { return m } } } return "" } // AsRestoreBindError 供 handler 取 HTTP 状态。 func AsRestoreBindError(err error) *RestoreBindError { var e *RestoreBindError if errors.As(err, &e) { return e } return nil }