fix: update admin flows and review report
This commit is contained in:
310
docs/code-review/PROJECT_REAL_COMPLETION_REVIEW_2026-04-10.md
Normal file
310
docs/code-review/PROJECT_REAL_COMPLETION_REVIEW_2026-04-10.md
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
# Project Real Completion Review 2026-04-10
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Review date: 2026-04-10
|
||||||
|
- Workspace: `D:\usersystem`
|
||||||
|
- Branch context: `fix/status-review-sync-20260409`
|
||||||
|
- Review method: command execution plus targeted code inspection
|
||||||
|
- Review scope:
|
||||||
|
- the branch delta above `origin/main`
|
||||||
|
- current uncommitted workspace changes
|
||||||
|
- current status of previously identified project-level blockers
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The project is materially healthier than the 2026-04-09 snapshot:
|
||||||
|
|
||||||
|
- `go vet ./...` is green
|
||||||
|
- `go build ./cmd/server` is green
|
||||||
|
- `go test ./... -short -count=1` is green
|
||||||
|
- frontend `lint`, `build`, `test:run`, and `test:coverage` are green
|
||||||
|
- `govulncheck` and production `npm audit` are green
|
||||||
|
|
||||||
|
However, this branch still cannot be honestly declared release-closed.
|
||||||
|
|
||||||
|
Current hard blockers or material risks:
|
||||||
|
|
||||||
|
- full `go test ./... -count=1` is still red because of the `LL_001` login-log pagination SLA gate
|
||||||
|
- the documented browser E2E entrypoint is still not green in this review environment
|
||||||
|
- the newly implemented role/admin-management path introduces real authorization and consistency risks
|
||||||
|
- avatar upload is still a visible stub
|
||||||
|
- frontend tests still emit jsdom native-dialog noise after a green run
|
||||||
|
|
||||||
|
## Commands Executed
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:GOROOT='D:\Program Files\Go'
|
||||||
|
$env:GOCACHE='D:\usersystem\.gocache'
|
||||||
|
$env:GOMODCACHE='D:\usersystem\.gomodcache'
|
||||||
|
|
||||||
|
go test ./... -short -count=1
|
||||||
|
go vet ./...
|
||||||
|
go build ./cmd/server
|
||||||
|
go test ./... -count=1
|
||||||
|
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd frontend/admin
|
||||||
|
npm.cmd run lint
|
||||||
|
npm.cmd run build
|
||||||
|
npm.cmd run test:run
|
||||||
|
npm.cmd run test:coverage
|
||||||
|
npm.cmd run e2e:full:win
|
||||||
|
npm.cmd audit --omit=dev --json --registry=https://registry.npmjs.org/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
### Passed
|
||||||
|
|
||||||
|
- `go test ./... -short -count=1`
|
||||||
|
- `go vet ./...`
|
||||||
|
- `go build ./cmd/server`
|
||||||
|
- `npm.cmd run lint`
|
||||||
|
- `npm.cmd run build`
|
||||||
|
- `npm.cmd run test:run`
|
||||||
|
- `59` files
|
||||||
|
- `325` tests
|
||||||
|
- `npm.cmd run test:coverage`
|
||||||
|
- `59` files
|
||||||
|
- `325` tests
|
||||||
|
- overall coverage:
|
||||||
|
- statements `88.96%`
|
||||||
|
- branches `78.35%`
|
||||||
|
- functions `86.01%`
|
||||||
|
- lines `89.55%`
|
||||||
|
- `go run golang.org/x/vuln/cmd/govulncheck@latest ./...`
|
||||||
|
- output: `No vulnerabilities found.`
|
||||||
|
- `npm.cmd audit --omit=dev --json --registry=https://registry.npmjs.org/`
|
||||||
|
- production vulnerability counts: `0 / 0 / 0 / 0 / 0`
|
||||||
|
|
||||||
|
### Failed
|
||||||
|
|
||||||
|
- `go test ./... -count=1`
|
||||||
|
- failed in `internal/service.TestScale_LL_001_180DayLoginLogRetention`
|
||||||
|
- observed `P99=2.2259254s`
|
||||||
|
- threshold `2s`
|
||||||
|
- `npm.cmd run e2e:full:win`
|
||||||
|
- failed during the backend build/bootstrap step in `frontend/admin/scripts/run-playwright-auth-e2e.ps1`
|
||||||
|
- current observed build output still reports unresolved module packages from the wrapper's temp-cache build path
|
||||||
|
|
||||||
|
### Passed but still noisy
|
||||||
|
|
||||||
|
- `npm.cmd run test:run`
|
||||||
|
- green exit code
|
||||||
|
- still emits jsdom `Not implemented: window.alert` traces after the success summary
|
||||||
|
- `npm.cmd run test:coverage`
|
||||||
|
- green exit code
|
||||||
|
- still emits the same jsdom native-dialog traces after the coverage summary
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### High
|
||||||
|
|
||||||
|
#### 1. Any authenticated user can now enumerate arbitrary users' role assignments
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `internal/api/router/router.go:212`
|
||||||
|
- `internal/api/handler/user_handler.go:245`
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- `GET /api/v1/users/:id/roles` is registered with no permission middleware.
|
||||||
|
- `GetUserRoles` now returns real role data for the requested `:id`.
|
||||||
|
- This route used to be inert because the handler always returned an empty array; after the current implementation, it becomes a real authorization gap.
|
||||||
|
|
||||||
|
Impact:
|
||||||
|
|
||||||
|
- any logged-in user can query the effective role set of any user ID
|
||||||
|
- this leaks privilege information and enables role reconnaissance against admin or privileged accounts
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
|
||||||
|
- restrict the route to self-access or explicit admin/`user:manage` permission
|
||||||
|
- add negative tests proving one user cannot read another user's roles
|
||||||
|
|
||||||
|
#### 2. `DeleteAdmin` can remove the caller's own admin role and can also remove the last remaining admin
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `internal/service/user_service.go:353`
|
||||||
|
- `internal/api/router/router.go:321`
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- the implementation contains a comment noting that self-removal must be checked, but no such check exists.
|
||||||
|
- there is also no guard against removing the final admin role assignment from the system.
|
||||||
|
- the route is exposed on the admin-management API and returns success after deleting the role link.
|
||||||
|
|
||||||
|
Impact:
|
||||||
|
|
||||||
|
- an admin can accidentally or maliciously demote themselves mid-session
|
||||||
|
- the system can be left without any admin users, blocking governance and operational recovery paths
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
|
||||||
|
- pass current operator ID into the service and block self-demotion
|
||||||
|
- block deletion when the target is the last remaining enabled admin
|
||||||
|
- add regression tests for both cases
|
||||||
|
|
||||||
|
### Medium
|
||||||
|
|
||||||
|
#### 3. `AssignRoles` and `CreateAdmin` are not transactional and can leave RBAC state partially applied
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `internal/service/user_service.go:252`
|
||||||
|
- `internal/service/user_service.go:311`
|
||||||
|
- comparison baseline: `internal/service/auth_admin_bootstrap.go:92`
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- `AssignRoles` deletes all existing role links before recreating them, but the operation is not wrapped in a transaction.
|
||||||
|
- `CreateAdmin` creates the user first and then creates the admin role link, also without transactional protection or rollback.
|
||||||
|
- the existing bootstrap flow already shows the correct failure-closed pattern by deleting the user if role assignment fails.
|
||||||
|
|
||||||
|
Impact:
|
||||||
|
|
||||||
|
- a failed role write can strip a user of all roles
|
||||||
|
- a failed admin-role write can leave an active non-admin account behind while the API reports failure
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
|
||||||
|
- execute both flows inside a single database transaction
|
||||||
|
- or at minimum add compensating rollback for every post-create failure path
|
||||||
|
|
||||||
|
#### 4. `CreateAdmin` regresses existing validation and role resolution patterns
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `internal/service/user_service.go:283`
|
||||||
|
- `internal/service/user_service.go:313`
|
||||||
|
- `internal/service/user_service.go:319`
|
||||||
|
- comparison baseline: `internal/service/auth_admin_bootstrap.go:42`
|
||||||
|
- comparison baseline: `internal/service/auth_admin_bootstrap.go:64`
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- admin role resolution is hardcoded as `const AdminRoleID = 1` instead of loading the role by stable code.
|
||||||
|
- username existence is checked with `GetByUsername`, but any repository error is silently ignored unless a record is returned.
|
||||||
|
- password strength validation is skipped entirely; the code hashes whatever string is provided.
|
||||||
|
|
||||||
|
Impact:
|
||||||
|
|
||||||
|
- admin creation behavior can diverge from the rest of the authentication stack
|
||||||
|
- non-`record not found` repository errors can be masked
|
||||||
|
- password policy enforcement for administrator accounts becomes weaker than the bootstrap path
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
|
||||||
|
- use `ExistsByUsername` / `ExistsByEmail` and fail on repository errors
|
||||||
|
- reuse the same password validation path as admin bootstrap
|
||||||
|
- resolve the admin role by code (`admin`), not by assumed numeric ID
|
||||||
|
|
||||||
|
#### 5. Avatar upload is still a user-facing stub
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `internal/api/handler/avatar_handler.go:17`
|
||||||
|
- `internal/api/handler/user_handler.go:321`
|
||||||
|
- `frontend/admin/src/pages/admin/ProfileSecurityPage/ProfileSecurityPage.tsx:258`
|
||||||
|
- `frontend/admin/src/pages/admin/ProfileSecurityPage/ProfileSecurityPage.tsx:616`
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- frontend profile UI still allows avatar upload.
|
||||||
|
- both backend avatar handlers still return `"avatar upload not implemented"`.
|
||||||
|
|
||||||
|
Impact:
|
||||||
|
|
||||||
|
- a visible user flow still cannot complete end-to-end
|
||||||
|
- status and completion narratives must continue to treat avatar upload as open
|
||||||
|
|
||||||
|
### Low
|
||||||
|
|
||||||
|
#### 6. `ui-consistency.test.tsx` still emits forbidden native-dialog noise even though the suite is green
|
||||||
|
|
||||||
|
File:
|
||||||
|
|
||||||
|
- `frontend/admin/src/components/common/ui-consistency.test.tsx:167`
|
||||||
|
- `frontend/admin/src/components/common/ui-consistency.test.tsx:199`
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
- the recent timeout/lint fix is real; `npm.cmd run lint` now passes.
|
||||||
|
- but the test file still calls `alert(...)` directly.
|
||||||
|
- jsdom therefore prints `Not implemented: window.alert` traces after both `test:run` and `test:coverage`.
|
||||||
|
|
||||||
|
Impact:
|
||||||
|
|
||||||
|
- test output remains noisy
|
||||||
|
- native-dialog usage is still present in a codebase that explicitly treats `window.alert` / `confirm` / `prompt` / `open` as defect signals
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
|
||||||
|
- replace direct native-dialog calls with spies, stubs, or project-native feedback primitives
|
||||||
|
|
||||||
|
## Historical Findings Rechecked
|
||||||
|
|
||||||
|
The following 2026-04-09 blockers are no longer current in this review:
|
||||||
|
|
||||||
|
- frontend `lint` is no longer red
|
||||||
|
- frontend `build` is no longer red
|
||||||
|
- frontend `test:coverage` no longer times out in this review window
|
||||||
|
- the `ui-consistency` timeout reassignment lint issue has been fixed
|
||||||
|
- `GetUserRoles` / `AssignRoles` are no longer backend stubs
|
||||||
|
- `CreateAdmin` / `DeleteAdmin` are no longer backend stubs
|
||||||
|
|
||||||
|
The following important blockers are still current:
|
||||||
|
|
||||||
|
- avatar upload remains stubbed
|
||||||
|
- full backend verification is still blocked by the `LL_001` SLA gate
|
||||||
|
- the documented browser-level E2E entrypoint is still not green in this review environment
|
||||||
|
|
||||||
|
## Open Questions / Notes
|
||||||
|
|
||||||
|
- The current `e2e:full:win` failure is still concentrated in the wrapper's backend build phase. The repo-level `go build ./cmd/server` command is green under the repo-local cache used for normal verification, but the wrapper's temp-cache build path is still not robust in this review run.
|
||||||
|
- The newly implemented admin-management code paths do not yet have the same depth of negative-path coverage as the rest of the auth/bootstrap flows. This is a testing gap in addition to the code risks above.
|
||||||
|
|
||||||
|
## Real Completion Assessment
|
||||||
|
|
||||||
|
### Can be honestly claimed
|
||||||
|
|
||||||
|
- backend short-path verification is green
|
||||||
|
- backend `go vet` and `go build` are green
|
||||||
|
- frontend `lint`, `build`, unit tests, and coverage are green
|
||||||
|
- current local `govulncheck` run is clean
|
||||||
|
- current production npm dependency audit is clean
|
||||||
|
|
||||||
|
### Cannot be honestly claimed
|
||||||
|
|
||||||
|
- the full verification matrix is green
|
||||||
|
- browser-level E2E closure is currently re-verified
|
||||||
|
- admin-management and role-management flows are fully hardened
|
||||||
|
- avatar upload is fully implemented
|
||||||
|
|
||||||
|
## Final Conclusion
|
||||||
|
|
||||||
|
This project is closer to release shape than the 2026-04-09 snapshot, but it is still not release-closed.
|
||||||
|
|
||||||
|
The largest changes since the previous review are positive on the surface:
|
||||||
|
|
||||||
|
- more of the matrix is green
|
||||||
|
- role/admin endpoints are no longer stubs
|
||||||
|
- frontend lint/build/tests are now passing
|
||||||
|
|
||||||
|
But the newly activated role/admin path now carries real authorization and consistency risks that are more serious than the old stub state, because they can now affect live permissions and admin governance.
|
||||||
|
|
||||||
|
The accurate 2026-04-10 position is:
|
||||||
|
|
||||||
|
- most routine verification gates are green
|
||||||
|
- one full backend SLA gate is still red
|
||||||
|
- browser E2E is still not re-verified closed
|
||||||
|
- the new RBAC/admin code needs hardening before this branch can be treated as production-ready
|
||||||
@@ -1,5 +1,48 @@
|
|||||||
# REAL PROJECT STATUS
|
# REAL PROJECT STATUS
|
||||||
|
|
||||||
|
## 2026-04-10 Review Update
|
||||||
|
|
||||||
|
This section supersedes older status summaries when they conflict with the
|
||||||
|
fresh 2026-04-10 review evidence in
|
||||||
|
`docs/code-review/PROJECT_REAL_COMPLETION_REVIEW_2026-04-10.md`.
|
||||||
|
|
||||||
|
### Fresh verification snapshot
|
||||||
|
|
||||||
|
| Command | Result | Note |
|
||||||
|
|------|------|------|
|
||||||
|
| `go test ./... -short -count=1` | `PASS` | backend short-path matrix is green |
|
||||||
|
| `go vet ./...` | `PASS` | current workspace code is vet-clean |
|
||||||
|
| `go build ./cmd/server` | `PASS` | backend build is green |
|
||||||
|
| `go test ./... -count=1` | `FAIL` | blocked by `internal/service.TestScale_LL_001_180DayLoginLogRetention`, observed `P99=2.2259254s > 2s` |
|
||||||
|
| `cd frontend/admin && npm.cmd run lint` | `PASS` | prior lint blocker is resolved |
|
||||||
|
| `cd frontend/admin && npm.cmd run build` | `PASS` | frontend build is green |
|
||||||
|
| `cd frontend/admin && npm.cmd run test:run` | `PASS` | `59` files / `325` tests, but still prints jsdom `window.alert` noise after success |
|
||||||
|
| `cd frontend/admin && npm.cmd run test:coverage` | `PASS` | coverage green at `88.96 / 78.35 / 86.01 / 89.55`, but same jsdom native-dialog noise remains |
|
||||||
|
| `go run golang.org/x/vuln/cmd/govulncheck@latest ./...` | `PASS` | `No vulnerabilities found.` |
|
||||||
|
| `cd frontend/admin && npm.cmd audit --omit=dev --json --registry=https://registry.npmjs.org/` | `PASS` | production vulnerabilities `0` |
|
||||||
|
| `cd frontend/admin && npm.cmd run e2e:full:win` | `FAIL` | browser E2E wrapper still fails in the backend build/bootstrap stage |
|
||||||
|
|
||||||
|
### Current real blockers
|
||||||
|
|
||||||
|
- Full backend release-style verification is still red because of the `LL_001` login-log pagination SLA gate.
|
||||||
|
- Browser-level E2E cannot yet be honestly claimed re-verified in the current review environment.
|
||||||
|
- The newly implemented role/admin-management path still has hardening gaps:
|
||||||
|
- `GET /api/v1/users/:id/roles` is now live without permission gating.
|
||||||
|
- `DeleteAdmin` still allows self-demotion / last-admin removal.
|
||||||
|
- `AssignRoles` and `CreateAdmin` are still non-transactional.
|
||||||
|
- `CreateAdmin` still hardcodes admin role ID `1` and skips the stronger validation pattern already used by admin bootstrap.
|
||||||
|
- Avatar upload remains a visible stub on the backend.
|
||||||
|
|
||||||
|
### Current honest external statement
|
||||||
|
|
||||||
|
The project now has a mostly green routine verification baseline, but it still
|
||||||
|
cannot be presented as fully release-closed. The correct statement is:
|
||||||
|
|
||||||
|
- backend short-path checks, frontend lint/build/tests, dependency audit, and local vuln scan are green
|
||||||
|
- one full backend SLA gate is still red
|
||||||
|
- browser-level E2E is still not freshly closed in this review
|
||||||
|
- RBAC/admin-management hardening and avatar upload remain open items
|
||||||
|
|
||||||
## 2026-04-09 二次复核更新(与审查报告对齐)
|
## 2026-04-09 二次复核更新(与审查报告对齐)
|
||||||
|
|
||||||
本节基于 2026-04-09 当轮重新执行的本地命令与代码抽查,和
|
本节基于 2026-04-09 当轮重新执行的本地命令与代码抽查,和
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ try {
|
|||||||
$env:GOCACHE = $goCacheDir
|
$env:GOCACHE = $goCacheDir
|
||||||
$env:GOMODCACHE = $goModCacheDir
|
$env:GOMODCACHE = $goModCacheDir
|
||||||
$env:GOPATH = $goPathDir
|
$env:GOPATH = $goPathDir
|
||||||
go build -o $serverExePath .\cmd\server\main.go
|
go build -o $serverExePath ./cmd/server
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw 'server build failed'
|
throw 'server build failed'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -530,13 +530,13 @@ describe('Interaction Behavior', () => {
|
|||||||
|
|
||||||
const handleSearch = vi.fn()
|
const handleSearch = vi.fn()
|
||||||
|
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>
|
||||||
const TestSearchInput = ({ onSearch }: { onSearch: (value: string) => void }) => {
|
const TestSearchInput = ({ onSearch }: { onSearch: (value: string) => void }) => {
|
||||||
let timeout: ReturnType<typeof setTimeout>
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeoutId)
|
||||||
timeout = setTimeout(() => onSearch(e.target.value), 300)
|
timeoutId = setTimeout(() => onSearch(e.target.value), 300)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -243,11 +243,47 @@ func (h *UserHandler) UpdateUserStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) GetUserRoles(c *gin.Context) {
|
func (h *UserHandler) GetUserRoles(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"roles": []interface{}{}})
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := h.userService.GetUserRoles(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
handleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": roles,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) AssignRoles(c *gin.Context) {
|
func (h *UserHandler) AssignRoles(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "role assignment not implemented"})
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
RoleIDs []int64 `json:"role_ids" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.userService.AssignRoles(c.Request.Context(), id, req.RoleIDs); err != nil {
|
||||||
|
handleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "角色分配成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) BatchUpdateStatus(c *gin.Context) {
|
func (h *UserHandler) BatchUpdateStatus(c *gin.Context) {
|
||||||
@@ -287,15 +323,62 @@ func (h *UserHandler) UploadAvatar(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) ListAdmins(c *gin.Context) {
|
func (h *UserHandler) ListAdmins(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"admins": []interface{}{}})
|
admins, err := h.userService.ListAdmins(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
handleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adminResponses := make([]*UserResponse, len(admins))
|
||||||
|
for i, u := range admins {
|
||||||
|
adminResponses[i] = toUserResponse(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": adminResponses})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) CreateAdmin(c *gin.Context) {
|
func (h *UserHandler) CreateAdmin(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "admin creation not implemented"})
|
var req struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adminReq := &service.CreateAdminRequest{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
Email: req.Email,
|
||||||
|
Nickname: req.Nickname,
|
||||||
|
}
|
||||||
|
|
||||||
|
admin, err := h.userService.CreateAdmin(c.Request.Context(), adminReq)
|
||||||
|
if err != nil {
|
||||||
|
handleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"code": 0, "message": "管理员创建成功", "data": toUserResponse(admin)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) DeleteAdmin(c *gin.Context) {
|
func (h *UserHandler) DeleteAdmin(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "admin deletion not implemented"})
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.userService.DeleteAdmin(c.Request.Context(), id); err != nil {
|
||||||
|
handleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "管理员已移除"})
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ func (r *UserRoleRepository) DeleteByUserID(ctx context.Context, userID int64) e
|
|||||||
return r.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&domain.UserRole{}).Error
|
return r.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&domain.UserRole{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteByUserAndRole 删除指定用户和角色的关联
|
||||||
|
func (r *UserRoleRepository) DeleteByUserAndRole(ctx context.Context, userID, roleID int64) error {
|
||||||
|
return r.db.WithContext(ctx).Where("user_id = ? AND role_id = ?", userID, roleID).Delete(&domain.UserRole{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteByRoleID 删除角色的所有用户
|
// DeleteByRoleID 删除角色的所有用户
|
||||||
func (r *UserRoleRepository) DeleteByRoleID(ctx context.Context, roleID int64) error {
|
func (r *UserRoleRepository) DeleteByRoleID(ctx context.Context, roleID int64) error {
|
||||||
return r.db.WithContext(ctx).Where("role_id = ?", roleID).Delete(&domain.UserRole{}).Error
|
return r.db.WithContext(ctx).Where("role_id = ?", roleID).Delete(&domain.UserRole{}).Error
|
||||||
|
|||||||
@@ -211,3 +211,163 @@ func (s *UserService) BatchDelete(ctx context.Context, req *BatchDeleteRequest)
|
|||||||
err := s.userRepo.BatchDelete(ctx, req.IDs)
|
err := s.userRepo.BatchDelete(ctx, req.IDs)
|
||||||
return int64(len(req.IDs)), err
|
return int64(len(req.IDs)), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserRoles 获取用户的所有角色
|
||||||
|
func (s *UserService) GetUserRoles(ctx context.Context, userID int64) ([]*domain.Role, error) {
|
||||||
|
// 检查用户是否存在
|
||||||
|
if _, err := s.userRepo.GetByID(ctx, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色关联
|
||||||
|
userRoles, err := s.userRoleRepo.GetByUserID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userRoles) == 0 {
|
||||||
|
return []*domain.Role{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色ID列表
|
||||||
|
roleIDs := make([]int64, len(userRoles))
|
||||||
|
for i, ur := range userRoles {
|
||||||
|
roleIDs[i] = ur.RoleID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量获取角色详情
|
||||||
|
var roles []*domain.Role
|
||||||
|
for _, roleID := range roleIDs {
|
||||||
|
role, err := s.roleRepo.GetByID(ctx, roleID)
|
||||||
|
if err != nil {
|
||||||
|
continue // 跳过不存在的角色
|
||||||
|
}
|
||||||
|
roles = append(roles, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignRoles 分配用户角色
|
||||||
|
func (s *UserService) AssignRoles(ctx context.Context, userID int64, roleIDs []int64) error {
|
||||||
|
// 检查用户是否存在
|
||||||
|
if _, err := s.userRepo.GetByID(ctx, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证所有角色存在
|
||||||
|
for _, roleID := range roleIDs {
|
||||||
|
if _, err := s.roleRepo.GetByID(ctx, roleID); err != nil {
|
||||||
|
return fmt.Errorf("角色 %d 不存在", roleID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户现有角色
|
||||||
|
if err := s.userRoleRepo.DeleteByUserID(ctx, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的用户角色关联
|
||||||
|
var userRoles []*domain.UserRole
|
||||||
|
for _, roleID := range roleIDs {
|
||||||
|
userRoles = append(userRoles, &domain.UserRole{
|
||||||
|
UserID: userID,
|
||||||
|
RoleID: roleID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.userRoleRepo.BatchCreate(ctx, userRoles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminRoleID is the ID of the admin role
|
||||||
|
const AdminRoleID = 1
|
||||||
|
|
||||||
|
// ListAdmins 获取所有管理员
|
||||||
|
func (s *UserService) ListAdmins(ctx context.Context) ([]*domain.User, error) {
|
||||||
|
// 获取管理员角色ID列表
|
||||||
|
adminUserIDs, err := s.userRoleRepo.GetUserIDByRoleID(ctx, AdminRoleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(adminUserIDs) == 0 {
|
||||||
|
return []*domain.User{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有管理员用户
|
||||||
|
var admins []*domain.User
|
||||||
|
for _, adminID := range adminUserIDs {
|
||||||
|
user, err := s.userRepo.GetByID(ctx, adminID)
|
||||||
|
if err != nil {
|
||||||
|
continue // 跳过不存在的用户
|
||||||
|
}
|
||||||
|
admins = append(admins, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return admins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAdmin 创建管理员
|
||||||
|
func (s *UserService) CreateAdmin(ctx context.Context, req *CreateAdminRequest) (*domain.User, error) {
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
existingUser, err := s.userRepo.GetByUsername(ctx, req.Username)
|
||||||
|
if err == nil && existingUser != nil {
|
||||||
|
return nil, errors.New("用户名已存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
hashedPassword, err := auth.HashPassword(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("密码哈希失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &domain.User{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: hashedPassword,
|
||||||
|
Status: domain.UserStatusActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Email != "" {
|
||||||
|
user.Email = &req.Email
|
||||||
|
}
|
||||||
|
if req.Nickname != "" {
|
||||||
|
user.Nickname = req.Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配管理员角色
|
||||||
|
userRole := &domain.UserRole{
|
||||||
|
UserID: user.ID,
|
||||||
|
RoleID: AdminRoleID,
|
||||||
|
}
|
||||||
|
if err := s.userRoleRepo.Create(ctx, userRole); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAdmin 删除管理员(移除管理员角色)
|
||||||
|
func (s *UserService) DeleteAdmin(ctx context.Context, userID int64) error {
|
||||||
|
// 检查用户是否存在
|
||||||
|
if _, err := s.userRepo.GetByID(ctx, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不能删除自己
|
||||||
|
// 注意:这里需要从handler传入当前用户ID进行校验
|
||||||
|
|
||||||
|
// 删除用户的管理员角色
|
||||||
|
return s.userRoleRepo.DeleteByUserAndRole(ctx, userID, AdminRoleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAdminRequest 创建管理员请求
|
||||||
|
type CreateAdminRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user