Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
3bd30272
Commit
3bd30272
authored
Apr 20, 2026
by
IanShaw027
Browse files
feat: expose auth identity migration reports
parent
aaf4946b
Changes
6
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/admin_basic_handlers_test.go
View file @
3bd30272
...
...
@@ -22,6 +22,8 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
redeemHandler
:=
NewRedeemHandler
(
adminSvc
,
nil
)
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"
,
userHandler
.
ListAuthIdentityMigrationReports
)
router
.
GET
(
"/api/v1/admin/users/:id"
,
userHandler
.
GetByID
)
router
.
POST
(
"/api/v1/admin/users"
,
userHandler
.
Create
)
router
.
PUT
(
"/api/v1/admin/users/:id"
,
userHandler
.
Update
)
...
...
@@ -70,6 +72,16 @@ func TestUserHandlerEndpoints(t *testing.T) {
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/users/auth-identity-migration-reports/summary"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/users/auth-identity-migration-reports?report_type=oidc_synthetic_email_requires_manual_recovery"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/users/1"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
...
...
backend/internal/handler/admin/admin_service_stub_test.go
View file @
3bd30272
...
...
@@ -17,6 +17,7 @@ type stubAdminService struct {
proxies
[]
service
.
Proxy
proxyCounts
[]
service
.
ProxyWithAccountCount
redeems
[]
service
.
RedeemCode
migrationReports
[]
service
.
AuthIdentityMigrationReport
createdAccounts
[]
*
service
.
CreateAccountInput
createdProxies
[]
*
service
.
CreateProxyInput
updatedProxyIDs
[]
int64
...
...
@@ -123,6 +124,15 @@ func newStubAdminService() *stubAdminService {
proxies
:
[]
service
.
Proxy
{
proxy
},
proxyCounts
:
[]
service
.
ProxyWithAccountCount
{{
Proxy
:
proxy
,
AccountCount
:
1
}},
redeems
:
[]
service
.
RedeemCode
{
redeem
},
migrationReports
:
[]
service
.
AuthIdentityMigrationReport
{
{
ID
:
1
,
ReportType
:
"oidc_synthetic_email_requires_manual_recovery"
,
ReportKey
:
"u-1"
,
Details
:
map
[
string
]
any
{
"user_id"
:
1
},
CreatedAt
:
now
,
},
},
}
}
...
...
@@ -167,6 +177,30 @@ func (s *stubAdminService) GetUserUsageStats(ctx context.Context, userID int64,
return
map
[
string
]
any
{
"user_id"
:
userID
},
nil
}
func
(
s
*
stubAdminService
)
ListAuthIdentityMigrationReports
(
ctx
context
.
Context
,
reportType
string
,
page
,
pageSize
int
)
([]
service
.
AuthIdentityMigrationReport
,
int64
,
error
)
{
if
reportType
==
""
{
return
s
.
migrationReports
,
int64
(
len
(
s
.
migrationReports
)),
nil
}
filtered
:=
make
([]
service
.
AuthIdentityMigrationReport
,
0
,
len
(
s
.
migrationReports
))
for
_
,
report
:=
range
s
.
migrationReports
{
if
strings
.
EqualFold
(
report
.
ReportType
,
reportType
)
{
filtered
=
append
(
filtered
,
report
)
}
}
return
filtered
,
int64
(
len
(
filtered
)),
nil
}
func
(
s
*
stubAdminService
)
GetAuthIdentityMigrationReportSummary
(
ctx
context
.
Context
)
(
*
service
.
AuthIdentityMigrationReportSummary
,
error
)
{
summary
:=
&
service
.
AuthIdentityMigrationReportSummary
{
ByType
:
map
[
string
]
int64
{},
}
for
_
,
report
:=
range
s
.
migrationReports
{
summary
.
Total
++
summary
.
ByType
[
report
.
ReportType
]
++
}
return
summary
,
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
}
...
...
backend/internal/handler/admin/user_handler.go
View file @
3bd30272
...
...
@@ -172,6 +172,31 @@ func (h *UserHandler) GetByID(c *gin.Context) {
response
.
Success
(
c
,
dto
.
UserFromServiceAdmin
(
user
))
}
// GetAuthIdentityMigrationReportSummary returns aggregate migration report counts.
// GET /api/v1/admin/users/auth-identity-migration-reports/summary
func
(
h
*
UserHandler
)
GetAuthIdentityMigrationReportSummary
(
c
*
gin
.
Context
)
{
summary
,
err
:=
h
.
adminService
.
GetAuthIdentityMigrationReportSummary
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
summary
)
}
// ListAuthIdentityMigrationReports returns paginated auth identity migration reports.
// GET /api/v1/admin/users/auth-identity-migration-reports
func
(
h
*
UserHandler
)
ListAuthIdentityMigrationReports
(
c
*
gin
.
Context
)
{
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
reportType
:=
strings
.
TrimSpace
(
c
.
Query
(
"report_type"
))
reports
,
total
,
err
:=
h
.
adminService
.
ListAuthIdentityMigrationReports
(
c
.
Request
.
Context
(),
reportType
,
page
,
pageSize
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Paginated
(
c
,
reports
,
total
,
page
,
pageSize
)
}
// Create handles creating a new user
// POST /api/v1/admin/users
func
(
h
*
UserHandler
)
Create
(
c
*
gin
.
Context
)
{
...
...
backend/internal/server/routes/admin.go
View file @
3bd30272
...
...
@@ -210,6 +210,8 @@ func registerDashboardRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
func
registerUserManagementRoutes
(
admin
*
gin
.
RouterGroup
,
h
*
handler
.
Handlers
)
{
users
:=
admin
.
Group
(
"/users"
)
{
users
.
GET
(
"/auth-identity-migration-reports/summary"
,
h
.
Admin
.
User
.
GetAuthIdentityMigrationReportSummary
)
users
.
GET
(
"/auth-identity-migration-reports"
,
h
.
Admin
.
User
.
ListAuthIdentityMigrationReports
)
users
.
GET
(
""
,
h
.
Admin
.
User
.
List
)
users
.
GET
(
"/:id"
,
h
.
Admin
.
User
.
GetByID
)
users
.
POST
(
""
,
h
.
Admin
.
User
.
Create
)
...
...
backend/internal/service/admin_service.go
View file @
3bd30272
...
...
@@ -2,6 +2,8 @@ package service
import
(
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
...
...
@@ -16,6 +18,8 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/util/httputil"
entsql
"entgo.io/ent/dialect/sql"
)
// AdminService interface defines admin management operations
...
...
@@ -33,6 +37,8 @@ type AdminService interface {
// codeType is optional - pass empty string to return all types.
// Also returns totalRecharged (sum of all positive balance top-ups).
GetUserBalanceHistory
(
ctx
context
.
Context
,
userID
int64
,
page
,
pageSize
int
,
codeType
string
)
([]
RedeemCode
,
int64
,
float64
,
error
)
ListAuthIdentityMigrationReports
(
ctx
context
.
Context
,
reportType
string
,
page
,
pageSize
int
)
([]
AuthIdentityMigrationReport
,
int64
,
error
)
GetAuthIdentityMigrationReportSummary
(
ctx
context
.
Context
)
(
*
AuthIdentityMigrationReportSummary
,
error
)
// Group management
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
,
search
string
,
isExclusive
*
bool
,
sortBy
,
sortOrder
string
)
([]
Group
,
int64
,
error
)
...
...
@@ -127,6 +133,19 @@ type UpdateUserInput struct {
GroupRates
map
[
int64
]
*
float64
}
type
AuthIdentityMigrationReport
struct
{
ID
int64
`json:"id"`
ReportType
string
`json:"report_type"`
ReportKey
string
`json:"report_key"`
Details
map
[
string
]
any
`json:"details"`
CreatedAt
time
.
Time
`json:"created_at"`
}
type
AuthIdentityMigrationReportSummary
struct
{
Total
int64
`json:"total"`
ByType
map
[
string
]
int64
`json:"by_type"`
}
type
CreateGroupInput
struct
{
Name
string
Description
string
...
...
@@ -788,6 +807,122 @@ func (s *adminServiceImpl) GetUserBalanceHistory(ctx context.Context, userID int
return
codes
,
result
.
Total
,
totalRecharged
,
nil
}
func
(
s
*
adminServiceImpl
)
ListAuthIdentityMigrationReports
(
ctx
context
.
Context
,
reportType
string
,
page
,
pageSize
int
)
([]
AuthIdentityMigrationReport
,
int64
,
error
)
{
db
,
err
:=
s
.
adminSQLDB
()
if
err
!=
nil
{
return
nil
,
0
,
err
}
reportType
=
strings
.
TrimSpace
(
reportType
)
if
page
<=
0
{
page
=
1
}
if
pageSize
<=
0
{
pageSize
=
20
}
offset
:=
(
page
-
1
)
*
pageSize
var
total
int64
if
err
:=
db
.
QueryRowContext
(
ctx
,
`
SELECT COUNT(*)
FROM auth_identity_migration_reports
WHERE ($1 = '' OR report_type = $1)`
,
reportType
,
)
.
Scan
(
&
total
);
err
!=
nil
{
return
nil
,
0
,
err
}
rows
,
err
:=
db
.
QueryContext
(
ctx
,
`
SELECT id, report_type, report_key, details, created_at
FROM auth_identity_migration_reports
WHERE ($1 = '' OR report_type = $1)
ORDER BY created_at DESC, id DESC
LIMIT $2 OFFSET $3`
,
reportType
,
pageSize
,
offset
,
)
if
err
!=
nil
{
return
nil
,
0
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
reports
:=
make
([]
AuthIdentityMigrationReport
,
0
)
for
rows
.
Next
()
{
report
,
scanErr
:=
scanAuthIdentityMigrationReport
(
rows
)
if
scanErr
!=
nil
{
return
nil
,
0
,
scanErr
}
reports
=
append
(
reports
,
report
)
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
0
,
err
}
return
reports
,
total
,
nil
}
func
(
s
*
adminServiceImpl
)
GetAuthIdentityMigrationReportSummary
(
ctx
context
.
Context
)
(
*
AuthIdentityMigrationReportSummary
,
error
)
{
db
,
err
:=
s
.
adminSQLDB
()
if
err
!=
nil
{
return
nil
,
err
}
rows
,
err
:=
db
.
QueryContext
(
ctx
,
`
SELECT report_type, COUNT(*)
FROM auth_identity_migration_reports
GROUP BY report_type
ORDER BY report_type ASC`
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
summary
:=
&
AuthIdentityMigrationReportSummary
{
ByType
:
make
(
map
[
string
]
int64
),
}
for
rows
.
Next
()
{
var
reportType
string
var
count
int64
if
err
:=
rows
.
Scan
(
&
reportType
,
&
count
);
err
!=
nil
{
return
nil
,
err
}
summary
.
ByType
[
reportType
]
=
count
summary
.
Total
+=
count
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
summary
,
nil
}
func
(
s
*
adminServiceImpl
)
adminSQLDB
()
(
*
sql
.
DB
,
error
)
{
if
s
==
nil
||
s
.
entClient
==
nil
{
return
nil
,
infraerrors
.
ServiceUnavailable
(
"ADMIN_SQL_NOT_READY"
,
"admin sql access is not ready"
)
}
driver
,
ok
:=
s
.
entClient
.
Driver
()
.
(
*
entsql
.
Driver
)
if
!
ok
||
driver
.
DB
()
==
nil
{
return
nil
,
infraerrors
.
ServiceUnavailable
(
"ADMIN_SQL_NOT_READY"
,
"admin sql access is not ready"
)
}
return
driver
.
DB
(),
nil
}
func
scanAuthIdentityMigrationReport
(
scanner
interface
{
Scan
(
dest
...
any
)
error
})
(
AuthIdentityMigrationReport
,
error
)
{
var
(
report
AuthIdentityMigrationReport
details
[]
byte
)
if
err
:=
scanner
.
Scan
(
&
report
.
ID
,
&
report
.
ReportType
,
&
report
.
ReportKey
,
&
details
,
&
report
.
CreatedAt
);
err
!=
nil
{
return
AuthIdentityMigrationReport
{},
err
}
report
.
Details
=
map
[
string
]
any
{}
if
len
(
details
)
>
0
{
if
err
:=
json
.
Unmarshal
(
details
,
&
report
.
Details
);
err
!=
nil
{
return
AuthIdentityMigrationReport
{},
err
}
}
return
report
,
nil
}
// Group management implementations
func
(
s
*
adminServiceImpl
)
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
,
search
string
,
isExclusive
*
bool
,
sortBy
,
sortOrder
string
)
([]
Group
,
int64
,
error
)
{
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
,
SortBy
:
sortBy
,
SortOrder
:
sortOrder
}
...
...
backend/internal/service/admin_service_identity_migration_report_test.go
0 → 100644
View file @
3bd30272
package
service
import
(
"context"
"database/sql"
"testing"
"time"
dbent
"github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/enttest"
"github.com/stretchr/testify/require"
"entgo.io/ent/dialect"
entsql
"entgo.io/ent/dialect/sql"
_
"modernc.org/sqlite"
)
func
newAdminServiceMigrationReportTestClient
(
t
*
testing
.
T
)
*
dbent
.
Client
{
t
.
Helper
()
db
,
err
:=
sql
.
Open
(
"sqlite"
,
"file:admin_service_migration_reports?mode=memory&cache=shared&_fk=1"
)
require
.
NoError
(
t
,
err
)
t
.
Cleanup
(
func
()
{
_
=
db
.
Close
()
})
_
,
err
=
db
.
Exec
(
"PRAGMA foreign_keys = ON"
)
require
.
NoError
(
t
,
err
)
_
,
err
=
db
.
Exec
(
`CREATE TABLE auth_identity_migration_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_type TEXT NOT NULL,
report_key TEXT NOT NULL,
details TEXT NOT NULL DEFAULT '{}',
created_at DATETIME NOT NULL
)`
)
require
.
NoError
(
t
,
err
)
drv
:=
entsql
.
OpenDB
(
dialect
.
SQLite
,
db
)
client
:=
enttest
.
NewClient
(
t
,
enttest
.
WithOptions
(
dbent
.
Driver
(
drv
)))
t
.
Cleanup
(
func
()
{
_
=
client
.
Close
()
})
return
client
}
func
TestAdminServiceListAuthIdentityMigrationReports
(
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),
($5, $6, $7, $8)`
,
"oidc_synthetic_email_requires_manual_recovery"
,
"u-1"
,
`{"user_id":1}`
,
now
,
"wechat_provider_key_conflict"
,
"u-2"
,
`{"user_id":2}`
,
now
.
Add
(
-
time
.
Minute
),
)
require
.
NoError
(
t
,
err
)
svc
:=
&
adminServiceImpl
{
entClient
:
client
}
reports
,
total
,
err
:=
svc
.
ListAuthIdentityMigrationReports
(
context
.
Background
(),
"oidc_synthetic_email_requires_manual_recovery"
,
1
,
20
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
1
),
total
)
require
.
Len
(
t
,
reports
,
1
)
require
.
Equal
(
t
,
"oidc_synthetic_email_requires_manual_recovery"
,
reports
[
0
]
.
ReportType
)
require
.
Equal
(
t
,
float64
(
1
),
reports
[
0
]
.
Details
[
"user_id"
])
}
func
TestAdminServiceGetAuthIdentityMigrationReportSummary
(
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),
($5, $6, $7, $8),
($9, $10, $11, $12)`
,
"oidc_synthetic_email_requires_manual_recovery"
,
"u-1"
,
`{"user_id":1}`
,
now
,
"wechat_provider_key_conflict"
,
"u-2"
,
`{"user_id":2}`
,
now
.
Add
(
-
time
.
Minute
),
"wechat_provider_key_conflict"
,
"u-3"
,
`{"user_id":3}`
,
now
.
Add
(
-
2
*
time
.
Minute
),
)
require
.
NoError
(
t
,
err
)
svc
:=
&
adminServiceImpl
{
entClient
:
client
}
summary
,
err
:=
svc
.
GetAuthIdentityMigrationReportSummary
(
context
.
Background
())
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
3
),
summary
.
Total
)
require
.
Equal
(
t
,
int64
(
1
),
summary
.
ByType
[
"oidc_synthetic_email_requires_manual_recovery"
])
require
.
Equal
(
t
,
int64
(
2
),
summary
.
ByType
[
"wechat_provider_key_conflict"
])
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment