Add BD leader admin alias support
This commit is contained in:
parent
1cffc335ce
commit
9a89e76a11
@ -347,6 +347,7 @@ type BDProfile struct {
|
||||
Role string `json:"role"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
ParentLeaderUserID int64 `json:"parentLeaderUserId,string"`
|
||||
PositionAlias string `json:"positionAlias"`
|
||||
Status string `json:"status"`
|
||||
CreatedByUserID int64 `json:"createdByUserId"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
|
||||
@ -101,7 +101,7 @@ func (h *Handler) CreateBDLeader(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "create-bd-leader", "bd_profiles", profile.UserID,
|
||||
fmt.Sprintf("command_id=%s user_id=%d region_id=%d reason=%q", strings.TrimSpace(req.CommandID), profile.UserID, req.RegionID, strings.TrimSpace(req.Reason)))
|
||||
fmt.Sprintf("command_id=%s user_id=%d region_id=%d position_alias=%q reason=%q", strings.TrimSpace(req.CommandID), profile.UserID, req.RegionID, strings.TrimSpace(req.PositionAlias), strings.TrimSpace(req.Reason)))
|
||||
response.Created(c, profile)
|
||||
}
|
||||
|
||||
|
||||
@ -146,6 +146,11 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
if err := r.fillBDProfileCreators(ctx, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if role == "bd_leader" {
|
||||
if err := r.fillBDLeaderPositionAliases(ctx, appCode, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
@ -206,6 +211,78 @@ func (r *Reader) fillBDProfileCreators(ctx context.Context, items []*userclient.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) fillBDLeaderPositionAliases(ctx context.Context, appCode string, items []*userclient.BDProfile) error {
|
||||
if r == nil || r.adminDB == nil || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.UserID > 0 {
|
||||
ids = append(ids, item.UserID)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
args := append([]any{appCode}, ids...)
|
||||
rows, err := r.adminDB.QueryContext(ctx, `
|
||||
SELECT user_id, position_alias
|
||||
FROM admin_bd_leader_position_aliases
|
||||
WHERE app_code = ? AND user_id IN (`+sqlPlaceholders(len(ids))+`)
|
||||
`, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
aliases := make(map[int64]string, len(ids))
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
var alias string
|
||||
if err := rows.Scan(&userID, &alias); err != nil {
|
||||
return err
|
||||
}
|
||||
aliases[userID] = strings.TrimSpace(alias)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
item.PositionAlias = aliases[item.UserID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) SaveBDLeaderPositionAlias(ctx context.Context, userID int64, actorID int64, alias string) error {
|
||||
if r == nil || r.adminDB == nil {
|
||||
return fmt.Errorf("admin mysql is not configured")
|
||||
}
|
||||
if userID <= 0 {
|
||||
return fmt.Errorf("user_id is required")
|
||||
}
|
||||
appCode := appctx.FromContext(ctx)
|
||||
positionAlias := strings.TrimSpace(alias)
|
||||
if positionAlias == "" {
|
||||
// 空别名表示恢复 H5 配置表里的默认 admin 展示名,删除行比保留空值更清晰。
|
||||
_, err := r.adminDB.ExecContext(ctx, `
|
||||
DELETE FROM admin_bd_leader_position_aliases
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, appCode, userID)
|
||||
return err
|
||||
}
|
||||
nowMs := time.Now().UnixMilli()
|
||||
_, err := r.adminDB.ExecContext(ctx, `
|
||||
INSERT INTO admin_bd_leader_position_aliases (
|
||||
app_code, user_id, position_alias, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
position_alias = VALUES(position_alias),
|
||||
updated_by_admin_id = VALUES(updated_by_admin_id),
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, appCode, userID, positionAlias, actorID, actorID, nowMs, nowMs)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.BDProfile, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errUserDBNotConfigured()
|
||||
@ -247,6 +324,12 @@ func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.
|
||||
if affected == 0 {
|
||||
return nil, fmt.Errorf("bd leader not found")
|
||||
}
|
||||
if r.adminDB != nil {
|
||||
_, _ = r.adminDB.ExecContext(ctx, `
|
||||
DELETE FROM admin_bd_leader_position_aliases
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, appCode, userID)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
|
||||
@ -14,10 +14,11 @@ type listQuery struct {
|
||||
}
|
||||
|
||||
type createBDLeaderRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
TargetUserID int64 `json:"targetUserId" binding:"required"`
|
||||
RegionID int64 `json:"regionId" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
TargetUserID int64 `json:"targetUserId" binding:"required"`
|
||||
RegionID int64 `json:"regionId" binding:"required"`
|
||||
PositionAlias string `json:"positionAlias"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type createBDRequest struct {
|
||||
|
||||
@ -18,6 +18,7 @@ const (
|
||||
coinSellerStockTypePurchase = "usdt_purchase"
|
||||
coinSellerStockTypeCompensate = "coin_compensation"
|
||||
paidCurrencyUSDT = "USDT"
|
||||
bdLeaderPositionAliasMaxRunes = 64
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@ -118,8 +119,12 @@ func (s *Service) CreateBDLeader(ctx context.Context, actorID int64, requestID s
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
positionAlias := strings.TrimSpace(req.PositionAlias)
|
||||
if len([]rune(positionAlias)) > bdLeaderPositionAliasMaxRunes {
|
||||
return nil, fmt.Errorf("position_alias must not exceed %d characters", bdLeaderPositionAliasMaxRunes)
|
||||
}
|
||||
// BD Leader 创建时后台选择的区域是显式运营归属,由 user-service 在同一事务里更新 users.region_id 和 bd_profiles.region_id。
|
||||
return s.userClient.CreateBDLeader(ctx, userclient.CreateBDLeaderRequest{
|
||||
profile, err := s.userClient.CreateBDLeader(ctx, userclient.CreateBDLeaderRequest{
|
||||
RequestID: requestID,
|
||||
Caller: "hyapp-admin-server",
|
||||
CommandID: strings.TrimSpace(req.CommandID),
|
||||
@ -128,6 +133,15 @@ func (s *Service) CreateBDLeader(ctx context.Context, actorID int64, requestID s
|
||||
TargetUserID: targetUserID,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 职位别名只影响 Flutter H5 配置里 admin 入口的展示名,不参与 user-service 的身份创建事务,避免把后台展示字段扩散到 App 身份主协议。
|
||||
if err := s.reader.SaveBDLeaderPositionAlias(ctx, targetUserID, actorID, positionAlias); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.PositionAlias = positionAlias
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateBD(ctx context.Context, actorID int64, requestID string, req createBDRequest) (*userclient.BDProfile, error) {
|
||||
|
||||
13
server/admin/migrations/034_bd_leader_position_alias.sql
Normal file
13
server/admin/migrations/034_bd_leader_position_alias.sql
Normal file
@ -0,0 +1,13 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_bd_leader_position_aliases (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
user_id BIGINT NOT NULL COMMENT 'BD Leader 用户 ID',
|
||||
position_alias VARCHAR(64) NOT NULL DEFAULT '' COMMENT '职位别名,用于覆盖 App H5 admin 配置展示名',
|
||||
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建管理员 ID',
|
||||
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新管理员 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, user_id),
|
||||
KEY idx_admin_bd_leader_position_alias_updated (app_code, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BD Leader 职位别名配置表';
|
||||
@ -5,15 +5,21 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const h5LinkGroup = "h5-links"
|
||||
|
||||
const listH5LinksSQL = "SELECT `key`, COALESCE(description, ''), COALESCE(value, ''), updated_at_ms FROM admin_app_configs WHERE `group` = ? ORDER BY `key` ASC"
|
||||
const bdLeaderPositionAliasSQL = `
|
||||
SELECT position_alias
|
||||
FROM admin_bd_leader_position_aliases
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
LIMIT 1`
|
||||
const listExploreTabsSQL = `
|
||||
SELECT id, app_code, tab, h5_url, enabled, sort_order, updated_at_ms
|
||||
FROM admin_app_explore_tabs
|
||||
@ -44,6 +50,12 @@ type H5Link struct {
|
||||
UpdatedAtMs int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
// H5LinkQuery 是 H5 入口配置的可选用户态筛选条件。
|
||||
type H5LinkQuery struct {
|
||||
AppCode string
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// ExploreTab 是后台 APP配置/Explore配置 中启用的 H5 tab。
|
||||
type ExploreTab struct {
|
||||
ID uint `json:"id"`
|
||||
@ -105,7 +117,7 @@ type Version struct {
|
||||
|
||||
// Reader 是 HTTP 层读取 H5 配置的最小依赖。
|
||||
type Reader interface {
|
||||
ListH5Links(ctx context.Context) ([]H5Link, error)
|
||||
ListH5Links(ctx context.Context, query H5LinkQuery) ([]H5Link, error)
|
||||
ListExploreTabs(ctx context.Context, appCode string) ([]ExploreTab, error)
|
||||
ListBanners(ctx context.Context, query BannerQuery) ([]Banner, error)
|
||||
LatestVersion(ctx context.Context, query VersionQuery) (Version, error)
|
||||
@ -136,7 +148,7 @@ func (r *MySQLReader) Close() error {
|
||||
}
|
||||
|
||||
// ListH5Links 返回后台动态维护的 H5 入口集合。
|
||||
func (r *MySQLReader) ListH5Links(ctx context.Context) ([]H5Link, error) {
|
||||
func (r *MySQLReader) ListH5Links(ctx context.Context, query H5LinkQuery) ([]H5Link, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("app config reader is not configured")
|
||||
}
|
||||
@ -164,9 +176,33 @@ func (r *MySQLReader) ListH5Links(ctx context.Context) ([]H5Link, error) {
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alias, err := r.bdLeaderPositionAlias(ctx, query); err != nil {
|
||||
return nil, err
|
||||
} else if alias != "" {
|
||||
applyAdminH5LinkAlias(items, alias)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *MySQLReader) bdLeaderPositionAlias(ctx context.Context, query H5LinkQuery) (string, error) {
|
||||
if query.UserID <= 0 {
|
||||
return "", nil
|
||||
}
|
||||
var alias string
|
||||
err := r.db.QueryRowContext(ctx, bdLeaderPositionAliasSQL, normalizeAppCode(query.AppCode), query.UserID).Scan(&alias)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
if isMissingTableError(err) {
|
||||
// gateway 读的是 admin 库;迁移未发布前保持 H5 配置原始返回,不让可选别名阻断老环境。
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(alias), nil
|
||||
}
|
||||
|
||||
// ListExploreTabs 返回当前 App 启用的 Explore H5 tabs;后台关闭的 tab 不下发给客户端。
|
||||
func (r *MySQLReader) ListExploreTabs(ctx context.Context, appCode string) ([]ExploreTab, error) {
|
||||
if r == nil || r.db == nil {
|
||||
@ -276,21 +312,25 @@ func (r *MySQLReader) LatestVersion(ctx context.Context, query VersionQuery) (Ve
|
||||
|
||||
// StaticReader 给测试和临时环境提供内存版 H5 配置读取。
|
||||
type StaticReader struct {
|
||||
Links []H5Link
|
||||
ExploreTabs []ExploreTab
|
||||
Banners []Banner
|
||||
Version Version
|
||||
Err error
|
||||
Links []H5Link
|
||||
ExploreTabs []ExploreTab
|
||||
Banners []Banner
|
||||
Version Version
|
||||
Err error
|
||||
PositionAliases map[string]string
|
||||
}
|
||||
|
||||
// ListH5Links 返回预置 H5 配置。
|
||||
func (r StaticReader) ListH5Links(context.Context) ([]H5Link, error) {
|
||||
func (r StaticReader) ListH5Links(_ context.Context, query H5LinkQuery) ([]H5Link, error) {
|
||||
if r.Err != nil {
|
||||
return nil, r.Err
|
||||
}
|
||||
|
||||
out := make([]H5Link, len(r.Links))
|
||||
copy(out, r.Links)
|
||||
if alias := strings.TrimSpace(r.PositionAliases[bdLeaderPositionAliasKey(query.AppCode, query.UserID)]); alias != "" {
|
||||
applyAdminH5LinkAlias(out, alias)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@ -352,6 +392,28 @@ func normalizeCountry(value string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func applyAdminH5LinkAlias(items []H5Link, alias string) {
|
||||
alias = strings.TrimSpace(alias)
|
||||
if alias == "" {
|
||||
return
|
||||
}
|
||||
for index := range items {
|
||||
if strings.EqualFold(strings.TrimSpace(items[index].Key), "admin") {
|
||||
// admin 入口只覆盖展示名,key 和 URL 仍来自后台 H5 配置,保证 Flutter 路由识别不变。
|
||||
items[index].Label = alias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bdLeaderPositionAliasKey(appCode string, userID int64) string {
|
||||
return normalizeAppCode(appCode) + ":" + strconv.FormatInt(userID, 10)
|
||||
}
|
||||
|
||||
func isMissingTableError(err error) bool {
|
||||
var mysqlErr *mysqlDriver.MySQLError
|
||||
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1146
|
||||
}
|
||||
|
||||
// NormalizeBannerDisplayScope 统一 App 和后台约定的显示范围枚举。
|
||||
func NormalizeBannerDisplayScope(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/services/gateway-service/internal/appconfig"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
)
|
||||
|
||||
@ -85,7 +86,10 @@ func (h *Handler) listH5Links(writer http.ResponseWriter, request *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.appConfigReader.ListH5Links(request.Context())
|
||||
items, err := h.appConfigReader.ListH5Links(request.Context(), appconfig.H5LinkQuery{
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserID: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
// 配置读取失败属于服务端依赖异常,客户端只需要根据 request_id 排查。
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
|
||||
// ConfigReader 是 gateway 对后台 App 配置的只读依赖。
|
||||
type ConfigReader interface {
|
||||
ListH5Links(ctx context.Context) ([]appconfig.H5Link, error)
|
||||
ListH5Links(ctx context.Context, query appconfig.H5LinkQuery) ([]appconfig.H5Link, error)
|
||||
ListExploreTabs(ctx context.Context, appCode string) ([]appconfig.ExploreTab, error)
|
||||
ListBanners(ctx context.Context, query appconfig.BannerQuery) ([]appconfig.Banner, error)
|
||||
LatestVersion(ctx context.Context, query appconfig.VersionQuery) (appconfig.Version, error)
|
||||
|
||||
@ -26,8 +26,8 @@ func (h *Handler) profileAPIHandler(jwtVerifier *auth.Verifier, next http.Handle
|
||||
}
|
||||
|
||||
// publicAPIHandler 包装只需要 app 解析的公开 API;登录/注册会用解析结果创建对应 App 的用户。
|
||||
func (h *Handler) publicAPIHandler(next http.HandlerFunc) http.Handler {
|
||||
return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(next)))
|
||||
func (h *Handler) publicAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
|
||||
return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withOptionalAuth(jwtVerifier, next))))
|
||||
}
|
||||
|
||||
// withAccessLog 统一记录 gateway 业务 API 的请求体、响应体、状态码和耗时。
|
||||
@ -63,6 +63,32 @@ func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) ht
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) withOptionalAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
header := strings.TrimSpace(request.Header.Get("Authorization"))
|
||||
if header == "" || jwtVerifier == nil {
|
||||
next.ServeHTTP(writer, request)
|
||||
return
|
||||
}
|
||||
claims, err := jwtVerifier.Verify(header)
|
||||
if err != nil || strings.TrimSpace(claims.SessionID) == "" {
|
||||
// 公开接口不能因为旧客户端带了过期或残缺 token 就失败;无效 token 只是不参与用户态配置覆盖。
|
||||
next.ServeHTTP(writer, request)
|
||||
return
|
||||
}
|
||||
ctx := auth.WithClaims(request.Context(), claims)
|
||||
revoked, err := h.revokedSession(ctx, claims.AppCode, claims.SessionID)
|
||||
if err != nil {
|
||||
logx.Warn(ctx, "optional_session_denylist_read_failed", slog.String("component", "gateway_public_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error()))
|
||||
}
|
||||
if revoked {
|
||||
next.ServeHTTP(writer, request)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(writer, request.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) revokedSession(ctx context.Context, appCode string, sessionID string) (bool, error) {
|
||||
if h.loginRiskCache == nil {
|
||||
return false, nil
|
||||
|
||||
@ -2995,6 +2995,45 @@ func TestListH5LinksReturnsAdminAppConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListH5LinksUsesBDLeaderPositionAliasForAdminItem(t *testing.T) {
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetAppConfigReader(appconfig.StaticReader{
|
||||
Links: []appconfig.H5Link{
|
||||
{Key: "admin", Label: "Admin Center", URL: "https://h5.example.com/admin", UpdatedAtMs: 1700000000000},
|
||||
{Key: "bd", Label: "BD Center", URL: "https://h5.example.com/bd", UpdatedAtMs: 1700000001000},
|
||||
},
|
||||
PositionAliases: map[string]string{"lalu:42": "Senior Admin"},
|
||||
})
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/h5-links", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-h5-links-alias")
|
||||
request.Header.Set("X-App-Package", "com.org.laluparty")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data, ok := response.Data.(map[string]any)
|
||||
if response.Code != httpkit.CodeOK || !ok {
|
||||
t.Fatalf("unexpected envelope: %+v", response)
|
||||
}
|
||||
items, ok := data["items"].([]any)
|
||||
if !ok || len(items) != 2 {
|
||||
t.Fatalf("h5 items shape mismatch: %+v", data)
|
||||
}
|
||||
first, ok := items[0].(map[string]any)
|
||||
if !ok || first["key"] != "admin" || first["label"] != "Senior Admin" || first["url"] != "https://h5.example.com/admin" {
|
||||
t.Fatalf("admin h5 alias mismatch: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListH5LinksRequiresConfigReader(t *testing.T) {
|
||||
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}).Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/h5-links", nil)
|
||||
|
||||
@ -119,7 +119,9 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
userHandlers.UnequipMyResource = resourceAPI.UnequipMyResource
|
||||
|
||||
return httproutes.New(httproutes.Config{
|
||||
PublicWrap: h.publicAPIHandler,
|
||||
PublicWrap: func(handler http.HandlerFunc) http.Handler {
|
||||
return h.publicAPIHandler(jwtVerifier, handler)
|
||||
},
|
||||
AuthWrap: func(handler http.HandlerFunc) http.Handler {
|
||||
return h.apiHandler(jwtVerifier, handler)
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user