feat: resolve auth identity migration reports
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
servermiddleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,10 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
|
|||||||
router.GET("/api/v1/admin/users", userHandler.List)
|
router.GET("/api/v1/admin/users", userHandler.List)
|
||||||
router.GET("/api/v1/admin/users/auth-identity-migration-reports/summary", userHandler.GetAuthIdentityMigrationReportSummary)
|
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/auth-identity-migration-reports", userHandler.ListAuthIdentityMigrationReports)
|
||||||
|
router.POST("/api/v1/admin/users/auth-identity-migration-reports/:id/resolve", func(c *gin.Context) {
|
||||||
|
c.Set(string(servermiddleware.ContextKeyUser), servermiddleware.AuthSubject{UserID: 99})
|
||||||
|
userHandler.ResolveAuthIdentityMigrationReport(c)
|
||||||
|
})
|
||||||
router.GET("/api/v1/admin/users/:id", userHandler.GetByID)
|
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/:id/auth-identities", userHandler.BindAuthIdentity)
|
||||||
router.POST("/api/v1/admin/users", userHandler.Create)
|
router.POST("/api/v1/admin/users", userHandler.Create)
|
||||||
@@ -83,6 +88,13 @@ func TestUserHandlerEndpoints(t *testing.T) {
|
|||||||
router.ServeHTTP(rec, req)
|
router.ServeHTTP(rec, req)
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{"resolution_note": "resolved by manual bind"})
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/auth-identity-migration-reports/1/resolve", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1", nil)
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/users/1", nil)
|
||||||
router.ServeHTTP(rec, req)
|
router.ServeHTTP(rec, req)
|
||||||
@@ -99,7 +111,7 @@ func TestUserHandlerEndpoints(t *testing.T) {
|
|||||||
"channel_subject": "openid-123",
|
"channel_subject": "openid-123",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(bindBody)
|
body, _ = json.Marshal(bindBody)
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/1/auth-identities", bytes.NewReader(body))
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/users/1/auth-identities", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -249,6 +249,20 @@ func (s *stubAdminService) BindUserAuthIdentity(ctx context.Context, userID int6
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) ResolveAuthIdentityMigrationReport(ctx context.Context, reportID, resolvedByUserID int64, resolutionNote string) (*service.AuthIdentityMigrationReport, error) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
for i := range s.migrationReports {
|
||||||
|
if s.migrationReports[i].ID != reportID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.migrationReports[i].ResolvedAt = &now
|
||||||
|
s.migrationReports[i].ResolvedByUserID = &resolvedByUserID
|
||||||
|
s.migrationReports[i].ResolutionNote = resolutionNote
|
||||||
|
return &s.migrationReports[i], nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]service.Group, int64, error) {
|
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
|
return s.groups, int64(len(s.groups)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
|
servermiddleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -82,6 +83,10 @@ type BindUserAuthIdentityChannelRequest struct {
|
|||||||
Metadata map[string]any `json:"metadata"`
|
Metadata map[string]any `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResolveAuthIdentityMigrationReportRequest struct {
|
||||||
|
ResolutionNote string `json:"resolution_note"`
|
||||||
|
}
|
||||||
|
|
||||||
// List handles listing all users with pagination
|
// List handles listing all users with pagination
|
||||||
// GET /api/v1/admin/users
|
// GET /api/v1/admin/users
|
||||||
// Query params:
|
// Query params:
|
||||||
@@ -252,6 +257,40 @@ func (h *UserHandler) BindAuthIdentity(c *gin.Context) {
|
|||||||
response.Success(c, result)
|
response.Success(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveAuthIdentityMigrationReport marks a migration report as resolved.
|
||||||
|
// POST /api/v1/admin/users/auth-identity-migration-reports/:id/resolve
|
||||||
|
func (h *UserHandler) ResolveAuthIdentityMigrationReport(c *gin.Context) {
|
||||||
|
reportID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "Invalid report ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, ok := servermiddleware.GetAuthSubjectFromContext(c)
|
||||||
|
if !ok || subject.UserID <= 0 {
|
||||||
|
response.Unauthorized(c, "Authentication required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req ResolveAuthIdentityMigrationReportRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := h.adminService.ResolveAuthIdentityMigrationReport(
|
||||||
|
c.Request.Context(),
|
||||||
|
reportID,
|
||||||
|
subject.UserID,
|
||||||
|
req.ResolutionNote,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, report)
|
||||||
|
}
|
||||||
|
|
||||||
// Create handles creating a new user
|
// Create handles creating a new user
|
||||||
// POST /api/v1/admin/users
|
// POST /api/v1/admin/users
|
||||||
func (h *UserHandler) Create(c *gin.Context) {
|
func (h *UserHandler) Create(c *gin.Context) {
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ func registerUserManagementRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
{
|
{
|
||||||
users.GET("/auth-identity-migration-reports/summary", h.Admin.User.GetAuthIdentityMigrationReportSummary)
|
users.GET("/auth-identity-migration-reports/summary", h.Admin.User.GetAuthIdentityMigrationReportSummary)
|
||||||
users.GET("/auth-identity-migration-reports", h.Admin.User.ListAuthIdentityMigrationReports)
|
users.GET("/auth-identity-migration-reports", h.Admin.User.ListAuthIdentityMigrationReports)
|
||||||
|
users.POST("/auth-identity-migration-reports/:id/resolve", h.Admin.User.ResolveAuthIdentityMigrationReport)
|
||||||
users.GET("", h.Admin.User.List)
|
users.GET("", h.Admin.User.List)
|
||||||
users.GET("/:id", h.Admin.User.GetByID)
|
users.GET("/:id", h.Admin.User.GetByID)
|
||||||
users.POST("/:id/auth-identities", h.Admin.User.BindAuthIdentity)
|
users.POST("/:id/auth-identities", h.Admin.User.BindAuthIdentity)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type AdminService interface {
|
|||||||
ListAuthIdentityMigrationReports(ctx context.Context, reportType string, page, pageSize int) ([]AuthIdentityMigrationReport, int64, error)
|
ListAuthIdentityMigrationReports(ctx context.Context, reportType string, page, pageSize int) ([]AuthIdentityMigrationReport, int64, error)
|
||||||
GetAuthIdentityMigrationReportSummary(ctx context.Context) (*AuthIdentityMigrationReportSummary, error)
|
GetAuthIdentityMigrationReportSummary(ctx context.Context) (*AuthIdentityMigrationReportSummary, error)
|
||||||
BindUserAuthIdentity(ctx context.Context, userID int64, input AdminBindAuthIdentityInput) (*AdminBoundAuthIdentity, error)
|
BindUserAuthIdentity(ctx context.Context, userID int64, input AdminBindAuthIdentityInput) (*AdminBoundAuthIdentity, error)
|
||||||
|
ResolveAuthIdentityMigrationReport(ctx context.Context, reportID, resolvedByUserID int64, resolutionNote string) (*AuthIdentityMigrationReport, error)
|
||||||
|
|
||||||
// Group management
|
// Group management
|
||||||
ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]Group, int64, error)
|
ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]Group, int64, error)
|
||||||
@@ -137,16 +138,21 @@ type UpdateUserInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AuthIdentityMigrationReport struct {
|
type AuthIdentityMigrationReport struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
ReportType string `json:"report_type"`
|
ReportType string `json:"report_type"`
|
||||||
ReportKey string `json:"report_key"`
|
ReportKey string `json:"report_key"`
|
||||||
Details map[string]any `json:"details"`
|
Details map[string]any `json:"details"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||||
|
ResolvedByUserID *int64 `json:"resolved_by_user_id,omitempty"`
|
||||||
|
ResolutionNote string `json:"resolution_note,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthIdentityMigrationReportSummary struct {
|
type AuthIdentityMigrationReportSummary struct {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
ByType map[string]int64 `json:"by_type"`
|
OpenTotal int64 `json:"open_total"`
|
||||||
|
ResolvedTotal int64 `json:"resolved_total"`
|
||||||
|
ByType map[string]int64 `json:"by_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminBindAuthIdentityInput struct {
|
type AdminBindAuthIdentityInput struct {
|
||||||
@@ -874,7 +880,7 @@ WHERE ($1 = '' OR report_type = $1)`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := db.QueryContext(ctx, `
|
||||||
SELECT id, report_type, report_key, details, created_at
|
SELECT id, report_type, report_key, details, created_at, resolved_at, resolved_by_user_id, resolution_note
|
||||||
FROM auth_identity_migration_reports
|
FROM auth_identity_migration_reports
|
||||||
WHERE ($1 = '' OR report_type = $1)
|
WHERE ($1 = '' OR report_type = $1)
|
||||||
ORDER BY created_at DESC, id DESC
|
ORDER BY created_at DESC, id DESC
|
||||||
@@ -909,7 +915,11 @@ func (s *adminServiceImpl) GetAuthIdentityMigrationReportSummary(ctx context.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.QueryContext(ctx, `
|
rows, err := db.QueryContext(ctx, `
|
||||||
SELECT report_type, COUNT(*)
|
SELECT
|
||||||
|
report_type,
|
||||||
|
COUNT(*),
|
||||||
|
SUM(CASE WHEN resolved_at IS NULL THEN 1 ELSE 0 END),
|
||||||
|
SUM(CASE WHEN resolved_at IS NOT NULL THEN 1 ELSE 0 END)
|
||||||
FROM auth_identity_migration_reports
|
FROM auth_identity_migration_reports
|
||||||
GROUP BY report_type
|
GROUP BY report_type
|
||||||
ORDER BY report_type ASC`)
|
ORDER BY report_type ASC`)
|
||||||
@@ -924,11 +934,15 @@ ORDER BY report_type ASC`)
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var reportType string
|
var reportType string
|
||||||
var count int64
|
var count int64
|
||||||
if err := rows.Scan(&reportType, &count); err != nil {
|
var openCount int64
|
||||||
|
var resolvedCount int64
|
||||||
|
if err := rows.Scan(&reportType, &count, &openCount, &resolvedCount); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
summary.ByType[reportType] = count
|
summary.ByType[reportType] = count
|
||||||
summary.Total += count
|
summary.Total += count
|
||||||
|
summary.OpenTotal += openCount
|
||||||
|
summary.ResolvedTotal += resolvedCount
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -936,6 +950,56 @@ ORDER BY report_type ASC`)
|
|||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *adminServiceImpl) ResolveAuthIdentityMigrationReport(ctx context.Context, reportID, resolvedByUserID int64, resolutionNote string) (*AuthIdentityMigrationReport, error) {
|
||||||
|
if reportID <= 0 {
|
||||||
|
return nil, infraerrors.BadRequest("INVALID_INPUT", "report id must be greater than 0")
|
||||||
|
}
|
||||||
|
if resolvedByUserID <= 0 {
|
||||||
|
return nil, infraerrors.BadRequest("INVALID_INPUT", "resolved_by_user_id must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := s.adminSQLDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
result, err := db.ExecContext(ctx, `
|
||||||
|
UPDATE auth_identity_migration_reports
|
||||||
|
SET
|
||||||
|
resolved_at = COALESCE(resolved_at, $2),
|
||||||
|
resolved_by_user_id = COALESCE(resolved_by_user_id, $3),
|
||||||
|
resolution_note = $4
|
||||||
|
WHERE id = $1`,
|
||||||
|
reportID,
|
||||||
|
now,
|
||||||
|
resolvedByUserID,
|
||||||
|
strings.TrimSpace(resolutionNote),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return nil, infraerrors.NotFound("AUTH_IDENTITY_MIGRATION_REPORT_NOT_FOUND", "auth identity migration report not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
row := db.QueryRowContext(ctx, `
|
||||||
|
SELECT id, report_type, report_key, details, created_at, resolved_at, resolved_by_user_id, resolution_note
|
||||||
|
FROM auth_identity_migration_reports
|
||||||
|
WHERE id = $1`,
|
||||||
|
reportID,
|
||||||
|
)
|
||||||
|
report, err := scanAuthIdentityMigrationReport(row)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &report, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adminServiceImpl) BindUserAuthIdentity(ctx context.Context, userID int64, input AdminBindAuthIdentityInput) (*AdminBoundAuthIdentity, error) {
|
func (s *adminServiceImpl) BindUserAuthIdentity(ctx context.Context, userID int64, input AdminBindAuthIdentityInput) (*AdminBoundAuthIdentity, error) {
|
||||||
if userID <= 0 {
|
if userID <= 0 {
|
||||||
return nil, infraerrors.BadRequest("INVALID_INPUT", "user_id must be greater than 0")
|
return nil, infraerrors.BadRequest("INVALID_INPUT", "user_id must be greater than 0")
|
||||||
@@ -1170,10 +1234,22 @@ func cloneAdminAuthIdentityMetadata(input map[string]any) map[string]any {
|
|||||||
|
|
||||||
func scanAuthIdentityMigrationReport(scanner interface{ Scan(dest ...any) error }) (AuthIdentityMigrationReport, error) {
|
func scanAuthIdentityMigrationReport(scanner interface{ Scan(dest ...any) error }) (AuthIdentityMigrationReport, error) {
|
||||||
var (
|
var (
|
||||||
report AuthIdentityMigrationReport
|
report AuthIdentityMigrationReport
|
||||||
details []byte
|
details []byte
|
||||||
|
resolvedAt sql.NullTime
|
||||||
|
resolvedByUserID sql.NullInt64
|
||||||
|
resolutionNote sql.NullString
|
||||||
)
|
)
|
||||||
if err := scanner.Scan(&report.ID, &report.ReportType, &report.ReportKey, &details, &report.CreatedAt); err != nil {
|
if err := scanner.Scan(
|
||||||
|
&report.ID,
|
||||||
|
&report.ReportType,
|
||||||
|
&report.ReportKey,
|
||||||
|
&details,
|
||||||
|
&report.CreatedAt,
|
||||||
|
&resolvedAt,
|
||||||
|
&resolvedByUserID,
|
||||||
|
&resolutionNote,
|
||||||
|
); err != nil {
|
||||||
return AuthIdentityMigrationReport{}, err
|
return AuthIdentityMigrationReport{}, err
|
||||||
}
|
}
|
||||||
report.Details = map[string]any{}
|
report.Details = map[string]any{}
|
||||||
@@ -1182,6 +1258,15 @@ func scanAuthIdentityMigrationReport(scanner interface{ Scan(dest ...any) error
|
|||||||
return AuthIdentityMigrationReport{}, err
|
return AuthIdentityMigrationReport{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if resolvedAt.Valid {
|
||||||
|
report.ResolvedAt = &resolvedAt.Time
|
||||||
|
}
|
||||||
|
if resolvedByUserID.Valid {
|
||||||
|
report.ResolvedByUserID = &resolvedByUserID.Int64
|
||||||
|
}
|
||||||
|
if resolutionNote.Valid {
|
||||||
|
report.ResolutionNote = resolutionNote.String
|
||||||
|
}
|
||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ func newAdminServiceMigrationReportTestClient(t *testing.T) *dbent.Client {
|
|||||||
report_type TEXT NOT NULL,
|
report_type TEXT NOT NULL,
|
||||||
report_key TEXT NOT NULL,
|
report_key TEXT NOT NULL,
|
||||||
details TEXT NOT NULL DEFAULT '{}',
|
details TEXT NOT NULL DEFAULT '{}',
|
||||||
created_at DATETIME NOT NULL
|
created_at DATETIME NOT NULL,
|
||||||
|
resolved_at DATETIME NULL,
|
||||||
|
resolved_by_user_id INTEGER NULL,
|
||||||
|
resolution_note TEXT NOT NULL DEFAULT ''
|
||||||
)`)
|
)`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -87,6 +90,35 @@ VALUES
|
|||||||
summary, err := svc.GetAuthIdentityMigrationReportSummary(context.Background())
|
summary, err := svc.GetAuthIdentityMigrationReportSummary(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, int64(3), summary.Total)
|
require.Equal(t, int64(3), summary.Total)
|
||||||
|
require.Equal(t, int64(3), summary.OpenTotal)
|
||||||
|
require.Zero(t, summary.ResolvedTotal)
|
||||||
require.Equal(t, int64(1), summary.ByType["oidc_synthetic_email_requires_manual_recovery"])
|
require.Equal(t, int64(1), summary.ByType["oidc_synthetic_email_requires_manual_recovery"])
|
||||||
require.Equal(t, int64(2), summary.ByType["wechat_provider_key_conflict"])
|
require.Equal(t, int64(2), summary.ByType["wechat_provider_key_conflict"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdminServiceResolveAuthIdentityMigrationReport(t *testing.T) {
|
||||||
|
client := newAdminServiceMigrationReportTestClient(t)
|
||||||
|
driver, ok := client.Driver().(*entsql.Driver)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
_, err := driver.DB().ExecContext(context.Background(), `
|
||||||
|
INSERT INTO auth_identity_migration_reports (report_type, report_key, details, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4)`,
|
||||||
|
"oidc_synthetic_email_requires_manual_recovery", "u-1", `{"user_id":1}`, now,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
svc := &adminServiceImpl{entClient: client}
|
||||||
|
report, err := svc.ResolveAuthIdentityMigrationReport(context.Background(), 1, 99, "resolved by admin binding")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, report.ResolvedAt)
|
||||||
|
require.NotNil(t, report.ResolvedByUserID)
|
||||||
|
require.Equal(t, int64(99), *report.ResolvedByUserID)
|
||||||
|
require.Equal(t, "resolved by admin binding", report.ResolutionNote)
|
||||||
|
|
||||||
|
summary, err := svc.GetAuthIdentityMigrationReportSummary(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Zero(t, summary.OpenTotal)
|
||||||
|
require.Equal(t, int64(1), summary.ResolvedTotal)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE auth_identity_migration_reports
|
||||||
|
ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMPTZ NULL;
|
||||||
|
|
||||||
|
ALTER TABLE auth_identity_migration_reports
|
||||||
|
ADD COLUMN IF NOT EXISTS resolved_by_user_id BIGINT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE auth_identity_migration_reports
|
||||||
|
ADD COLUMN IF NOT EXISTS resolution_note TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_auth_identity_migration_reports_resolved_at
|
||||||
|
ON auth_identity_migration_reports (resolved_at);
|
||||||
Reference in New Issue
Block a user