1
This commit is contained in:
@@ -171,6 +171,18 @@ const definitions = {
|
||||
required: ["user_id", "expire_at"],
|
||||
properties: {
|
||||
user_id: { type: "integer", format: "int64" },
|
||||
key: { type: "string", description: "账号唯一标识,可选" },
|
||||
token_name: { type: "string" },
|
||||
expire_at: { type: "string" },
|
||||
},
|
||||
},
|
||||
BizTokenEditRequest: {
|
||||
type: "object",
|
||||
title: "BizTokenEditRequest",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "integer", format: "int64" },
|
||||
key: { type: "string", description: "可空表示清空" },
|
||||
token_name: { type: "string" },
|
||||
expire_at: { type: "string" },
|
||||
},
|
||||
@@ -306,6 +318,7 @@ paths["/admin_api/biz_subscription/export"] = post("订阅导出 CSV 数据", "B
|
||||
|
||||
paths["/admin_api/biz_token/page"] = post("API Token 分页(不含 secret_cipher)", "BizAdminPageRequest");
|
||||
paths["/admin_api/biz_token/create"] = post("创建 Token,返回 plain_token", "BizTokenCreateRequest");
|
||||
paths["/admin_api/biz_token/edit"] = post("编辑 Token(名称/key/过期时间,不改密钥)", "BizTokenEditRequest");
|
||||
paths["/admin_api/biz_token/revoke"] = post("吊销 Token", "BizTokenIdRequest");
|
||||
paths["/admin_api/biz_token/regenerate"] = post("重新生成密钥(返回新 plain_token)", "BizTokenIdRequest");
|
||||
paths["/admin_api/biz_token/export"] = post("Token 导出", "BizAdminPageRequest");
|
||||
|
||||
34
_docs/sql/sys_menu_subscription_api_token.sql
Normal file
34
_docs/sql/sys_menu_subscription_api_token.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- 订阅模块:API Token(biz_api_token)管理端菜单
|
||||
-- 说明:
|
||||
-- 1) 若执行报错「表不存在」,请将 `sys_menus` 改为实际表名(常见为 `sys_menu` 或 `sys_menus`)。
|
||||
-- 2) parent_id:挂到「订阅」等父菜单下时,改为实际父菜单 id;顶层可填 0。
|
||||
-- 3) component 必须与 admin/src/router/component-map.js 的 key 一致(二选一):
|
||||
-- subscription/token 或 subscription/biz_api_token
|
||||
|
||||
INSERT INTO `sys_menu` (
|
||||
`name`,
|
||||
`parent_id`,
|
||||
`icon`,
|
||||
`path`,
|
||||
`type`,
|
||||
`model_id`,
|
||||
`form_id`,
|
||||
`component`,
|
||||
`api_path`,
|
||||
`is_show_menu`,
|
||||
`is_show`,
|
||||
`sort`
|
||||
) VALUES (
|
||||
'API Token',
|
||||
0,
|
||||
'md-key',
|
||||
'/subscription/token',
|
||||
'页面',
|
||||
0,
|
||||
0,
|
||||
'subscription/token',
|
||||
'',
|
||||
1,
|
||||
1,
|
||||
45
|
||||
);
|
||||
@@ -3865,6 +3865,10 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "账号唯一标识,可选"
|
||||
},
|
||||
"token_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4085,6 +4089,29 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"BizTokenEditRequest": {
|
||||
"type": "object",
|
||||
"title": "BizTokenEditRequest",
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "可空表示清空"
|
||||
},
|
||||
"token_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"expire_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -10492,6 +10519,50 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin_api/biz_api_call_log/export": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"管理端-业务订阅"
|
||||
],
|
||||
"summary": "API 调用明细导出",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/BizAdminPageRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "框架统一包装;成功时 code=0,业务数据在 data"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin_api/biz_token/edit": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"管理端-业务订阅"
|
||||
],
|
||||
"summary": "编辑 Token(名称/key/过期时间,不改密钥)",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "body",
|
||||
"name": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/BizTokenEditRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "框架统一包装;成功时 code=0,业务数据在 data"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"swagger": "2.0",
|
||||
|
||||
@@ -7,6 +7,10 @@ class TokenServer {
|
||||
return window.framework.http.post("/biz_token/create", row);
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
return window.framework.http.post("/biz_token/edit", row);
|
||||
}
|
||||
|
||||
async revoke(row) {
|
||||
return window.framework.http.post("/biz_token/revoke", row);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ const componentMap = {
|
||||
'subscription/plan': SubscriptionPlans,
|
||||
'subscription/subscription': SubscriptionRecords,
|
||||
'subscription/token': SubscriptionTokens,
|
||||
/** 与 biz_api_token 管理页同一视图,便于菜单 component 语义对应 */
|
||||
'subscription/biz_api_token': SubscriptionTokens,
|
||||
'subscription/payment': SubscriptionPayment,
|
||||
'subscription/usage': SubscriptionUsage,
|
||||
'subscription/audit': SubscriptionAuditLog,
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal v-model="editModal" title="编辑 Token" width="560" :loading="editSaving" @on-ok="submitEdit">
|
||||
<Form :label-width="100">
|
||||
<FormItem label="账号 key"><Input v-model="editForm.key" placeholder="账号唯一标识(可空)" /></FormItem>
|
||||
<FormItem label="名称"><Input v-model="editForm.token_name" placeholder="名称" /></FormItem>
|
||||
<FormItem label="过期时间"><Input v-model="editForm.expire_at" placeholder="YYYY-MM-DD HH:mm:ss" /></FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="createModal" title="创建 Token" width="560" :loading="saving" @on-ok="submitCreate">
|
||||
<Form :label-width="100">
|
||||
<FormItem label="用户">
|
||||
@@ -91,6 +99,9 @@ export default {
|
||||
seachOption: { key: 'user_id', value: '' },
|
||||
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||
},
|
||||
editModal: false,
|
||||
editSaving: false,
|
||||
editForm: {},
|
||||
createModal: false,
|
||||
plainModal: false,
|
||||
plainModalTitle: '请立即保存 Token 明文',
|
||||
@@ -113,15 +124,26 @@ export default {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'a',
|
||||
width: 178,
|
||||
width: 248,
|
||||
render: (h, p) => {
|
||||
const btns = []
|
||||
btns.push(
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'primary', size: 'small', ghost: true },
|
||||
on: { click: () => this.openEdit(p.row) },
|
||||
},
|
||||
'编辑'
|
||||
)
|
||||
)
|
||||
if (p.row.status === 'active') {
|
||||
btns.push(
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'warning', size: 'small' },
|
||||
class: { ml8: true },
|
||||
on: { click: () => this.doRegenerate(p.row) },
|
||||
},
|
||||
'重新生成'
|
||||
@@ -133,7 +155,7 @@ export default {
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'error', size: 'small' },
|
||||
class: { ml8: btns.length > 0 },
|
||||
class: { ml8: true },
|
||||
on: { click: () => this.doRevoke(p.row) },
|
||||
},
|
||||
'吊销'
|
||||
@@ -191,6 +213,61 @@ export default {
|
||||
this._submitCreate()
|
||||
return false
|
||||
},
|
||||
openEdit(row) {
|
||||
this.editForm = {
|
||||
id: row.id,
|
||||
key: row.key != null ? String(row.key) : '',
|
||||
token_name: row.token_name || '',
|
||||
expire_at: row.expire_at
|
||||
? typeof row.expire_at === 'string'
|
||||
? row.expire_at
|
||||
: this._fmt_expire(row.expire_at)
|
||||
: '',
|
||||
}
|
||||
this.editModal = true
|
||||
},
|
||||
_fmt_expire(d) {
|
||||
if (!d) return ''
|
||||
const dt = new Date(d)
|
||||
if (Number.isNaN(dt.getTime())) return String(d)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(
|
||||
dt.getMinutes()
|
||||
)}:${pad(dt.getSeconds())}`
|
||||
},
|
||||
submitEdit() {
|
||||
if (!this.editForm.id) return false
|
||||
if (!this.editForm.token_name || !String(this.editForm.token_name).trim()) {
|
||||
this.$Message.warning('请填写名称')
|
||||
return false
|
||||
}
|
||||
if (!this.editForm.expire_at || !String(this.editForm.expire_at).trim()) {
|
||||
this.$Message.warning('请填写过期时间')
|
||||
return false
|
||||
}
|
||||
this.editSaving = true
|
||||
this._submitEdit()
|
||||
return false
|
||||
},
|
||||
async _submitEdit() {
|
||||
try {
|
||||
const res = await tokenServer.edit({
|
||||
id: this.editForm.id,
|
||||
key: this.editForm.key === '' ? null : this.editForm.key,
|
||||
token_name: this.editForm.token_name,
|
||||
expire_at: this.editForm.expire_at,
|
||||
})
|
||||
if (res && res.code === 0) {
|
||||
this.editModal = false
|
||||
this.$Message.success('已保存')
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '保存失败')
|
||||
}
|
||||
} finally {
|
||||
this.editSaving = false
|
||||
}
|
||||
},
|
||||
async _submitCreate() {
|
||||
const uid = this.createForm.user_id
|
||||
try {
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
· ID {{ create_token_target_user.id }}
|
||||
</p>
|
||||
<Form :label-width="100">
|
||||
<FormItem label="账号 key"><Input v-model="create_token_form.key" placeholder="账号唯一标识(可选)" /></FormItem>
|
||||
<FormItem label="名称"><Input v-model="create_token_form.token_name" placeholder="default" /></FormItem>
|
||||
<FormItem label="过期时间"><Input v-model="create_token_form.expire_at" placeholder="YYYY-MM-DD 23:59:59" />
|
||||
</FormItem>
|
||||
@@ -429,6 +430,7 @@ export default {
|
||||
}
|
||||
this.create_token_target_user = user_row
|
||||
this.create_token_form = {
|
||||
key: '',
|
||||
token_name: 'default',
|
||||
expire_at: this.default_token_expire_input(),
|
||||
}
|
||||
@@ -446,6 +448,7 @@ export default {
|
||||
try {
|
||||
const res = await tokenServer.create({
|
||||
user_id: row.id,
|
||||
key: this.create_token_form.key || null,
|
||||
token_name: this.create_token_form.token_name || 'default',
|
||||
expire_at: this.create_token_form.expire_at,
|
||||
})
|
||||
|
||||
@@ -45,6 +45,21 @@ module.exports = {
|
||||
warn: result.warn,
|
||||
});
|
||||
},
|
||||
"POST /biz_token/edit": async (ctx) => {
|
||||
const body = ctx.getBody();
|
||||
const row = await tokenLogic.updateToken(body);
|
||||
await audit.logAudit({
|
||||
admin_user_id: audit.pickAdminId(ctx),
|
||||
biz_user_id: row.user_id,
|
||||
action: "biz_token.edit",
|
||||
resource_type: "biz_api_token",
|
||||
resource_id: row.id,
|
||||
detail: { token_name: row.token_name, key: row.key },
|
||||
});
|
||||
const plain = row.get ? row.get({ plain: true }) : { ...row };
|
||||
delete plain.secret_cipher;
|
||||
ctx.success(plain);
|
||||
},
|
||||
"POST /biz_token/revoke": async (ctx) => {
|
||||
const body = ctx.getBody();
|
||||
const row = await tokenLogic.revokeToken(body);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const Sequelize = require("sequelize");
|
||||
|
||||
/**
|
||||
* 业务 API Token(管理端页面:admin/src/views/subscription/tokens.vue)
|
||||
* 动态路由 component 与 admin/src/router/component-map.js 中
|
||||
* subscription/token 或 subscription/biz_api_token 对应。
|
||||
*/
|
||||
module.exports = (db) => {
|
||||
const biz_api_token = db.define(
|
||||
"biz_api_token",
|
||||
|
||||
@@ -3,6 +3,7 @@ const Sequelize = require("sequelize");
|
||||
const op = Sequelize.Op;
|
||||
const baseModel = require("../../middleware/baseModel");
|
||||
const biz_token_secret_cipher = require("../utils/biz_token_secret_cipher");
|
||||
const { normalize_for_write } = require("../utils/query_helpers");
|
||||
|
||||
const MAX_TOKENS_PER_USER = 5;
|
||||
|
||||
@@ -74,6 +75,35 @@ async function createToken(body) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端编辑:名称、账号 key、过期时间(不改密钥)
|
||||
*/
|
||||
async function updateToken(body) {
|
||||
const id = body.id;
|
||||
if (id == null || id === "") throw new Error("缺少 id");
|
||||
const row = await baseModel.biz_api_token.findByPk(id);
|
||||
if (!row) throw new Error("Token 不存在");
|
||||
|
||||
const payload = normalize_for_write(
|
||||
baseModel.biz_api_token,
|
||||
{
|
||||
token_name: body.token_name,
|
||||
key: body.key,
|
||||
expire_at: body.expire_at,
|
||||
},
|
||||
{ for_create: false }
|
||||
);
|
||||
const patch = {};
|
||||
if (payload.token_name !== undefined) patch.token_name = payload.token_name;
|
||||
if (payload.key !== undefined) patch.key = payload.key;
|
||||
if (payload.expire_at !== undefined) patch.expire_at = payload.expire_at;
|
||||
if (Object.keys(patch).length === 0) throw new Error("没有可更新字段");
|
||||
|
||||
await row.update(patch);
|
||||
await row.reload();
|
||||
return row;
|
||||
}
|
||||
|
||||
async function revokeToken(body) {
|
||||
const id = body.id;
|
||||
if (id == null) throw new Error("缺少 id");
|
||||
@@ -130,6 +160,7 @@ async function revokeAllForUser(userId) {
|
||||
module.exports = {
|
||||
hashPlainToken,
|
||||
createToken,
|
||||
updateToken,
|
||||
regenerateToken,
|
||||
revokeToken,
|
||||
revokeAllForUser,
|
||||
|
||||
Reference in New Issue
Block a user