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
d08757ce
Commit
d08757ce
authored
Apr 21, 2026
by
IanShaw027
Browse files
refactor(admin): remove auth migration reports
parent
c624cce8
Changes
28
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/admin_basic_handlers_test.go
View file @
d08757ce
...
@@ -7,7 +7,6 @@ import (
...
@@ -7,7 +7,6 @@ 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"
)
)
...
@@ -23,12 +22,6 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
...
@@ -23,12 +22,6 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
redeemHandler
:=
NewRedeemHandler
(
adminSvc
,
nil
)
redeemHandler
:=
NewRedeemHandler
(
adminSvc
,
nil
)
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"
,
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
)
...
@@ -78,23 +71,6 @@ func TestUserHandlerEndpoints(t *testing.T) {
...
@@ -78,23 +71,6 @@ 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
)
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
)
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
)
...
@@ -111,7 +87,7 @@ func TestUserHandlerEndpoints(t *testing.T) {
...
@@ -111,7 +87,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"
)
...
...
backend/internal/handler/admin/admin_service_stub_test.go
View file @
d08757ce
...
@@ -17,7 +17,6 @@ type stubAdminService struct {
...
@@ -17,7 +17,6 @@ type stubAdminService struct {
proxies
[]
service
.
Proxy
proxies
[]
service
.
Proxy
proxyCounts
[]
service
.
ProxyWithAccountCount
proxyCounts
[]
service
.
ProxyWithAccountCount
redeems
[]
service
.
RedeemCode
redeems
[]
service
.
RedeemCode
migrationReports
[]
service
.
AuthIdentityMigrationReport
boundAuthIdentity
*
service
.
AdminBindAuthIdentityInput
boundAuthIdentity
*
service
.
AdminBindAuthIdentityInput
boundAuthIdentityFor
int64
boundAuthIdentityFor
int64
createdAccounts
[]
*
service
.
CreateAccountInput
createdAccounts
[]
*
service
.
CreateAccountInput
...
@@ -134,15 +133,6 @@ func newStubAdminService() *stubAdminService {
...
@@ -134,15 +133,6 @@ func newStubAdminService() *stubAdminService {
proxies
:
[]
service
.
Proxy
{
proxy
},
proxies
:
[]
service
.
Proxy
{
proxy
},
proxyCounts
:
[]
service
.
ProxyWithAccountCount
{{
Proxy
:
proxy
,
AccountCount
:
1
}},
proxyCounts
:
[]
service
.
ProxyWithAccountCount
{{
Proxy
:
proxy
,
AccountCount
:
1
}},
redeems
:
[]
service
.
RedeemCode
{
redeem
},
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
,
},
},
}
}
}
}
...
@@ -193,30 +183,6 @@ func (s *stubAdminService) GetUserUsageStats(ctx context.Context, userID int64,
...
@@ -193,30 +183,6 @@ func (s *stubAdminService) GetUserUsageStats(ctx context.Context, userID int64,
return
map
[
string
]
any
{
"user_id"
:
userID
},
nil
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
)
BindUserAuthIdentity
(
ctx
context
.
Context
,
userID
int64
,
input
service
.
AdminBindAuthIdentityInput
)
(
*
service
.
AdminBoundAuthIdentity
,
error
)
{
func
(
s
*
stubAdminService
)
BindUserAuthIdentity
(
ctx
context
.
Context
,
userID
int64
,
input
service
.
AdminBindAuthIdentityInput
)
(
*
service
.
AdminBoundAuthIdentity
,
error
)
{
s
.
boundAuthIdentityFor
=
userID
s
.
boundAuthIdentityFor
=
userID
copied
:=
input
copied
:=
input
...
@@ -263,20 +229,6 @@ func (s *stubAdminService) BindUserAuthIdentity(ctx context.Context, userID int6
...
@@ -263,20 +229,6 @@ 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
}
}
...
...
backend/internal/handler/admin/user_handler.go
View file @
d08757ce
...
@@ -7,7 +7,6 @@ import (
...
@@ -7,7 +7,6 @@ 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"
...
@@ -83,10 +82,6 @@ type BindUserAuthIdentityChannelRequest struct {
...
@@ -83,10 +82,6 @@ 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:
...
@@ -193,31 +188,6 @@ func (h *UserHandler) GetByID(c *gin.Context) {
...
@@ -193,31 +188,6 @@ func (h *UserHandler) GetByID(c *gin.Context) {
response
.
Success
(
c
,
dto
.
UserFromServiceAdmin
(
user
))
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
)
}
// BindAuthIdentity manually binds a canonical auth identity to a user.
// BindAuthIdentity manually binds a canonical auth identity to a user.
// POST /api/v1/admin/users/:id/auth-identities
// POST /api/v1/admin/users/:id/auth-identities
func
(
h
*
UserHandler
)
BindAuthIdentity
(
c
*
gin
.
Context
)
{
func
(
h
*
UserHandler
)
BindAuthIdentity
(
c
*
gin
.
Context
)
{
...
@@ -257,40 +227,6 @@ func (h *UserHandler) BindAuthIdentity(c *gin.Context) {
...
@@ -257,40 +227,6 @@ 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
)
{
...
...
backend/internal/handler/admin/user_handler_activity_test.go
View file @
d08757ce
...
@@ -29,7 +29,6 @@ func TestUserHandlerListIncludesActivityFieldsAndSortParams(t *testing.T) {
...
@@ -29,7 +29,6 @@ func TestUserHandlerListIncludesActivityFieldsAndSortParams(t *testing.T) {
Username
:
"activity-user"
,
Username
:
"activity-user"
,
Role
:
service
.
RoleUser
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Status
:
service
.
StatusActive
,
LastLoginAt
:
&
lastLoginAt
,
LastActiveAt
:
&
lastActiveAt
,
LastActiveAt
:
&
lastActiveAt
,
LastUsedAt
:
&
lastUsedAt
,
LastUsedAt
:
&
lastUsedAt
,
CreatedAt
:
lastLoginAt
.
Add
(
-
24
*
time
.
Hour
),
CreatedAt
:
lastLoginAt
.
Add
(
-
24
*
time
.
Hour
),
...
@@ -57,7 +56,6 @@ func TestUserHandlerListIncludesActivityFieldsAndSortParams(t *testing.T) {
...
@@ -57,7 +56,6 @@ func TestUserHandlerListIncludesActivityFieldsAndSortParams(t *testing.T) {
Code
int
`json:"code"`
Code
int
`json:"code"`
Data
struct
{
Data
struct
{
Items
[]
struct
{
Items
[]
struct
{
LastLoginAt
*
time
.
Time
`json:"last_login_at"`
LastActiveAt
*
time
.
Time
`json:"last_active_at"`
LastActiveAt
*
time
.
Time
`json:"last_active_at"`
LastUsedAt
*
time
.
Time
`json:"last_used_at"`
LastUsedAt
*
time
.
Time
`json:"last_used_at"`
}
`json:"items"`
}
`json:"items"`
...
@@ -66,7 +64,6 @@ func TestUserHandlerListIncludesActivityFieldsAndSortParams(t *testing.T) {
...
@@ -66,7 +64,6 @@ func TestUserHandlerListIncludesActivityFieldsAndSortParams(t *testing.T) {
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Len
(
t
,
resp
.
Data
.
Items
,
1
)
require
.
Len
(
t
,
resp
.
Data
.
Items
,
1
)
require
.
WithinDuration
(
t
,
lastLoginAt
,
*
resp
.
Data
.
Items
[
0
]
.
LastLoginAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastActiveAt
,
*
resp
.
Data
.
Items
[
0
]
.
LastActiveAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastActiveAt
,
*
resp
.
Data
.
Items
[
0
]
.
LastActiveAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastUsedAt
,
*
resp
.
Data
.
Items
[
0
]
.
LastUsedAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastUsedAt
,
*
resp
.
Data
.
Items
[
0
]
.
LastUsedAt
,
time
.
Second
)
}
}
...
@@ -86,7 +83,6 @@ func TestUserHandlerGetByIDIncludesActivityFields(t *testing.T) {
...
@@ -86,7 +83,6 @@ func TestUserHandlerGetByIDIncludesActivityFields(t *testing.T) {
Username
:
"detail-user"
,
Username
:
"detail-user"
,
Role
:
service
.
RoleUser
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Status
:
service
.
StatusActive
,
LastLoginAt
:
&
lastLoginAt
,
LastActiveAt
:
&
lastActiveAt
,
LastActiveAt
:
&
lastActiveAt
,
LastUsedAt
:
&
lastUsedAt
,
LastUsedAt
:
&
lastUsedAt
,
CreatedAt
:
lastLoginAt
.
Add
(
-
24
*
time
.
Hour
),
CreatedAt
:
lastLoginAt
.
Add
(
-
24
*
time
.
Hour
),
...
@@ -107,14 +103,12 @@ func TestUserHandlerGetByIDIncludesActivityFields(t *testing.T) {
...
@@ -107,14 +103,12 @@ func TestUserHandlerGetByIDIncludesActivityFields(t *testing.T) {
var
resp
struct
{
var
resp
struct
{
Code
int
`json:"code"`
Code
int
`json:"code"`
Data
struct
{
Data
struct
{
LastLoginAt
*
time
.
Time
`json:"last_login_at"`
LastActiveAt
*
time
.
Time
`json:"last_active_at"`
LastActiveAt
*
time
.
Time
`json:"last_active_at"`
LastUsedAt
*
time
.
Time
`json:"last_used_at"`
LastUsedAt
*
time
.
Time
`json:"last_used_at"`
}
`json:"data"`
}
`json:"data"`
}
}
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
WithinDuration
(
t
,
lastLoginAt
,
*
resp
.
Data
.
LastLoginAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastActiveAt
,
*
resp
.
Data
.
LastActiveAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastActiveAt
,
*
resp
.
Data
.
LastActiveAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastUsedAt
,
*
resp
.
Data
.
LastUsedAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastUsedAt
,
*
resp
.
Data
.
LastUsedAt
,
time
.
Second
)
}
}
backend/internal/handler/auth_oauth_pending_flow_test.go
View file @
d08757ce
...
@@ -2293,6 +2293,10 @@ func (r *oauthPendingFlowUserRepo) Update(ctx context.Context, user *service.Use
...
@@ -2293,6 +2293,10 @@ func (r *oauthPendingFlowUserRepo) Update(ctx context.Context, user *service.Use
return
nil
return
nil
}
}
func
(
r
*
oauthPendingFlowUserRepo
)
UpdateUserLastActiveAt
(
ctx
context
.
Context
,
userID
int64
,
activeAt
time
.
Time
)
error
{
return
r
.
client
.
User
.
UpdateOneID
(
userID
)
.
SetLastActiveAt
(
activeAt
)
.
Exec
(
ctx
)
}
func
(
r
*
oauthPendingFlowUserRepo
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
func
(
r
*
oauthPendingFlowUserRepo
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
if
r
.
options
.
rejectDeleteWhileAuthIdentityExists
{
if
r
.
options
.
rejectDeleteWhileAuthIdentityExists
{
count
,
err
:=
r
.
client
.
AuthIdentity
.
Query
()
.
Where
(
authidentity
.
UserIDEQ
(
id
))
.
Count
(
ctx
)
count
,
err
:=
r
.
client
.
AuthIdentity
.
Query
()
.
Where
(
authidentity
.
UserIDEQ
(
id
))
.
Count
(
ctx
)
...
...
backend/internal/handler/dto/mappers.go
View file @
d08757ce
...
@@ -21,7 +21,6 @@ func UserFromServiceShallow(u *service.User) *User {
...
@@ -21,7 +21,6 @@ func UserFromServiceShallow(u *service.User) *User {
Concurrency
:
u
.
Concurrency
,
Concurrency
:
u
.
Concurrency
,
Status
:
u
.
Status
,
Status
:
u
.
Status
,
AllowedGroups
:
u
.
AllowedGroups
,
AllowedGroups
:
u
.
AllowedGroups
,
LastLoginAt
:
u
.
LastLoginAt
,
LastActiveAt
:
u
.
LastActiveAt
,
LastActiveAt
:
u
.
LastActiveAt
,
CreatedAt
:
u
.
CreatedAt
,
CreatedAt
:
u
.
CreatedAt
,
UpdatedAt
:
u
.
UpdatedAt
,
UpdatedAt
:
u
.
UpdatedAt
,
...
...
backend/internal/handler/dto/types.go
View file @
d08757ce
...
@@ -15,7 +15,6 @@ type User struct {
...
@@ -15,7 +15,6 @@ type User struct {
Concurrency
int
`json:"concurrency"`
Concurrency
int
`json:"concurrency"`
Status
string
`json:"status"`
Status
string
`json:"status"`
AllowedGroups
[]
int64
`json:"allowed_groups"`
AllowedGroups
[]
int64
`json:"allowed_groups"`
LastLoginAt
*
time
.
Time
`json:"last_login_at,omitempty"`
LastActiveAt
*
time
.
Time
`json:"last_active_at,omitempty"`
LastActiveAt
*
time
.
Time
`json:"last_active_at,omitempty"`
CreatedAt
time
.
Time
`json:"created_at"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
...
...
backend/internal/handler/dto/user_mapper_activity_test.go
View file @
d08757ce
...
@@ -21,16 +21,13 @@ func TestUserFromServiceAdmin_MapsActivityTimestamps(t *testing.T) {
...
@@ -21,16 +21,13 @@ func TestUserFromServiceAdmin_MapsActivityTimestamps(t *testing.T) {
Username
:
"admin"
,
Username
:
"admin"
,
Role
:
service
.
RoleAdmin
,
Role
:
service
.
RoleAdmin
,
Status
:
service
.
StatusActive
,
Status
:
service
.
StatusActive
,
LastLoginAt
:
&
lastLoginAt
,
LastActiveAt
:
&
lastActiveAt
,
LastActiveAt
:
&
lastActiveAt
,
LastUsedAt
:
&
lastUsedAt
,
LastUsedAt
:
&
lastUsedAt
,
})
})
require
.
NotNil
(
t
,
out
)
require
.
NotNil
(
t
,
out
)
require
.
NotNil
(
t
,
out
.
LastLoginAt
)
require
.
NotNil
(
t
,
out
.
LastActiveAt
)
require
.
NotNil
(
t
,
out
.
LastActiveAt
)
require
.
NotNil
(
t
,
out
.
LastUsedAt
)
require
.
NotNil
(
t
,
out
.
LastUsedAt
)
require
.
WithinDuration
(
t
,
lastLoginAt
,
*
out
.
LastLoginAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastActiveAt
,
*
out
.
LastActiveAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastActiveAt
,
*
out
.
LastActiveAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastUsedAt
,
*
out
.
LastUsedAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastUsedAt
,
*
out
.
LastUsedAt
,
time
.
Second
)
}
}
backend/internal/handler/user_handler_test.go
View file @
d08757ce
...
@@ -99,6 +99,12 @@ func (s *userHandlerRepoStub) GetLatestUsedAtByUserIDs(context.Context, []int64)
...
@@ -99,6 +99,12 @@ func (s *userHandlerRepoStub) GetLatestUsedAtByUserIDs(context.Context, []int64)
func
(
s
*
userHandlerRepoStub
)
GetLatestUsedAtByUserID
(
context
.
Context
,
int64
)
(
*
time
.
Time
,
error
)
{
func
(
s
*
userHandlerRepoStub
)
GetLatestUsedAtByUserID
(
context
.
Context
,
int64
)
(
*
time
.
Time
,
error
)
{
return
nil
,
nil
return
nil
,
nil
}
}
func
(
s
*
userHandlerRepoStub
)
UpdateUserLastActiveAt
(
_
context
.
Context
,
_
int64
,
activeAt
time
.
Time
)
error
{
if
s
.
user
!=
nil
{
s
.
user
.
LastActiveAt
=
&
activeAt
}
return
nil
}
func
(
s
*
userHandlerRepoStub
)
RemoveGroupFromUserAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
func
(
s
*
userHandlerRepoStub
)
RemoveGroupFromUserAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
return
nil
return
nil
}
}
...
...
backend/internal/repository/auth_identity_migration_report.go
deleted
100644 → 0
View file @
c624cce8
package
repository
import
(
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
)
type
AuthIdentityMigrationReport
struct
{
ID
int64
ReportType
string
ReportKey
string
Details
map
[
string
]
any
CreatedAt
time
.
Time
}
type
AuthIdentityMigrationReportQuery
struct
{
ReportType
string
Limit
int
Offset
int
}
type
AuthIdentityMigrationReportSummary
struct
{
Total
int64
ByType
map
[
string
]
int64
}
func
(
r
*
userRepository
)
ListAuthIdentityMigrationReports
(
ctx
context
.
Context
,
query
AuthIdentityMigrationReportQuery
)
([]
AuthIdentityMigrationReport
,
error
)
{
exec
:=
txAwareSQLExecutor
(
ctx
,
r
.
sql
,
r
.
client
)
if
exec
==
nil
{
return
nil
,
fmt
.
Errorf
(
"sql executor is not configured"
)
}
limit
:=
query
.
Limit
if
limit
<=
0
{
limit
=
100
}
rows
,
err
:=
exec
.
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`
,
strings
.
TrimSpace
(
query
.
ReportType
),
limit
,
query
.
Offset
,
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
reports
:=
make
([]
AuthIdentityMigrationReport
,
0
)
for
rows
.
Next
()
{
report
,
scanErr
:=
scanAuthIdentityMigrationReport
(
rows
)
if
scanErr
!=
nil
{
return
nil
,
scanErr
}
reports
=
append
(
reports
,
report
)
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
return
reports
,
nil
}
func
(
r
*
userRepository
)
GetAuthIdentityMigrationReport
(
ctx
context
.
Context
,
reportType
,
reportKey
string
)
(
*
AuthIdentityMigrationReport
,
error
)
{
exec
:=
txAwareSQLExecutor
(
ctx
,
r
.
sql
,
r
.
client
)
if
exec
==
nil
{
return
nil
,
fmt
.
Errorf
(
"sql executor is not configured"
)
}
rows
,
err
:=
exec
.
QueryContext
(
ctx
,
`
SELECT id, report_type, report_key, details, created_at
FROM auth_identity_migration_reports
WHERE report_type = $1 AND report_key = $2
LIMIT 1`
,
strings
.
TrimSpace
(
reportType
),
strings
.
TrimSpace
(
reportKey
),
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
_
=
rows
.
Close
()
}()
if
!
rows
.
Next
()
{
return
nil
,
sql
.
ErrNoRows
}
report
,
err
:=
scanAuthIdentityMigrationReport
(
rows
)
if
err
!=
nil
{
return
nil
,
err
}
return
&
report
,
rows
.
Err
()
}
func
(
r
*
userRepository
)
SummarizeAuthIdentityMigrationReports
(
ctx
context
.
Context
)
(
*
AuthIdentityMigrationReportSummary
,
error
)
{
exec
:=
txAwareSQLExecutor
(
ctx
,
r
.
sql
,
r
.
client
)
if
exec
==
nil
{
return
nil
,
fmt
.
Errorf
(
"sql executor is not configured"
)
}
rows
,
err
:=
exec
.
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
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
}
backend/internal/repository/user_profile_identity_repo_contract_test.go
View file @
d08757ce
...
@@ -36,7 +36,6 @@ TRUNCATE TABLE
...
@@ -36,7 +36,6 @@ TRUNCATE TABLE
auth_identity_channels,
auth_identity_channels,
auth_identities,
auth_identities,
pending_auth_sessions,
pending_auth_sessions,
auth_identity_migration_reports,
user_provider_default_grants,
user_provider_default_grants,
user_avatars
user_avatars
RESTART IDENTITY`
)
RESTART IDENTITY`
)
...
@@ -393,36 +392,6 @@ func (s *UserProfileIdentityRepoSuite) TestUserAvatarCRUDAndUserLookup() {
...
@@ -393,36 +392,6 @@ func (s *UserProfileIdentityRepoSuite) TestUserAvatarCRUDAndUserLookup() {
s
.
Require
()
.
Nil
(
loadedAvatar
)
s
.
Require
()
.
Nil
(
loadedAvatar
)
}
}
func
(
s
*
UserProfileIdentityRepoSuite
)
TestAuthIdentityMigrationReportHelpers_ListAndSummarize
()
{
_
,
err
:=
integrationDB
.
ExecContext
(
s
.
ctx
,
`
INSERT INTO auth_identity_migration_reports (report_type, report_key, details, created_at)
VALUES
('wechat_openid_only_requires_remediation', 'u-1', '{"user_id":1}'::jsonb, '2026-04-20T10:00:00Z'),
('wechat_openid_only_requires_remediation', 'u-2', '{"user_id":2}'::jsonb, '2026-04-20T11:00:00Z'),
('oidc_synthetic_email_requires_manual_recovery', 'u-3', '{"user_id":3}'::jsonb, '2026-04-20T12:00:00Z')`
)
s
.
Require
()
.
NoError
(
err
)
summary
,
err
:=
s
.
repo
.
SummarizeAuthIdentityMigrationReports
(
s
.
ctx
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Equal
(
int64
(
3
),
summary
.
Total
)
s
.
Require
()
.
Equal
(
int64
(
2
),
summary
.
ByType
[
"wechat_openid_only_requires_remediation"
])
s
.
Require
()
.
Equal
(
int64
(
1
),
summary
.
ByType
[
"oidc_synthetic_email_requires_manual_recovery"
])
reports
,
err
:=
s
.
repo
.
ListAuthIdentityMigrationReports
(
s
.
ctx
,
AuthIdentityMigrationReportQuery
{
ReportType
:
"wechat_openid_only_requires_remediation"
,
Limit
:
10
,
})
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
reports
,
2
)
s
.
Require
()
.
Equal
(
"u-2"
,
reports
[
0
]
.
ReportKey
)
s
.
Require
()
.
Equal
(
float64
(
2
),
reports
[
0
]
.
Details
[
"user_id"
])
report
,
err
:=
s
.
repo
.
GetAuthIdentityMigrationReport
(
s
.
ctx
,
"oidc_synthetic_email_requires_manual_recovery"
,
"u-3"
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Equal
(
"u-3"
,
report
.
ReportKey
)
s
.
Require
()
.
Equal
(
float64
(
3
),
report
.
Details
[
"user_id"
])
}
func
(
s
*
UserProfileIdentityRepoSuite
)
TestUpdateUserLastLoginAndActiveAt_UsesDedicatedColumns
()
{
func
(
s
*
UserProfileIdentityRepoSuite
)
TestUpdateUserLastLoginAndActiveAt_UsesDedicatedColumns
()
{
user
:=
s
.
mustCreateUser
(
"activity"
)
user
:=
s
.
mustCreateUser
(
"activity"
)
loginAt
:=
time
.
Date
(
2026
,
4
,
20
,
8
,
0
,
0
,
0
,
time
.
UTC
)
loginAt
:=
time
.
Date
(
2026
,
4
,
20
,
8
,
0
,
0
,
0
,
time
.
UTC
)
...
...
backend/internal/repository/user_repo.go
View file @
d08757ce
...
@@ -445,10 +445,6 @@ func userListOrder(params pagination.PaginationParams) []func(*entsql.Selector)
...
@@ -445,10 +445,6 @@ func userListOrder(params pagination.PaginationParams) []func(*entsql.Selector)
case
"created_at"
:
case
"created_at"
:
field
=
dbuser
.
FieldCreatedAt
field
=
dbuser
.
FieldCreatedAt
defaultField
=
false
defaultField
=
false
case
"last_login_at"
:
field
=
dbuser
.
FieldLastLoginAt
defaultField
=
false
nullsLastField
=
true
case
"last_active_at"
:
case
"last_active_at"
:
field
=
dbuser
.
FieldLastActiveAt
field
=
dbuser
.
FieldLastActiveAt
defaultField
=
false
defaultField
=
false
...
...
backend/internal/repository/user_repo_sort_integration_test.go
View file @
d08757ce
...
@@ -95,27 +95,6 @@ func (s *UserRepoSuite) TestUpdate_PersistsSignupSourceAndActivityTimestamps() {
...
@@ -95,27 +95,6 @@ func (s *UserRepoSuite) TestUpdate_PersistsSignupSourceAndActivityTimestamps() {
s
.
Require
()
.
True
(
got
.
LastActiveAt
.
Equal
(
lastActiveAt
))
s
.
Require
()
.
True
(
got
.
LastActiveAt
.
Equal
(
lastActiveAt
))
}
}
func
(
s
*
UserRepoSuite
)
TestListWithFilters_SortByLastLoginAtDesc
()
{
older
:=
time
.
Now
()
.
Add
(
-
4
*
time
.
Hour
)
.
UTC
()
.
Truncate
(
time
.
Microsecond
)
newer
:=
time
.
Now
()
.
Add
(
-
1
*
time
.
Hour
)
.
UTC
()
.
Truncate
(
time
.
Microsecond
)
s
.
mustCreateUser
(
&
service
.
User
{
Email
:
"nil-login@example.com"
})
s
.
mustCreateUser
(
&
service
.
User
{
Email
:
"older-login@example.com"
,
LastLoginAt
:
&
older
})
s
.
mustCreateUser
(
&
service
.
User
{
Email
:
"newer-login@example.com"
,
LastLoginAt
:
&
newer
})
users
,
_
,
err
:=
s
.
repo
.
ListWithFilters
(
s
.
ctx
,
pagination
.
PaginationParams
{
Page
:
1
,
PageSize
:
10
,
SortBy
:
"last_login_at"
,
SortOrder
:
"desc"
,
},
service
.
UserListFilters
{})
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
users
,
3
)
s
.
Require
()
.
Equal
(
"newer-login@example.com"
,
users
[
0
]
.
Email
)
s
.
Require
()
.
Equal
(
"older-login@example.com"
,
users
[
1
]
.
Email
)
s
.
Require
()
.
Equal
(
"nil-login@example.com"
,
users
[
2
]
.
Email
)
}
func
(
s
*
UserRepoSuite
)
TestListWithFilters_SortByLastActiveAtAsc
()
{
func
(
s
*
UserRepoSuite
)
TestListWithFilters_SortByLastActiveAtAsc
()
{
earlier
:=
time
.
Now
()
.
Add
(
-
3
*
time
.
Hour
)
.
UTC
()
.
Truncate
(
time
.
Microsecond
)
earlier
:=
time
.
Now
()
.
Add
(
-
3
*
time
.
Hour
)
.
UTC
()
.
Truncate
(
time
.
Microsecond
)
later
:=
time
.
Now
()
.
Add
(
-
45
*
time
.
Minute
)
.
UTC
()
.
Truncate
(
time
.
Microsecond
)
later
:=
time
.
Now
()
.
Add
(
-
45
*
time
.
Minute
)
.
UTC
()
.
Truncate
(
time
.
Microsecond
)
...
...
backend/internal/server/routes/admin.go
View file @
d08757ce
...
@@ -210,9 +210,6 @@ func registerDashboardRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
...
@@ -210,9 +210,6 @@ func registerDashboardRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
func
registerUserManagementRoutes
(
admin
*
gin
.
RouterGroup
,
h
*
handler
.
Handlers
)
{
func
registerUserManagementRoutes
(
admin
*
gin
.
RouterGroup
,
h
*
handler
.
Handlers
)
{
users
:=
admin
.
Group
(
"/users"
)
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
.
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
)
...
...
backend/internal/service/admin_service.go
View file @
d08757ce
...
@@ -39,10 +39,7 @@ type AdminService interface {
...
@@ -39,10 +39,7 @@ type AdminService interface {
// codeType is optional - pass empty string to return all types.
// codeType is optional - pass empty string to return all types.
// Also returns totalRecharged (sum of all positive balance top-ups).
// 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
)
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
)
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,24 +134,6 @@ type UpdateUserInput struct {
...
@@ -137,24 +134,6 @@ type UpdateUserInput struct {
GroupRates
map
[
int64
]
*
float64
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"`
ResolvedAt
*
time
.
Time
`json:"resolved_at,omitempty"`
ResolvedByUserID
*
int64
`json:"resolved_by_user_id,omitempty"`
ResolutionNote
string
`json:"resolution_note,omitempty"`
}
type
AuthIdentityMigrationReportSummary
struct
{
Total
int64
`json:"total"`
OpenTotal
int64
`json:"open_total"`
ResolvedTotal
int64
`json:"resolved_total"`
ByType
map
[
string
]
int64
`json:"by_type"`
}
type
AdminBindAuthIdentityInput
struct
{
type
AdminBindAuthIdentityInput
struct
{
ProviderType
string
ProviderType
string
ProviderKey
string
ProviderKey
string
...
@@ -874,152 +853,6 @@ func (s *adminServiceImpl) GetUserBalanceHistory(ctx context.Context, userID int
...
@@ -874,152 +853,6 @@ func (s *adminServiceImpl) GetUserBalanceHistory(ctx context.Context, userID int
return
codes
,
result
.
Total
,
totalRecharged
,
nil
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, resolved_at, resolved_by_user_id, resolution_note
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(*),
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
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
var
openCount
int64
var
resolvedCount
int64
if
err
:=
rows
.
Scan
(
&
reportType
,
&
count
,
&
openCount
,
&
resolvedCount
);
err
!=
nil
{
return
nil
,
err
}
summary
.
ByType
[
reportType
]
=
count
summary
.
Total
+=
count
summary
.
OpenTotal
+=
openCount
summary
.
ResolvedTotal
+=
resolvedCount
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
return
nil
,
err
}
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"
)
...
@@ -1252,44 +1085,6 @@ func cloneAdminAuthIdentityMetadata(input map[string]any) map[string]any {
...
@@ -1252,44 +1085,6 @@ func cloneAdminAuthIdentityMetadata(input map[string]any) map[string]any {
return
out
return
out
}
}
func
scanAuthIdentityMigrationReport
(
scanner
interface
{
Scan
(
dest
...
any
)
error
})
(
AuthIdentityMigrationReport
,
error
)
{
var
(
report
AuthIdentityMigrationReport
details
[]
byte
resolvedAt
sql
.
NullTime
resolvedByUserID
sql
.
NullInt64
resolutionNote
sql
.
NullString
)
if
err
:=
scanner
.
Scan
(
&
report
.
ID
,
&
report
.
ReportType
,
&
report
.
ReportKey
,
&
details
,
&
report
.
CreatedAt
,
&
resolvedAt
,
&
resolvedByUserID
,
&
resolutionNote
,
);
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
}
}
if
resolvedAt
.
Valid
{
report
.
ResolvedAt
=
&
resolvedAt
.
Time
}
if
resolvedByUserID
.
Valid
{
report
.
ResolvedByUserID
=
&
resolvedByUserID
.
Int64
}
if
resolutionNote
.
Valid
{
report
.
ResolutionNote
=
resolutionNote
.
String
}
return
report
,
nil
}
// Group management implementations
// 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
)
{
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
}
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
,
SortBy
:
sortBy
,
SortOrder
:
sortOrder
}
...
...
backend/internal/service/admin_service_identity_migration_report_test.go
deleted
100644 → 0
View file @
c624cce8
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,
resolved_at DATETIME NULL,
resolved_by_user_id INTEGER NULL,
resolution_note TEXT NOT NULL DEFAULT ''
)`
)
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
(
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
(
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
)
}
frontend/src/api/__tests__/users.migrationReports.spec.ts
deleted
100644 → 0
View file @
c624cce8
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
const
{
get
,
post
}
=
vi
.
hoisted
(()
=>
({
get
:
vi
.
fn
(),
post
:
vi
.
fn
(),
}))
vi
.
mock
(
'
@/api/client
'
,
()
=>
({
apiClient
:
{
get
,
post
,
},
}))
import
{
bindUserAuthIdentity
,
getAuthIdentityMigrationReportSummary
,
listAuthIdentityMigrationReports
,
resolveAuthIdentityMigrationReport
,
}
from
'
@/api/admin/users
'
describe
(
'
admin users auth identity migration reports API
'
,
()
=>
{
beforeEach
(()
=>
{
get
.
mockReset
()
post
.
mockReset
()
})
it
(
'
lists migration reports with pagination and report type filter
'
,
async
()
=>
{
const
response
=
{
items
:
[],
total
:
0
,
page
:
2
,
page_size
:
10
,
pages
:
0
,
}
get
.
mockResolvedValue
({
data
:
response
})
const
result
=
await
listAuthIdentityMigrationReports
({
page
:
2
,
pageSize
:
10
,
reportType
:
'
oidc_synthetic_email_requires_manual_recovery
'
,
})
expect
(
get
).
toHaveBeenCalledWith
(
'
/admin/users/auth-identity-migration-reports
'
,
{
params
:
{
page
:
2
,
page_size
:
10
,
report_type
:
'
oidc_synthetic_email_requires_manual_recovery
'
,
},
})
expect
(
result
).
toBe
(
response
)
})
it
(
'
loads migration report summary
'
,
async
()
=>
{
const
response
=
{
total
:
2
,
open_total
:
1
,
resolved_total
:
1
,
by_type
:
{
oidc_synthetic_email_requires_manual_recovery
:
2
,
},
}
get
.
mockResolvedValue
({
data
:
response
})
const
result
=
await
getAuthIdentityMigrationReportSummary
()
expect
(
get
).
toHaveBeenCalledWith
(
'
/admin/users/auth-identity-migration-reports/summary
'
)
expect
(
result
).
toBe
(
response
)
})
it
(
'
submits report resolution note
'
,
async
()
=>
{
const
response
=
{
id
:
7
,
resolution_note
:
'
resolved by admin
'
,
}
post
.
mockResolvedValue
({
data
:
response
})
const
result
=
await
resolveAuthIdentityMigrationReport
(
7
,
'
resolved by admin
'
)
expect
(
post
).
toHaveBeenCalledWith
(
'
/admin/users/auth-identity-migration-reports/7/resolve
'
,
{
resolution_note
:
'
resolved by admin
'
,
})
expect
(
result
).
toBe
(
response
)
})
it
(
'
binds a canonical auth identity to a user for remediation
'
,
async
()
=>
{
const
response
=
{
identity_id
:
11
,
provider_type
:
'
oidc
'
,
provider_key
:
'
https://issuer.example
'
,
provider_subject
:
'
subject-123
'
,
}
post
.
mockResolvedValue
({
data
:
response
})
const
result
=
await
bindUserAuthIdentity
(
42
,
{
provider_type
:
'
oidc
'
,
provider_key
:
'
https://issuer.example
'
,
provider_subject
:
'
subject-123
'
,
issuer
:
'
https://issuer.example
'
,
metadata
:
{
source
:
'
migration-report
'
},
})
expect
(
post
).
toHaveBeenCalledWith
(
'
/admin/users/42/auth-identities
'
,
{
provider_type
:
'
oidc
'
,
provider_key
:
'
https://issuer.example
'
,
provider_subject
:
'
subject-123
'
,
issuer
:
'
https://issuer.example
'
,
metadata
:
{
source
:
'
migration-report
'
},
})
expect
(
result
).
toBe
(
response
)
})
})
frontend/src/api/admin/users.ts
View file @
d08757ce
...
@@ -6,24 +6,6 @@
...
@@ -6,24 +6,6 @@
import
{
apiClient
}
from
'
../client
'
import
{
apiClient
}
from
'
../client
'
import
type
{
AdminUser
,
UpdateUserRequest
,
PaginatedResponse
,
ApiKey
}
from
'
@/types
'
import
type
{
AdminUser
,
UpdateUserRequest
,
PaginatedResponse
,
ApiKey
}
from
'
@/types
'
export
interface
AuthIdentityMigrationReport
{
id
:
number
report_type
:
string
report_key
:
string
details
:
Record
<
string
,
unknown
>
created_at
:
string
resolved_at
?:
string
|
null
resolved_by_user_id
?:
number
|
null
resolution_note
?:
string
}
export
interface
AuthIdentityMigrationReportSummary
{
total
:
number
open_total
:
number
resolved_total
:
number
by_type
:
Record
<
string
,
number
>
}
export
interface
AdminBindAuthIdentityChannelRequest
{
export
interface
AdminBindAuthIdentityChannelRequest
{
channel
:
string
channel
:
string
channel_app_id
?:
string
channel_app_id
?:
string
...
@@ -48,12 +30,6 @@ export interface AdminBoundAuthIdentity {
...
@@ -48,12 +30,6 @@ export interface AdminBoundAuthIdentity {
channel_id
?:
number
|
null
channel_id
?:
number
|
null
}
}
export
interface
ListAuthIdentityMigrationReportsParams
{
page
?:
number
pageSize
?:
number
reportType
?:
string
}
/**
/**
* List all users with pagination
* List all users with pagination
* @param page - Page number (default: 1)
* @param page - Page number (default: 1)
...
@@ -296,42 +272,6 @@ export async function replaceGroup(
...
@@ -296,42 +272,6 @@ export async function replaceGroup(
return
data
return
data
}
}
export
async
function
getAuthIdentityMigrationReportSummary
():
Promise
<
AuthIdentityMigrationReportSummary
>
{
const
{
data
}
=
await
apiClient
.
get
<
AuthIdentityMigrationReportSummary
>
(
'
/admin/users/auth-identity-migration-reports/summary
'
)
return
data
}
export
async
function
listAuthIdentityMigrationReports
(
params
:
ListAuthIdentityMigrationReportsParams
=
{}
):
Promise
<
PaginatedResponse
<
AuthIdentityMigrationReport
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
AuthIdentityMigrationReport
>>
(
'
/admin/users/auth-identity-migration-reports
'
,
{
params
:
{
page
:
params
.
page
??
1
,
page_size
:
params
.
pageSize
??
20
,
report_type
:
params
.
reportType
??
''
}
}
)
return
data
}
export
async
function
resolveAuthIdentityMigrationReport
(
id
:
number
,
resolutionNote
:
string
):
Promise
<
AuthIdentityMigrationReport
>
{
const
{
data
}
=
await
apiClient
.
post
<
AuthIdentityMigrationReport
>
(
`/admin/users/auth-identity-migration-reports/
${
id
}
/resolve`
,
{
resolution_note
:
resolutionNote
}
)
return
data
}
export
async
function
bindUserAuthIdentity
(
export
async
function
bindUserAuthIdentity
(
userId
:
number
,
userId
:
number
,
input
:
AdminBindAuthIdentityRequest
input
:
AdminBindAuthIdentityRequest
...
@@ -356,10 +296,7 @@ export const usersAPI = {
...
@@ -356,10 +296,7 @@ export const usersAPI = {
getUserUsageStats
,
getUserUsageStats
,
getUserBalanceHistory
,
getUserBalanceHistory
,
replaceGroup
,
replaceGroup
,
bindUserAuthIdentity
,
bindUserAuthIdentity
getAuthIdentityMigrationReportSummary
,
listAuthIdentityMigrationReports
,
resolveAuthIdentityMigrationReport
}
}
export
default
usersAPI
export
default
usersAPI
frontend/src/components/layout/AppSidebar.vue
View file @
d08757ce
...
@@ -663,12 +663,6 @@ const adminNavItems = computed((): NavItem[] => {
...
@@ -663,12 +663,6 @@ const adminNavItems = computed((): NavItem[] => {
?
[{
path
:
'
/admin/ops
'
,
label
:
t
(
'
nav.ops
'
),
icon
:
ChartIcon
}]
?
[{
path
:
'
/admin/ops
'
,
label
:
t
(
'
nav.ops
'
),
icon
:
ChartIcon
}]
:
[]),
:
[]),
{
path
:
'
/admin/users
'
,
label
:
t
(
'
nav.users
'
),
icon
:
UsersIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/users
'
,
label
:
t
(
'
nav.users
'
),
icon
:
UsersIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/users/auth-identity-migration-reports
'
,
label
:
'
Migration Reports
'
,
icon
:
UsersIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/groups
'
,
label
:
t
(
'
nav.groups
'
),
icon
:
FolderIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/groups
'
,
label
:
t
(
'
nav.groups
'
),
icon
:
FolderIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/channels
'
,
label
:
t
(
'
nav.channels
'
,
'
渠道管理
'
),
icon
:
ChannelIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/channels
'
,
label
:
t
(
'
nav.channels
'
,
'
渠道管理
'
),
icon
:
ChannelIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/subscriptions
'
,
label
:
t
(
'
nav.subscriptions
'
),
icon
:
CreditCardIcon
,
hideInSimpleMode
:
true
},
{
path
:
'
/admin/subscriptions
'
,
label
:
t
(
'
nav.subscriptions
'
),
icon
:
CreditCardIcon
,
hideInSimpleMode
:
true
},
...
...
frontend/src/components/layout/__tests__/AppSidebar.spec.ts
View file @
d08757ce
...
@@ -30,9 +30,3 @@ describe('AppSidebar header styles', () => {
...
@@ -30,9 +30,3 @@ describe('AppSidebar header styles', () => {
expect
(
sidebarBrandBlockMatch
?.[
0
]).
not
.
toContain
(
'
overflow: hidden;
'
)
expect
(
sidebarBrandBlockMatch
?.[
0
]).
not
.
toContain
(
'
overflow: hidden;
'
)
})
})
})
})
describe
(
'
AppSidebar admin navigation
'
,
()
=>
{
it
(
'
includes a visible entry for auth identity migration reports
'
,
()
=>
{
expect
(
componentSource
).
toContain
(
"
'/admin/users/auth-identity-migration-reports'
"
)
})
})
Prev
1
2
Next
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