mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
用户详情
This commit is contained in:
+177
@@ -253,6 +253,7 @@ const docTemplate = `{
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "认证源 ID 或名称",
|
"description": "认证源 ID 或名称",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -330,6 +331,7 @@ const docTemplate = `{
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "认证源 ID 或名称",
|
"description": "认证源 ID 或名称",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -397,6 +399,7 @@ const docTemplate = `{
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "认证源 ID 或名称",
|
"description": "认证源 ID 或名称",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -1948,6 +1951,156 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/admin/users/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "返回指定用户的完整个人资料和系统状态,需要管理员权限,不返回密码等敏感字段",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "获取用户详情",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "用户 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "用户详情",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/user.user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "参数错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未登录",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无管理员权限",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "用户不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "内部错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "删除指定非管理员用户,需要管理员权限,不能删除当前登录用户",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "删除用户",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "用户 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "删除成功",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "参数错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未登录",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无管理员权限、尝试删除管理员或当前用户",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "用户不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "内部错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/admin/users/{id}/status": {
|
"/api/v1/admin/users/{id}/status": {
|
||||||
"put": {
|
"put": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -2225,6 +2378,7 @@ const docTemplate = `{
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "外部帐号绑定记录 ID",
|
"description": "外部帐号绑定记录 ID",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -4084,6 +4238,11 @@ const docTemplate = `{
|
|||||||
"UploadStatusPending": "待使用",
|
"UploadStatusPending": "待使用",
|
||||||
"UploadStatusUsed": "已使用"
|
"UploadStatusUsed": "已使用"
|
||||||
},
|
},
|
||||||
|
"x-enum-descriptions": [
|
||||||
|
"待使用",
|
||||||
|
"已使用",
|
||||||
|
"已删除"
|
||||||
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"UploadStatusPending",
|
"UploadStatusPending",
|
||||||
"UploadStatusUsed",
|
"UploadStatusUsed",
|
||||||
@@ -4723,9 +4882,18 @@ const docTemplate = `{
|
|||||||
"avatar_url": {
|
"avatar_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"bio": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -4738,14 +4906,23 @@ const docTemplate = `{
|
|||||||
"last_login_at": {
|
"last_login_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"nickname": {
|
"nickname": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -246,6 +246,7 @@
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "认证源 ID 或名称",
|
"description": "认证源 ID 或名称",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -323,6 +324,7 @@
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "认证源 ID 或名称",
|
"description": "认证源 ID 或名称",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -390,6 +392,7 @@
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "认证源 ID 或名称",
|
"description": "认证源 ID 或名称",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -1941,6 +1944,156 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/admin/users/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "返回指定用户的完整个人资料和系统状态,需要管理员权限,不返回密码等敏感字段",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "获取用户详情",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "用户 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "用户详情",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/user.user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "参数错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未登录",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无管理员权限",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "用户不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "内部错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"SessionCookie": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "删除指定非管理员用户,需要管理员权限,不能删除当前登录用户",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"summary": "删除用户",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "用户 ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "删除成功",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "参数错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未登录",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "无管理员权限、尝试删除管理员或当前用户",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "用户不存在",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "内部错误",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/util.ResponseAny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/admin/users/{id}/status": {
|
"/api/v1/admin/users/{id}/status": {
|
||||||
"put": {
|
"put": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -2218,6 +2371,7 @@
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"description": "外部帐号绑定记录 ID",
|
"description": "外部帐号绑定记录 ID",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@@ -4077,6 +4231,11 @@
|
|||||||
"UploadStatusPending": "待使用",
|
"UploadStatusPending": "待使用",
|
||||||
"UploadStatusUsed": "已使用"
|
"UploadStatusUsed": "已使用"
|
||||||
},
|
},
|
||||||
|
"x-enum-descriptions": [
|
||||||
|
"待使用",
|
||||||
|
"已使用",
|
||||||
|
"已删除"
|
||||||
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"UploadStatusPending",
|
"UploadStatusPending",
|
||||||
"UploadStatusUsed",
|
"UploadStatusUsed",
|
||||||
@@ -4716,9 +4875,18 @@
|
|||||||
"avatar_url": {
|
"avatar_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"bio": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -4731,14 +4899,23 @@
|
|||||||
"last_login_at": {
|
"last_login_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"nickname": {
|
"nickname": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -409,6 +409,10 @@ definitions:
|
|||||||
UploadStatusDeleted: 已删除
|
UploadStatusDeleted: 已删除
|
||||||
UploadStatusPending: 待使用
|
UploadStatusPending: 待使用
|
||||||
UploadStatusUsed: 已使用
|
UploadStatusUsed: 已使用
|
||||||
|
x-enum-descriptions:
|
||||||
|
- 待使用
|
||||||
|
- 已使用
|
||||||
|
- 已删除
|
||||||
x-enum-varnames:
|
x-enum-varnames:
|
||||||
- UploadStatusPending
|
- UploadStatusPending
|
||||||
- UploadStatusUsed
|
- UploadStatusUsed
|
||||||
@@ -841,8 +845,14 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
avatar_url:
|
avatar_url:
|
||||||
type: string
|
type: string
|
||||||
|
bio:
|
||||||
|
type: string
|
||||||
created_at:
|
created_at:
|
||||||
type: string
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
gender:
|
||||||
|
type: string
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
is_active:
|
is_active:
|
||||||
@@ -851,12 +861,18 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
last_login_at:
|
last_login_at:
|
||||||
type: string
|
type: string
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
nickname:
|
nickname:
|
||||||
type: string
|
type: string
|
||||||
|
phone:
|
||||||
|
type: string
|
||||||
updated_at:
|
updated_at:
|
||||||
type: string
|
type: string
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
|
website:
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
util.ResponseAny:
|
util.ResponseAny:
|
||||||
properties:
|
properties:
|
||||||
@@ -1010,6 +1026,7 @@ paths:
|
|||||||
description: 删除指定认证源及其关联的所有外部帐号绑定记录,警告:删除后相关用户将无法通过该源登录,需要管理员权限
|
description: 删除指定认证源及其关联的所有外部帐号绑定记录,警告:删除后相关用户将无法通过该源登录,需要管理员权限
|
||||||
parameters:
|
parameters:
|
||||||
- description: 认证源 ID 或名称
|
- description: 认证源 ID 或名称
|
||||||
|
format: int64
|
||||||
in: path
|
in: path
|
||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
@@ -1049,6 +1066,7 @@ paths:
|
|||||||
description: 更新指定 ID 的认证源配置。若 client_secret 字段为空,则保留原有密钥不变,需要管理员权限
|
description: 更新指定 ID 的认证源配置。若 client_secret 字段为空,则保留原有密钥不变,需要管理员权限
|
||||||
parameters:
|
parameters:
|
||||||
- description: 认证源 ID 或名称
|
- description: 认证源 ID 或名称
|
||||||
|
format: int64
|
||||||
in: path
|
in: path
|
||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
@@ -1099,6 +1117,7 @@ paths:
|
|||||||
description: 启用或禁用指定认证源。尝试启用时将验证 Client ID 和 Client Secret 是否已配置,需要管理员权限
|
description: 启用或禁用指定认证源。尝试启用时将验证 Client ID 和 Client Secret 是否已配置,需要管理员权限
|
||||||
parameters:
|
parameters:
|
||||||
- description: 认证源 ID 或名称
|
- description: 认证源 ID 或名称
|
||||||
|
format: int64
|
||||||
in: path
|
in: path
|
||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
@@ -2038,6 +2057,97 @@ paths:
|
|||||||
summary: 创建用户
|
summary: 创建用户
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
|
/api/v1/admin/users/{id}:
|
||||||
|
delete:
|
||||||
|
description: 删除指定非管理员用户,需要管理员权限,不能删除当前登录用户
|
||||||
|
parameters:
|
||||||
|
- description: 用户 ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 删除成功
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/util.ResponseAny'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: 参数错误
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"401":
|
||||||
|
description: 未登录
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"403":
|
||||||
|
description: 无管理员权限、尝试删除管理员或当前用户
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"404":
|
||||||
|
description: 用户不存在
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"500":
|
||||||
|
description: 内部错误
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
security:
|
||||||
|
- SessionCookie: []
|
||||||
|
summary: 删除用户
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
|
get:
|
||||||
|
description: 返回指定用户的完整个人资料和系统状态,需要管理员权限,不返回密码等敏感字段
|
||||||
|
parameters:
|
||||||
|
- description: 用户 ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 用户详情
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/util.ResponseAny'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/user.user'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: 参数错误
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"401":
|
||||||
|
description: 未登录
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"403":
|
||||||
|
description: 无管理员权限
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"404":
|
||||||
|
description: 用户不存在
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
"500":
|
||||||
|
description: 内部错误
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/util.ResponseAny'
|
||||||
|
security:
|
||||||
|
- SessionCookie: []
|
||||||
|
summary: 获取用户详情
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
/api/v1/admin/users/{id}/status:
|
/api/v1/admin/users/{id}/status:
|
||||||
put:
|
put:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -2235,6 +2345,7 @@ paths:
|
|||||||
description: 解除当前登录用户与指定外部帐号的绑定关系,需要登录
|
description: 解除当前登录用户与指定外部帐号的绑定关系,需要登录
|
||||||
parameters:
|
parameters:
|
||||||
- description: 外部帐号绑定记录 ID
|
- description: 外部帐号绑定记录 ID
|
||||||
|
format: int64
|
||||||
in: path
|
in: path
|
||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -14,16 +14,32 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Eye,
|
Eye,
|
||||||
Filter,
|
Filter,
|
||||||
|
Globe,
|
||||||
Layers,
|
Layers,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Smartphone,
|
||||||
|
Trash2,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
UserX,
|
UserX,
|
||||||
|
VenusAndMars,
|
||||||
X
|
X
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
|
||||||
import {AdminUser} from "@/lib/services"
|
import {AdminUser} from "@/lib/services"
|
||||||
import {cn, formatDateTime} from "@/lib/utils"
|
import {cn, formatDateTime} from "@/lib/utils"
|
||||||
@@ -51,11 +67,16 @@ export function UsersManager() {
|
|||||||
setSearchUsername,
|
setSearchUsername,
|
||||||
setStatusFilter,
|
setStatusFilter,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
updateUserStatus
|
getUserDetail,
|
||||||
|
updateUserStatus,
|
||||||
|
deleteUser
|
||||||
} = useAdminUsers()
|
} = useAdminUsers()
|
||||||
|
|
||||||
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
|
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
|
||||||
const [detailOpen, setDetailOpen] = useState(false)
|
const [detailOpen, setDetailOpen] = useState(false)
|
||||||
|
const [detailLoading, setDetailLoading] = useState(false)
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<AdminUser | null>(null)
|
||||||
|
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,12 +91,38 @@ export function UsersManager() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShowDetail = (user: AdminUser) => {
|
const handleShowDetail = async (user: AdminUser) => {
|
||||||
setSelectedUser(user)
|
setSelectedUser(user)
|
||||||
setDetailOpen(true)
|
setDetailOpen(true)
|
||||||
|
setDetailLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detail = await getUserDetail(user.id)
|
||||||
|
setSelectedUser(detail)
|
||||||
|
} catch {
|
||||||
|
setSelectedUser(user)
|
||||||
|
} finally {
|
||||||
|
setDetailLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteUser = async () => {
|
||||||
|
if (!deleteTarget) return
|
||||||
|
|
||||||
|
setDeleteLoading(true)
|
||||||
|
try {
|
||||||
|
await deleteUser(deleteTarget)
|
||||||
|
if (selectedUser?.id === deleteTarget.id) {
|
||||||
|
setDetailOpen(false)
|
||||||
|
setSelectedUser(null)
|
||||||
|
}
|
||||||
|
setDeleteTarget(null)
|
||||||
|
} finally {
|
||||||
|
setDeleteLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayValue = (value?: string) => value && value.trim() ? value : "-"
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / pageSize)
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
const hasSearchFilter = Boolean(searchUserId || searchUsername)
|
const hasSearchFilter = Boolean(searchUserId || searchUsername)
|
||||||
@@ -318,7 +365,7 @@ export function UsersManager() {
|
|||||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8 pl-4">上次登陆</TableHead>
|
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8 pl-4">上次登陆</TableHead>
|
||||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">注册时间</TableHead>
|
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">注册时间</TableHead>
|
||||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">上次更新</TableHead>
|
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">上次更新</TableHead>
|
||||||
<TableHead className="sticky right-0 text-center bg-background z-10 w-[80px] py-2 h-8">操作</TableHead>
|
<TableHead className="sticky right-0 text-center bg-background z-10 w-[110px] py-2 h-8">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -394,6 +441,26 @@ export function UsersManager() {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
|
{!user.is_admin && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={() => setDeleteTarget(user)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="text-xs">
|
||||||
|
删除用户
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -435,6 +502,13 @@ export function UsersManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{detailLoading && (
|
||||||
|
<div className="flex items-center gap-1 text-[10px] text-muted-foreground">
|
||||||
|
<Loader2 className="size-3 animate-spin" />
|
||||||
|
正在刷新详情
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="gap-4 w-full max-w-[240px] mt-1 pt-4 border-t border-border/50">
|
<div className="gap-4 w-full max-w-[240px] mt-1 pt-4 border-t border-border/50">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<span className="text-[9px] uppercase tracking-widest text-muted-foreground font-medium">注册时间</span>
|
<span className="text-[9px] uppercase tracking-widest text-muted-foreground font-medium">注册时间</span>
|
||||||
@@ -445,9 +519,64 @@ export function UsersManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1">个人资料</h4>
|
||||||
|
<div className="rounded-lg border divide-y bg-background/50">
|
||||||
|
<div className="flex items-center justify-between gap-4 p-3.5 text-sm">
|
||||||
|
<span className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||||
|
<Mail className="size-3" />
|
||||||
|
邮箱
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-[10px]">{displayValue(selectedUser.email)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 p-3.5 text-sm">
|
||||||
|
<span className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||||
|
<Smartphone className="size-3" />
|
||||||
|
手机
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-[10px]">{displayValue(selectedUser.phone)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 p-3.5 text-sm">
|
||||||
|
<span className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||||
|
<VenusAndMars className="size-3" />
|
||||||
|
性别
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-[10px]">{displayValue(selectedUser.gender)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 p-3.5 text-sm">
|
||||||
|
<span className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||||
|
<MapPin className="size-3" />
|
||||||
|
所在地
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-[10px]">{displayValue(selectedUser.location)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 p-3.5 text-sm">
|
||||||
|
<span className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
||||||
|
<Globe className="size-3" />
|
||||||
|
网站
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-[10px]">{displayValue(selectedUser.website)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 p-3.5 text-sm">
|
||||||
|
<span className="text-[10px] text-muted-foreground">简介</span>
|
||||||
|
<span className="break-words text-[10px] leading-5">{displayValue(selectedUser.bio)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1">系统记录</h4>
|
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1">系统记录</h4>
|
||||||
<div className="rounded-lg border divide-y bg-background/50">
|
<div className="rounded-lg border divide-y bg-background/50">
|
||||||
|
<div className="flex items-center justify-between p-3.5 text-sm">
|
||||||
|
<span className="text-[10px]">账户状态</span>
|
||||||
|
<Badge variant={selectedUser.is_active ? "secondary" : "outline"} className="text-[10px]">
|
||||||
|
{selectedUser.is_active ? "正常" : "禁用"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between p-3.5 text-sm">
|
||||||
|
<span className="text-[10px]">管理员</span>
|
||||||
|
<span className="font-mono text-[10px]">{selectedUser.is_admin ? "是" : "否"}</span>
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between p-3.5 text-sm">
|
<div className="flex items-center justify-between p-3.5 text-sm">
|
||||||
<span className="text-[10px]">最后登录</span>
|
<span className="text-[10px]">最后登录</span>
|
||||||
<span className="font-mono text-[10px]">{formatDateTime(selectedUser.last_login_at)}</span>
|
<span className="font-mono text-[10px]">{formatDateTime(selectedUser.last_login_at)}</span>
|
||||||
@@ -468,7 +597,7 @@ export function UsersManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!selectedUser.is_admin && (
|
{!selectedUser.is_admin && (
|
||||||
<div className="p-4 border-t bg-background/80 backdrop-blur-md shrink-0">
|
<div className="p-4 border-t bg-background/80 backdrop-blur-md shrink-0 flex flex-col gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={selectedUser.is_active ? "destructive" : "default"}
|
variant={selectedUser.is_active ? "destructive" : "default"}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -491,6 +620,14 @@ export function UsersManager() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full h-9 text-xs font-medium"
|
||||||
|
onClick={() => setDeleteTarget(selectedUser)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3 mr-1" />
|
||||||
|
删除用户
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -498,6 +635,24 @@ export function UsersManager() {
|
|||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
||||||
|
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && !deleteLoading && setDeleteTarget(null)}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>确认删除用户</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
确定要删除用户 {deleteTarget?.nickname || deleteTarget?.username} 吗?该操作会移除用户账号,删除后无法撤销。
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={deleteLoading}>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDeleteUser} disabled={deleteLoading}>
|
||||||
|
{deleteLoading && <Loader2 className="size-3 animate-spin" />}
|
||||||
|
确认删除
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
<CreateUserModal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} />
|
<CreateUserModal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ interface AdminUsersContextState {
|
|||||||
|
|
||||||
fetchUsers: (force?: boolean) => Promise<void>
|
fetchUsers: (force?: boolean) => Promise<void>
|
||||||
refresh: () => Promise<void>
|
refresh: () => Promise<void>
|
||||||
|
getUserDetail: (id: string) => Promise<AdminUser>
|
||||||
updateUserStatus: (user: AdminUser) => Promise<void>
|
updateUserStatus: (user: AdminUser) => Promise<void>
|
||||||
createUser: (req: CreateUserRequest) => Promise<AdminUser>
|
createUser: (req: CreateUserRequest) => Promise<AdminUser>
|
||||||
|
deleteUser: (user: AdminUser) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes cache
|
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes cache
|
||||||
@@ -189,6 +191,10 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUserDetail = async (id: string) => {
|
||||||
|
return AdminService.getUser(id)
|
||||||
|
}
|
||||||
|
|
||||||
const createUser = async (req: CreateUserRequest) => {
|
const createUser = async (req: CreateUserRequest) => {
|
||||||
try {
|
try {
|
||||||
const newUser = await AdminService.createUser(req)
|
const newUser = await AdminService.createUser(req)
|
||||||
@@ -204,6 +210,19 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteUser = async (user: AdminUser) => {
|
||||||
|
try {
|
||||||
|
await AdminService.deleteUser(user.id)
|
||||||
|
setUsers(prev => prev.filter(u => u.id !== user.id))
|
||||||
|
setTotal(prev => Math.max(0, prev - 1))
|
||||||
|
cacheRef.current = {}
|
||||||
|
toast.success(`已删除用户 ${ user.username }`)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : '删除用户失败')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
users,
|
users,
|
||||||
total,
|
total,
|
||||||
@@ -221,8 +240,10 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
|
|||||||
setStatusFilter,
|
setStatusFilter,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
refresh,
|
refresh,
|
||||||
|
getUserDetail,
|
||||||
updateUserStatus,
|
updateUserStatus,
|
||||||
createUser
|
createUser,
|
||||||
|
deleteUser
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -289,6 +289,15 @@ export class AdminService extends BaseService {
|
|||||||
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
|
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户详情
|
||||||
|
* @param id - 用户 ID
|
||||||
|
* @returns 用户完整资料
|
||||||
|
*/
|
||||||
|
static async getUser(id: string): Promise<AdminUser> {
|
||||||
|
return this.get<AdminUser>(`/users/${ id }`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新用户状态
|
* 更新用户状态
|
||||||
* @param id - 用户 ID
|
* @param id - 用户 ID
|
||||||
@@ -326,6 +335,14 @@ export class AdminService extends BaseService {
|
|||||||
return this.post<AdminUser>('/users', request);
|
return this.post<AdminUser>('/users', request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户
|
||||||
|
* @param id - 用户 ID
|
||||||
|
*/
|
||||||
|
static async deleteUser(id: string): Promise<void> {
|
||||||
|
return this.delete<void>(`/users/${ id }`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统状态
|
* 获取系统状态
|
||||||
* @returns 系统状态指标数据
|
* @returns 系统状态指标数据
|
||||||
|
|||||||
@@ -194,12 +194,24 @@ export interface AdminUser {
|
|||||||
username: string;
|
username: string;
|
||||||
/** 昵称 */
|
/** 昵称 */
|
||||||
nickname: string;
|
nickname: string;
|
||||||
|
/** 邮箱 */
|
||||||
|
email?: string;
|
||||||
/** 头像 URL */
|
/** 头像 URL */
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
/** 是否激活 */
|
/** 是否激活 */
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
/** 是否管理员 */
|
/** 是否管理员 */
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
|
/** 个人简介 */
|
||||||
|
bio?: string;
|
||||||
|
/** 手机号 */
|
||||||
|
phone?: string;
|
||||||
|
/** 性别 */
|
||||||
|
gender?: string;
|
||||||
|
/** 个人网站 */
|
||||||
|
website?: string;
|
||||||
|
/** 所在地 */
|
||||||
|
location?: string;
|
||||||
/** 最后登录时间 */
|
/** 最后登录时间 */
|
||||||
last_login_at: string;
|
last_login_at: string;
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
@@ -363,4 +375,3 @@ export interface UpdateTemplateRequest {
|
|||||||
content: string;
|
content: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ package user
|
|||||||
const (
|
const (
|
||||||
userNotFound = "用户不存在"
|
userNotFound = "用户不存在"
|
||||||
cannotDisable = "不能禁用管理员用户"
|
cannotDisable = "不能禁用管理员用户"
|
||||||
|
cannotDelete = "不能删除管理员用户"
|
||||||
|
cannotDeleteSelf = "不能删除当前登录用户"
|
||||||
updateUserFailed = "更新用户状态失败"
|
updateUserFailed = "更新用户状态失败"
|
||||||
|
deleteUserFailed = "删除用户失败"
|
||||||
usernameExists = "用户名已存在"
|
usernameExists = "用户名已存在"
|
||||||
usernameRequired = "用户名不能为空"
|
usernameRequired = "用户名不能为空"
|
||||||
passwordTooShort = "密码长度不能少于 8 位"
|
passwordTooShort = "密码长度不能少于 8 位"
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
@@ -45,9 +47,15 @@ type user struct {
|
|||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
|
Email string `json:"email"`
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Gender string `json:"gender"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
Location string `json:"location"`
|
||||||
LastLoginAt time.Time `json:"last_login_at"`
|
LastLoginAt time.Time `json:"last_login_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -59,6 +67,35 @@ type listUsersResponse struct {
|
|||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseUserID(c *gin.Context) (uint64, bool) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, util.Err(userNotFound))
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUser(u model.User) user {
|
||||||
|
return user{
|
||||||
|
ID: u.ID,
|
||||||
|
Username: u.Username,
|
||||||
|
Nickname: u.Nickname,
|
||||||
|
Email: u.Email,
|
||||||
|
AvatarURL: u.AvatarURL,
|
||||||
|
IsActive: u.IsActive,
|
||||||
|
IsAdmin: u.IsAdmin,
|
||||||
|
Bio: u.Bio,
|
||||||
|
Phone: u.Phone,
|
||||||
|
Gender: u.Gender,
|
||||||
|
Website: u.Website,
|
||||||
|
Location: u.Location,
|
||||||
|
LastLoginAt: u.LastLoginAt,
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
UpdatedAt: u.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ListUsers 获取用户列表
|
// ListUsers 获取用户列表
|
||||||
// @Summary 获取用户列表
|
// @Summary 获取用户列表
|
||||||
// @Description 分页返回用户列表,支持按用户 ID 和用户名筛选,需要管理员权限
|
// @Description 分页返回用户列表,支持按用户 ID 和用户名筛选,需要管理员权限
|
||||||
@@ -118,6 +155,43 @@ func ListUsers(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUser 获取用户详情
|
||||||
|
// @Summary 获取用户详情
|
||||||
|
// @Description 返回指定用户的完整个人资料和系统状态,需要管理员权限,不返回密码等敏感字段
|
||||||
|
// @Tags admin
|
||||||
|
// @Produce json
|
||||||
|
// @Security SessionCookie
|
||||||
|
// @Param id path int true "用户 ID"
|
||||||
|
// @Success 200 {object} util.ResponseAny{data=user.user} "用户详情"
|
||||||
|
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||||
|
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||||
|
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||||
|
// @Failure 404 {object} util.ResponseAny "用户不存在"
|
||||||
|
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||||
|
// @Router /api/v1/admin/users/{id} [get]
|
||||||
|
func GetUser(c *gin.Context) {
|
||||||
|
id, ok := parseUserID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUser model.User
|
||||||
|
if err := db.DB(c.Request.Context()).
|
||||||
|
Select("id, username, nickname, email, avatar_url, is_active, is_admin, "+
|
||||||
|
"bio, phone, gender, website, location, last_login_at, created_at, updated_at").
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&targetUser).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, util.Err(userNotFound))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, util.OK(toUser(targetUser)))
|
||||||
|
}
|
||||||
|
|
||||||
// updateUserStatusRequest 更新用户状态请求
|
// updateUserStatusRequest 更新用户状态请求
|
||||||
type updateUserStatusRequest struct {
|
type updateUserStatusRequest struct {
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
@@ -146,7 +220,10 @@ func UpdateUserStatus(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id := c.Param("id")
|
id, ok := parseUserID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var targetUser struct {
|
var targetUser struct {
|
||||||
ID uint64 `gorm:"column:id"`
|
ID uint64 `gorm:"column:id"`
|
||||||
@@ -181,6 +258,70 @@ func UpdateUserStatus(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, util.OKNil())
|
c.JSON(http.StatusOK, util.OKNil())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUser 删除用户
|
||||||
|
// @Summary 删除用户
|
||||||
|
// @Description 删除指定非管理员用户,需要管理员权限,不能删除当前登录用户
|
||||||
|
// @Tags admin
|
||||||
|
// @Produce json
|
||||||
|
// @Security SessionCookie
|
||||||
|
// @Param id path int true "用户 ID"
|
||||||
|
// @Success 200 {object} util.ResponseAny{data=string} "删除成功"
|
||||||
|
// @Failure 400 {object} util.ResponseAny "参数错误"
|
||||||
|
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||||
|
// @Failure 403 {object} util.ResponseAny "无管理员权限、尝试删除管理员或当前用户"
|
||||||
|
// @Failure 404 {object} util.ResponseAny "用户不存在"
|
||||||
|
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||||
|
// @Router /api/v1/admin/users/{id} [delete]
|
||||||
|
func DeleteUser(c *gin.Context) {
|
||||||
|
id, ok := parseUserID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||||
|
if currUser != nil && currUser.ID == id {
|
||||||
|
c.JSON(http.StatusForbidden, util.Err(cannotDeleteSelf))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUser struct {
|
||||||
|
ID uint64 `gorm:"column:id"`
|
||||||
|
IsAdmin bool `gorm:"column:is_admin"`
|
||||||
|
}
|
||||||
|
if err := db.DB(c.Request.Context()).
|
||||||
|
Table("users").
|
||||||
|
Select("id, is_admin").
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&targetUser).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, util.Err(userNotFound))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetUser.IsAdmin {
|
||||||
|
c.JSON(http.StatusForbidden, util.Err(cannotDelete))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB(c.Request.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Where("user_id = ?", id).Delete(&model.AccessToken{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("user_id = ?", id).Delete(&model.ExternalAccount{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Where("id = ?", id).Delete(&model.User{}).Error
|
||||||
|
}); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, util.Err(deleteUserFailed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, util.OKNil())
|
||||||
|
}
|
||||||
|
|
||||||
// createUserRequest 创建用户请求
|
// createUserRequest 创建用户请求
|
||||||
type createUserRequest struct {
|
type createUserRequest struct {
|
||||||
Username string `json:"username" binding:"required,min=3,max=64"`
|
Username string `json:"username" binding:"required,min=3,max=64"`
|
||||||
@@ -257,17 +398,5 @@ func CreateUser(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := user{
|
c.JSON(http.StatusOK, util.OK(toUser(newUser)))
|
||||||
ID: newUser.ID,
|
|
||||||
Username: newUser.Username,
|
|
||||||
Nickname: newUser.Nickname,
|
|
||||||
AvatarURL: newUser.AvatarURL,
|
|
||||||
IsActive: newUser.IsActive,
|
|
||||||
IsAdmin: newUser.IsAdmin,
|
|
||||||
LastLoginAt: newUser.LastLoginAt,
|
|
||||||
CreatedAt: newUser.CreatedAt,
|
|
||||||
UpdatedAt: newUser.UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, util.OK(res))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ func setupTestRouter(authUser *model.User) *gin.Engine {
|
|||||||
|
|
||||||
adminGroup.GET("/users", ListUsers)
|
adminGroup.GET("/users", ListUsers)
|
||||||
adminGroup.POST("/users", CreateUser)
|
adminGroup.POST("/users", CreateUser)
|
||||||
|
adminGroup.GET("/users/:id", GetUser)
|
||||||
adminGroup.PUT("/users/:id/status", UpdateUserStatus)
|
adminGroup.PUT("/users/:id/status", UpdateUserStatus)
|
||||||
|
adminGroup.DELETE("/users/:id", DeleteUser)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +172,72 @@ func TestListUsers(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetUser(t *testing.T) {
|
||||||
|
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
targetUser := model.User{
|
||||||
|
ID: 1001,
|
||||||
|
Username: "alice",
|
||||||
|
Password: "secret-hash",
|
||||||
|
Nickname: "Alice Nickname",
|
||||||
|
Email: "alice@example.com",
|
||||||
|
AvatarURL: "https://example.com/avatar.png",
|
||||||
|
IsActive: true,
|
||||||
|
IsAdmin: false,
|
||||||
|
Bio: "hello",
|
||||||
|
Phone: "123456",
|
||||||
|
Gender: "female",
|
||||||
|
Website: "https://example.com",
|
||||||
|
Location: "Shanghai",
|
||||||
|
}
|
||||||
|
if err := dbConn.Create(&targetUser).Error; err != nil {
|
||||||
|
t.Fatalf("failed to seed user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true}
|
||||||
|
router := setupTestRouter(adminUser)
|
||||||
|
|
||||||
|
t.Run("get full user profile", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("GET", "/api/v1/admin/users/1001", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp util.ResponseAny
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("failed to parse response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, _ := json.Marshal(resp.Data)
|
||||||
|
var resUser user
|
||||||
|
if err := json.Unmarshal(dataBytes, &resUser); err != nil {
|
||||||
|
t.Fatalf("failed to parse response data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resUser.Email != targetUser.Email || resUser.Bio != targetUser.Bio || resUser.Phone != targetUser.Phone ||
|
||||||
|
resUser.Gender != targetUser.Gender || resUser.Website != targetUser.Website || resUser.Location != targetUser.Location {
|
||||||
|
t.Errorf("profile fields were not returned correctly: %+v", resUser)
|
||||||
|
}
|
||||||
|
if bytes.Contains(dataBytes, []byte("secret-hash")) {
|
||||||
|
t.Error("response should not include password")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get non-existent user", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("GET", "/api/v1/admin/users/9999", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("expected 404 Not Found, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateUserStatus(t *testing.T) {
|
func TestUpdateUserStatus(t *testing.T) {
|
||||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -349,3 +417,117 @@ func TestCreateUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteUser(t *testing.T) {
|
||||||
|
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
regularUser := model.User{
|
||||||
|
ID: 1001,
|
||||||
|
Username: "alice",
|
||||||
|
IsActive: true,
|
||||||
|
IsAdmin: false,
|
||||||
|
}
|
||||||
|
adminUser := model.User{
|
||||||
|
ID: 1002,
|
||||||
|
Username: "bob",
|
||||||
|
IsActive: true,
|
||||||
|
IsAdmin: true,
|
||||||
|
}
|
||||||
|
selfUser := model.User{
|
||||||
|
ID: 1003,
|
||||||
|
Username: "charlie",
|
||||||
|
IsActive: true,
|
||||||
|
IsAdmin: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbConn.Create(®ularUser).Error; err != nil {
|
||||||
|
t.Fatalf("failed to seed regular user: %v", err)
|
||||||
|
}
|
||||||
|
if err := dbConn.Create(&adminUser).Error; err != nil {
|
||||||
|
t.Fatalf("failed to seed admin user: %v", err)
|
||||||
|
}
|
||||||
|
if err := dbConn.Create(&selfUser).Error; err != nil {
|
||||||
|
t.Fatalf("failed to seed self user: %v", err)
|
||||||
|
}
|
||||||
|
if err := dbConn.Create(&model.AccessToken{
|
||||||
|
UserID: regularUser.ID,
|
||||||
|
Name: "api",
|
||||||
|
TokenHash: "hash-for-delete-user-test",
|
||||||
|
MaskedToken: "at_****test",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("failed to seed access token: %v", err)
|
||||||
|
}
|
||||||
|
if err := dbConn.Create(&model.ExternalAccount{
|
||||||
|
ID: 5001,
|
||||||
|
AuthSourceID: 1,
|
||||||
|
UserID: regularUser.ID,
|
||||||
|
ExternalID: "external-alice",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("failed to seed external account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := setupTestRouter(&selfUser)
|
||||||
|
|
||||||
|
t.Run("delete regular user successfully", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/1001", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := dbConn.Model(&model.User{}).Where("id = ?", 1001).Count(&count).Error; err != nil {
|
||||||
|
t.Fatalf("failed to count deleted user: %v", err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("expected deleted user count 0, got %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbConn.Model(&model.AccessToken{}).Where("user_id = ?", 1001).Count(&count).Error; err != nil {
|
||||||
|
t.Fatalf("failed to count deleted access tokens: %v", err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("expected deleted access token count 0, got %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbConn.Model(&model.ExternalAccount{}).Where("user_id = ?", 1001).Count(&count).Error; err != nil {
|
||||||
|
t.Fatalf("failed to count deleted external accounts: %v", err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("expected deleted external account count 0, got %d", count)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cannot delete admin user", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/1002", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("expected 403 Forbidden, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cannot delete current user", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/1003", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("expected 403 Forbidden, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete non-existent user", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("DELETE", "/api/v1/admin/users/9999", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("expected 404 Not Found, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -215,7 +215,9 @@ func Serve() {
|
|||||||
// Users
|
// Users
|
||||||
adminRouter.GET("/users", admin_user.ListUsers)
|
adminRouter.GET("/users", admin_user.ListUsers)
|
||||||
adminRouter.POST("/users", admin_user.CreateUser)
|
adminRouter.POST("/users", admin_user.CreateUser)
|
||||||
|
adminRouter.GET("/users/:id", admin_user.GetUser)
|
||||||
adminRouter.PUT("/users/:id/status", admin_user.UpdateUserStatus)
|
adminRouter.PUT("/users/:id/status", admin_user.UpdateUserStatus)
|
||||||
|
adminRouter.DELETE("/users/:id", admin_user.DeleteUser)
|
||||||
|
|
||||||
// System Config
|
// System Config
|
||||||
adminRouter.POST("/system-configs", system_config.CreateSystemConfig)
|
adminRouter.POST("/system-configs", system_config.CreateSystemConfig)
|
||||||
|
|||||||
Reference in New Issue
Block a user