1
This commit is contained in:
@@ -171,6 +171,18 @@ const definitions = {
|
|||||||
required: ["user_id", "expire_at"],
|
required: ["user_id", "expire_at"],
|
||||||
properties: {
|
properties: {
|
||||||
user_id: { type: "integer", format: "int64" },
|
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" },
|
token_name: { type: "string" },
|
||||||
expire_at: { 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/page"] = post("API Token 分页(不含 secret_cipher)", "BizAdminPageRequest");
|
||||||
paths["/admin_api/biz_token/create"] = post("创建 Token,返回 plain_token", "BizTokenCreateRequest");
|
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/revoke"] = post("吊销 Token", "BizTokenIdRequest");
|
||||||
paths["/admin_api/biz_token/regenerate"] = post("重新生成密钥(返回新 plain_token)", "BizTokenIdRequest");
|
paths["/admin_api/biz_token/regenerate"] = post("重新生成密钥(返回新 plain_token)", "BizTokenIdRequest");
|
||||||
paths["/admin_api/biz_token/export"] = post("Token 导出", "BizAdminPageRequest");
|
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",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "账号唯一标识,可选"
|
||||||
|
},
|
||||||
"token_name": {
|
"token_name": {
|
||||||
"type": "string"
|
"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": {
|
"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",
|
"swagger": "2.0",
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ class TokenServer {
|
|||||||
return window.framework.http.post("/biz_token/create", row);
|
return window.framework.http.post("/biz_token/create", row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async edit(row) {
|
||||||
|
return window.framework.http.post("/biz_token/edit", row);
|
||||||
|
}
|
||||||
|
|
||||||
async revoke(row) {
|
async revoke(row) {
|
||||||
return window.framework.http.post("/biz_token/revoke", row);
|
return window.framework.http.post("/biz_token/revoke", row);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const componentMap = {
|
|||||||
'subscription/plan': SubscriptionPlans,
|
'subscription/plan': SubscriptionPlans,
|
||||||
'subscription/subscription': SubscriptionRecords,
|
'subscription/subscription': SubscriptionRecords,
|
||||||
'subscription/token': SubscriptionTokens,
|
'subscription/token': SubscriptionTokens,
|
||||||
|
/** 与 biz_api_token 管理页同一视图,便于菜单 component 语义对应 */
|
||||||
|
'subscription/biz_api_token': SubscriptionTokens,
|
||||||
'subscription/payment': SubscriptionPayment,
|
'subscription/payment': SubscriptionPayment,
|
||||||
'subscription/usage': SubscriptionUsage,
|
'subscription/usage': SubscriptionUsage,
|
||||||
'subscription/audit': SubscriptionAuditLog,
|
'subscription/audit': SubscriptionAuditLog,
|
||||||
|
|||||||
@@ -52,6 +52,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<Modal v-model="createModal" title="创建 Token" width="560" :loading="saving" @on-ok="submitCreate">
|
||||||
<Form :label-width="100">
|
<Form :label-width="100">
|
||||||
<FormItem label="用户">
|
<FormItem label="用户">
|
||||||
@@ -91,6 +99,9 @@ export default {
|
|||||||
seachOption: { key: 'user_id', value: '' },
|
seachOption: { key: 'user_id', value: '' },
|
||||||
pageOption: { page: 1, pageSize: 20, total: 0 },
|
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||||
},
|
},
|
||||||
|
editModal: false,
|
||||||
|
editSaving: false,
|
||||||
|
editForm: {},
|
||||||
createModal: false,
|
createModal: false,
|
||||||
plainModal: false,
|
plainModal: false,
|
||||||
plainModalTitle: '请立即保存 Token 明文',
|
plainModalTitle: '请立即保存 Token 明文',
|
||||||
@@ -113,15 +124,26 @@ export default {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'a',
|
key: 'a',
|
||||||
width: 178,
|
width: 248,
|
||||||
render: (h, p) => {
|
render: (h, p) => {
|
||||||
const btns = []
|
const btns = []
|
||||||
|
btns.push(
|
||||||
|
h(
|
||||||
|
'Button',
|
||||||
|
{
|
||||||
|
props: { type: 'primary', size: 'small', ghost: true },
|
||||||
|
on: { click: () => this.openEdit(p.row) },
|
||||||
|
},
|
||||||
|
'编辑'
|
||||||
|
)
|
||||||
|
)
|
||||||
if (p.row.status === 'active') {
|
if (p.row.status === 'active') {
|
||||||
btns.push(
|
btns.push(
|
||||||
h(
|
h(
|
||||||
'Button',
|
'Button',
|
||||||
{
|
{
|
||||||
props: { type: 'warning', size: 'small' },
|
props: { type: 'warning', size: 'small' },
|
||||||
|
class: { ml8: true },
|
||||||
on: { click: () => this.doRegenerate(p.row) },
|
on: { click: () => this.doRegenerate(p.row) },
|
||||||
},
|
},
|
||||||
'重新生成'
|
'重新生成'
|
||||||
@@ -133,7 +155,7 @@ export default {
|
|||||||
'Button',
|
'Button',
|
||||||
{
|
{
|
||||||
props: { type: 'error', size: 'small' },
|
props: { type: 'error', size: 'small' },
|
||||||
class: { ml8: btns.length > 0 },
|
class: { ml8: true },
|
||||||
on: { click: () => this.doRevoke(p.row) },
|
on: { click: () => this.doRevoke(p.row) },
|
||||||
},
|
},
|
||||||
'吊销'
|
'吊销'
|
||||||
@@ -191,6 +213,61 @@ export default {
|
|||||||
this._submitCreate()
|
this._submitCreate()
|
||||||
return false
|
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() {
|
async _submitCreate() {
|
||||||
const uid = this.createForm.user_id
|
const uid = this.createForm.user_id
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
· ID {{ create_token_target_user.id }}
|
· ID {{ create_token_target_user.id }}
|
||||||
</p>
|
</p>
|
||||||
<Form :label-width="100">
|
<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.token_name" placeholder="default" /></FormItem>
|
||||||
<FormItem label="过期时间"><Input v-model="create_token_form.expire_at" placeholder="YYYY-MM-DD 23:59:59" />
|
<FormItem label="过期时间"><Input v-model="create_token_form.expire_at" placeholder="YYYY-MM-DD 23:59:59" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -429,6 +430,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.create_token_target_user = user_row
|
this.create_token_target_user = user_row
|
||||||
this.create_token_form = {
|
this.create_token_form = {
|
||||||
|
key: '',
|
||||||
token_name: 'default',
|
token_name: 'default',
|
||||||
expire_at: this.default_token_expire_input(),
|
expire_at: this.default_token_expire_input(),
|
||||||
}
|
}
|
||||||
@@ -446,6 +448,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const res = await tokenServer.create({
|
const res = await tokenServer.create({
|
||||||
user_id: row.id,
|
user_id: row.id,
|
||||||
|
key: this.create_token_form.key || null,
|
||||||
token_name: this.create_token_form.token_name || 'default',
|
token_name: this.create_token_form.token_name || 'default',
|
||||||
expire_at: this.create_token_form.expire_at,
|
expire_at: this.create_token_form.expire_at,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ module.exports = {
|
|||||||
warn: result.warn,
|
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) => {
|
"POST /biz_token/revoke": async (ctx) => {
|
||||||
const body = ctx.getBody();
|
const body = ctx.getBody();
|
||||||
const row = await tokenLogic.revokeToken(body);
|
const row = await tokenLogic.revokeToken(body);
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
const Sequelize = require("sequelize");
|
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) => {
|
module.exports = (db) => {
|
||||||
const biz_api_token = db.define(
|
const biz_api_token = db.define(
|
||||||
"biz_api_token",
|
"biz_api_token",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const Sequelize = require("sequelize");
|
|||||||
const op = Sequelize.Op;
|
const op = Sequelize.Op;
|
||||||
const baseModel = require("../../middleware/baseModel");
|
const baseModel = require("../../middleware/baseModel");
|
||||||
const biz_token_secret_cipher = require("../utils/biz_token_secret_cipher");
|
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;
|
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) {
|
async function revokeToken(body) {
|
||||||
const id = body.id;
|
const id = body.id;
|
||||||
if (id == null) throw new Error("缺少 id");
|
if (id == null) throw new Error("缺少 id");
|
||||||
@@ -130,6 +160,7 @@ async function revokeAllForUser(userId) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
hashPlainToken,
|
hashPlainToken,
|
||||||
createToken,
|
createToken,
|
||||||
|
updateToken,
|
||||||
regenerateToken,
|
regenerateToken,
|
||||||
revokeToken,
|
revokeToken,
|
||||||
revokeAllForUser,
|
revokeAllForUser,
|
||||||
|
|||||||
Reference in New Issue
Block a user