feat: add admin auth identity repair binding

This commit is contained in:
IanShaw027
2026-04-20 22:22:14 +08:00
parent 3bd3027251
commit 452e55a53c
6 changed files with 628 additions and 1 deletions

View File

@@ -25,6 +25,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
router.GET("/api/v1/admin/users/auth-identity-migration-reports/summary", userHandler.GetAuthIdentityMigrationReportSummary)
router.GET("/api/v1/admin/users/auth-identity-migration-reports", userHandler.ListAuthIdentityMigrationReports)
router.GET("/api/v1/admin/users/:id", userHandler.GetByID)
router.POST("/api/v1/admin/users/:id/auth-identities", userHandler.BindAuthIdentity)
router.POST("/api/v1/admin/users", userHandler.Create)
router.PUT("/api/v1/admin/users/:id", userHandler.Update)
router.DELETE("/api/v1/admin/users/:id", userHandler.Delete)
@@ -87,8 +88,26 @@ func TestUserHandlerEndpoints(t *testing.T) {
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
bindBody := map[string]any{
"provider_type": "wechat",
"provider_key": "wechat-main",
"provider_subject": "union-123",
"metadata": map[string]any{"source": "admin-repair"},
"channel": map[string]any{
"channel": "open",
"channel_app_id": "wx-open",
"channel_subject": "openid-123",
},
}
body, _ := json.Marshal(bindBody)
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/1/auth-identities", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
createBody := map[string]any{"email": "new@example.com", "password": "pass123", "balance": 1, "concurrency": 2}
body, _ := json.Marshal(createBody)
body, _ = json.Marshal(createBody)
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
@@ -125,6 +144,33 @@ func TestUserHandlerEndpoints(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code)
}
func TestUserHandlerBindAuthIdentityMapsRequest(t *testing.T) {
router, adminSvc := setupAdminRouter()
body, err := json.Marshal(map[string]any{
"provider_type": "oidc",
"provider_key": "https://issuer.example",
"provider_subject": "subject-123",
"issuer": "https://issuer.example",
"metadata": map[string]any{"report_id": 12},
})
require.NoError(t, err)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/9/auth-identities", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, int64(9), adminSvc.boundAuthIdentityFor)
require.NotNil(t, adminSvc.boundAuthIdentity)
require.Equal(t, "oidc", adminSvc.boundAuthIdentity.ProviderType)
require.Equal(t, "https://issuer.example", adminSvc.boundAuthIdentity.ProviderKey)
require.Equal(t, "subject-123", adminSvc.boundAuthIdentity.ProviderSubject)
require.Nil(t, adminSvc.boundAuthIdentity.Channel)
require.Equal(t, float64(12), adminSvc.boundAuthIdentity.Metadata["report_id"])
}
func TestGroupHandlerEndpoints(t *testing.T) {
router, _ := setupAdminRouter()

View File

@@ -18,6 +18,8 @@ type stubAdminService struct {
proxyCounts []service.ProxyWithAccountCount
redeems []service.RedeemCode
migrationReports []service.AuthIdentityMigrationReport
boundAuthIdentity *service.AdminBindAuthIdentityInput
boundAuthIdentityFor int64
createdAccounts []*service.CreateAccountInput
createdProxies []*service.CreateProxyInput
updatedProxyIDs []int64
@@ -201,6 +203,52 @@ func (s *stubAdminService) GetAuthIdentityMigrationReportSummary(ctx context.Con
return summary, nil
}
func (s *stubAdminService) BindUserAuthIdentity(ctx context.Context, userID int64, input service.AdminBindAuthIdentityInput) (*service.AdminBoundAuthIdentity, error) {
s.boundAuthIdentityFor = userID
copied := input
if input.Metadata != nil {
copied.Metadata = map[string]any{}
for key, value := range input.Metadata {
copied.Metadata[key] = value
}
}
if input.Channel != nil {
channel := *input.Channel
if input.Channel.Metadata != nil {
channel.Metadata = map[string]any{}
for key, value := range input.Channel.Metadata {
channel.Metadata[key] = value
}
}
copied.Channel = &channel
}
s.boundAuthIdentity = &copied
now := time.Now().UTC()
result := &service.AdminBoundAuthIdentity{
UserID: userID,
ProviderType: input.ProviderType,
ProviderKey: input.ProviderKey,
ProviderSubject: input.ProviderSubject,
VerifiedAt: &now,
Issuer: input.Issuer,
Metadata: input.Metadata,
CreatedAt: now,
UpdatedAt: now,
}
if input.Channel != nil {
result.Channel = &service.AdminBoundAuthIdentityChannel{
Channel: input.Channel.Channel,
ChannelAppID: input.Channel.ChannelAppID,
ChannelSubject: input.Channel.ChannelSubject,
Metadata: input.Channel.Metadata,
CreatedAt: now,
UpdatedAt: now,
}
}
return result, nil
}
func (s *stubAdminService) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]service.Group, int64, error) {
return s.groups, int64(len(s.groups)), nil
}

View File

@@ -66,6 +66,22 @@ type UpdateBalanceRequest struct {
Notes string `json:"notes"`
}
type BindUserAuthIdentityRequest struct {
ProviderType string `json:"provider_type"`
ProviderKey string `json:"provider_key"`
ProviderSubject string `json:"provider_subject"`
Issuer *string `json:"issuer"`
Metadata map[string]any `json:"metadata"`
Channel *BindUserAuthIdentityChannelRequest `json:"channel"`
}
type BindUserAuthIdentityChannelRequest struct {
Channel string `json:"channel"`
ChannelAppID string `json:"channel_app_id"`
ChannelSubject string `json:"channel_subject"`
Metadata map[string]any `json:"metadata"`
}
// List handles listing all users with pagination
// GET /api/v1/admin/users
// Query params:
@@ -197,6 +213,45 @@ func (h *UserHandler) ListAuthIdentityMigrationReports(c *gin.Context) {
response.Paginated(c, reports, total, page, pageSize)
}
// BindAuthIdentity manually binds a canonical auth identity to a user.
// POST /api/v1/admin/users/:id/auth-identities
func (h *UserHandler) BindAuthIdentity(c *gin.Context) {
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid user ID")
return
}
var req BindUserAuthIdentityRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
input := service.AdminBindAuthIdentityInput{
ProviderType: req.ProviderType,
ProviderKey: req.ProviderKey,
ProviderSubject: req.ProviderSubject,
Issuer: req.Issuer,
Metadata: req.Metadata,
}
if req.Channel != nil {
input.Channel = &service.AdminBindAuthIdentityChannelInput{
Channel: req.Channel.Channel,
ChannelAppID: req.Channel.ChannelAppID,
ChannelSubject: req.Channel.ChannelSubject,
Metadata: req.Channel.Metadata,
}
}
result, err := h.adminService.BindUserAuthIdentity(c.Request.Context(), userID, input)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, result)
}
// Create handles creating a new user
// POST /api/v1/admin/users
func (h *UserHandler) Create(c *gin.Context) {