Files
console-dev/app/pages/account/api-keys.vue
T

941 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
/**
* API 密钥管理:列表分页、创建、IP 白名单编辑及删除。私钥仅在创建响应中保留,关闭弹窗或离开页面即清空,不能视为可再次查询的资料。
*/
import "~/assets/styles/api-keys.Bq3Csr53.css";
import { toDisplayString } from "vue";
import { onMounted } from "vue";
import { onBeforeUnmount } from "vue";
import { createBlock } from "vue";
import { ref } from "vue";
import { withCtx } from "vue";
import { createCommentVNode } from "vue";
import { computed } from "vue";
import { reactive } from "vue";
import { isRef } from "vue";
import { createElementBlock } from "vue";
import { Fragment } from "vue";
import { createElementVNode } from "vue";
import { createTextVNode } from "vue";
import { unref } from "vue";
import { defineComponent } from "vue";
import { createVNode } from "vue";
import { openBlock } from "vue";
import { withModifiers } from "vue";
import { useHead } from "#app";
import { onBeforeRouteLeave } from "vue-router";
import { NuxtLink } from "#components";
import { useApi } from "~/lib/auth.js";
import dialog from "primevue/dialog";
import button from "primevue/button";
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
import message from "primevue/message";
import inputtext from "primevue/inputtext";
import datatable from "primevue/datatable";
import paginator from "primevue/paginator";
import checkbox from "primevue/checkbox";
import column from "primevue/column";
import textarea from "primevue/textarea";
// 集中管理列表权限、写操作忙碌态和仅本次可见的私钥;请求代次与存活标记防止旧响应更新已离开的页面。
function D() {
let e = useApi(),
t = ref([]),
r = ref(0),
a = ref(1),
o = ref(!1),
s = ref(!1),
l = ref(!1),
u = ref(!1),
d = ref(``),
f = ref(``),
p = ref(``),
m = ref(null),
h = ref(null),
g = reactive({
enabled: !1,
ip: ``,
}),
_ = ref(null),
v = !0,
y = 0,
b = () => {
m.value = null;
};
onBeforeUnmount(() => {
((v = !1), y++, b());
});
// 刷新当前页并读取服务端 create_api 权限;响应结构无效时不开放创建入口。
async function x() {
if (!v) return;
let n = ++y;
((o.value = !0), (d.value = ``), (u.value = !1));
try {
let i = await e.get(`/api`, {
page: a.value,
limit: 20,
orderby: `id`,
sort: `desc`,
});
if (!v || n !== y) return;
if (!Array.isArray(i?.list)) throw Error(`密钥列表响应异常,请重试`);
((t.value = i.list),
(r.value = Number(i.count) || 0),
(u.value = Number(i.create_api) === 1),
(l.value = !0));
} catch (e) {
v &&
n === y &&
((d.value = e instanceof Error ? e.message : `加载失败`),
(l.value = !1));
} finally {
v && n === y && (o.value = !1);
}
}
// 只在权限已加载且允许创建时提交。创建响应不完整时提示先刷新核实,避免误以为失败而重复生成密钥。
async function C() {
if (!(!v || s.value || m.value)) {
if (((d.value = ``), (f.value = ``), !l.value || o.value)) {
d.value = `权限尚未加载,请刷新列表`;
return;
}
if (!u.value) {
d.value = `当前账户暂未开放 API 密钥创建`;
return;
}
if (!p.value.trim()) {
d.value = `请填写密钥名称`;
return;
}
s.value = !0;
try {
let t = await e.post(`/api`, {
name: p.value.trim(),
});
if (!v) return;
if (!t?.private_key || !t?.token)
throw Error(
`服务端未返回完整密钥,请刷新列表核实创建结果,避免重复创建`,
);
((m.value = t),
(p.value = ``),
(f.value = `密钥已创建,请立即保存私钥。关闭后无法再次查看。`),
(a.value = 1),
await x());
} catch (e) {
v &&
(d.value = `${e instanceof Error ? e.message : `创建失败`}。请先刷新列表核实结果,再决定是否重试。`);
} finally {
v && (s.value = !1);
}
}
}
// 打开白名单编辑器时复制行数据,未保存的输入不直接修改列表。
function w(e) {
s.value ||
((d.value = ``),
(f.value = ``),
(h.value = e.id),
(g.enabled = Number(e.status) === 1),
(g.ip = e.ip || ``));
}
// 开启白名单必须提供 IP;保存成功后关闭编辑器并重新读取服务端数据。
async function T() {
if (!v || s.value || !h.value) return;
if (((d.value = ``), (f.value = ``), g.enabled && !g.ip.trim())) {
d.value = `开启白名单时请填写 IP 地址`;
return;
}
let t = h.value;
s.value = !0;
try {
if (
(await e.put(`/api/${t}/white_list`, {
status: +!!g.enabled,
ip: g.ip.trim(),
}),
!v)
)
return;
((h.value = null), (f.value = `白名单设置已保存`), await x());
} catch (e) {
v && (d.value = e instanceof Error ? e.message : `保存失败`);
} finally {
v && (s.value = !1);
}
}
// 删除成功后,若当前页只剩这一条则回退一页,再刷新列表。
async function E() {
if (v && !s.value && _.value?.id) {
((d.value = ``), (f.value = ``), (s.value = !0));
try {
if ((await e.del(`/api/${_.value.id}`), !v)) return;
((_.value = null),
(f.value = `密钥已删除`),
t.value.length === 1 && a.value > 1 && a.value--,
await x());
} catch (e) {
v && (d.value = e instanceof Error ? e.message : `删除失败`);
} finally {
v && (s.value = !1);
}
}
}
return {
rows: t,
count: r,
page: a,
limit: 20,
loading: o,
busy: s,
loaded: l,
createAllowed: u,
error: d,
success: f,
name: p,
secret: m,
clearSecret: b,
whitelistId: h,
whitelist: g,
deleteTarget: _,
load: x,
create: C,
editWhitelist: w,
saveWhitelist: T,
remove: E,
};
}
var O = {
class: `api-page account-management-page`,
},
k = {
class: `account-management-head`,
},
A = {
class: `account-management-actions`,
},
j = {
class: `create-key-field`,
},
oe = {
key: 2,
class: `muted`,
},
se = {
class: `list-table`,
},
ce = {
class: `token`,
},
le = {
class: `row-actions`,
},
ue = {
class: `token`,
},
de = {
key: 0,
},
M = _plugin_vue_export_helper_t(
defineComponent({
__name: `api-keys`,
setup(n) {
useHead({
title: `API 密钥`,
});
let {
rows: c,
count: h,
page: S,
limit: T,
loading: M,
busy: N,
loaded: P,
createAllowed: F,
error: I,
success: L,
name: R,
secret: z,
clearSecret: B,
whitelistId: V,
whitelist: H,
deleteTarget: U,
load: W,
create: G,
editWhitelist: fe,
saveWhitelist: K,
remove: pe,
} = D(),
q = ref(!1),
J = ref(``),
me = computed(() => !!z.value),
Y = computed({
get: () => V.value !== null,
set: (e) => {
!e && !N.value && (V.value = null);
},
}),
X = computed({
get: () => !!U.value,
set: (e) => {
!e && !N.value && (U.value = null);
},
});
// 复制创建时返回的私钥;剪贴板不可用时保留手动复制提示,不把私钥写入持久存储。
async function he() {
if (z.value) {
J.value = ``;
try {
(await navigator.clipboard.writeText(z.value.private_key),
(q.value = !0));
} catch {
J.value = `复制失败,请手动选择并复制私钥`;
}
}
}
// 关闭私钥展示同时清空密钥与复制反馈,避免之后再次暴露。
function Z() {
(B(), (q.value = !1), (J.value = ``));
}
// 分页器为零基页码,接口页码从 1 开始。
function ge(e) {
((S.value = e.page + 1), W());
}
let _e = (e) => (e ? new Date(e * 1e3).toLocaleString(`zh-CN`) : `—`);
return (
onMounted(W),
onBeforeRouteLeave(
() =>
!z.value ||
window.confirm(
`私钥仅显示一次,请确认已保存后离开。离开后将无法再次查看。`,
),
),
// 视图分别呈现权限/列表状态、白名单编辑与一次性私钥弹窗;关闭私钥必须调用 Z 清空敏感状态。
(t, n) => {
let i = NuxtLink,
s = button,
y = message,
b = inputtext,
D = column,
B = datatable,
V = paginator,
Q = textarea,
$ = dialog,
ve = checkbox;
return (
openBlock(),
createElementBlock(`section`, O, [
createElementVNode(`header`, k, [
createElementVNode(`div`, null, [
(n[10] ||= createElementVNode(`h2`, null, `API 密钥`, -1)),
createElementVNode(`p`, null, [
(n[9] ||= createTextVNode(
`管理程序访问凭证及 IP 白名单。`,
-1,
)),
createVNode(
i,
{
to: `/logbook?type=api`,
},
{
default: withCtx(() => [
...(n[8] ||= [
createTextVNode(`查看 API 日志`, -1),
]),
]),
_: 1,
},
),
]),
]),
createElementVNode(`div`, A, [
createVNode(
i,
{
to: `/account`,
class: `account-management-link`,
},
{
default: withCtx(() => [
...(n[11] ||= [
createElementVNode(
`i`,
{
class: `pi pi-angle-left`,
"aria-hidden": `true`,
},
null,
-1,
),
createTextVNode(`账户设置`, -1),
]),
]),
_: 1,
},
),
createVNode(
s,
{
label: `刷新`,
icon: `pi pi-refresh`,
severity: `secondary`,
outlined: ``,
loading: unref(M),
disabled: unref(N),
onClick: unref(W),
},
null,
8,
[`loading`, `disabled`, `onClick`],
),
]),
]),
unref(I)
? (openBlock(),
createBlock(
y,
{
key: 0,
severity: `error`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(toDisplayString(unref(I)), 1),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
unref(L)
? (openBlock(),
createBlock(
y,
{
key: 1,
severity: `success`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(toDisplayString(unref(L)), 1),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
createElementVNode(
`form`,
{
class: `create-key`,
onSubmit: (n[1] ||= withModifiers(
(...e) => unref(G) && unref(G)(...e),
[`prevent`],
)),
},
[
createElementVNode(`div`, j, [
(n[12] ||= createElementVNode(
`label`,
{
for: `api-name`,
},
`密钥名称`,
-1,
)),
createVNode(
b,
{
id: `api-name`,
modelValue: unref(R),
"onUpdate:modelValue": (n[0] ||= (e) =>
isRef(R) ? (R.value = e) : null),
maxlength: `100`,
placeholder: `例如:自动化管理`,
disabled: unref(N) || !unref(F),
},
null,
8,
[`modelValue`, `disabled`],
),
]),
createVNode(
s,
{
type: `submit`,
label: `创建密钥`,
loading: unref(N),
disabled:
!unref(P) || !unref(F) || unref(M) || !!unref(z),
},
null,
8,
[`loading`, `disabled`],
),
],
32,
),
unref(P) && !unref(F)
? (openBlock(),
createElementBlock(`p`, oe, `当前账户暂未开放创建密钥。`))
: createCommentVNode(``, !0),
createElementVNode(`div`, se, [
createVNode(
B,
{
value: unref(c),
loading: unref(M),
"data-key": `id`,
scrollable: ``,
},
{
empty: withCtx(() => [
createTextVNode(
toDisplayString(
unref(P)
? `暂无 API 密钥`
: `尚未加载密钥,请刷新重试`,
),
1,
),
]),
default: withCtx(() => [
createVNode(D, {
field: `name`,
header: `名称`,
}),
createVNode(
D,
{
field: `token`,
header: `Token`,
},
{
body: withCtx(({ data: t }) => [
createElementVNode(
`code`,
ce,
toDisplayString(t.token),
1,
),
]),
_: 1,
},
),
createVNode(
D,
{
header: `创建时间`,
},
{
body: withCtx(({ data: t }) => [
createTextVNode(
toDisplayString(_e(t.create_time)),
1,
),
]),
_: 1,
},
),
createVNode(
D,
{
header: `IP 白名单`,
},
{
body: withCtx(({ data: t }) => [
createTextVNode(
toDisplayString(
Number(t.status) === 1 ? t.ip : `未开启`,
),
1,
),
]),
_: 1,
},
),
createVNode(
D,
{
header: `操作`,
},
{
body: withCtx(({ data: e }) => [
createElementVNode(`div`, le, [
createVNode(
s,
{
label: `白名单`,
text: ``,
disabled: unref(N),
onClick: (t) => unref(fe)(e),
},
null,
8,
[`disabled`, `onClick`],
),
createVNode(
s,
{
label: `删除`,
severity: `danger`,
text: ``,
disabled: unref(N),
onClick: (t) => (U.value = e),
},
null,
8,
[`disabled`, `onClick`],
),
]),
]),
_: 1,
},
),
]),
_: 1,
},
8,
[`value`, `loading`],
),
]),
unref(h) > unref(T)
? (openBlock(),
createBlock(
V,
{
key: 3,
first: (unref(S) - 1) * unref(T),
rows: unref(T),
"total-records": unref(h),
onPage: ge,
},
null,
8,
[`first`, `rows`, `total-records`],
))
: createCommentVNode(``, !0),
createVNode(
$,
{
visible: unref(me),
header: `保存新私钥`,
modal: ``,
closable: !1,
draggable: !1,
style: {
width: `600px`,
maxWidth: `94vw`,
},
},
{
footer: withCtx(() => [
createVNode(
s,
{
label: unref(q) ? `已复制` : `复制私钥`,
severity: `secondary`,
onClick: he,
},
null,
8,
[`label`],
),
createVNode(s, {
label: `已安全保存,关闭`,
onClick: Z,
}),
]),
default: withCtx(() => [
(n[16] ||= createElementVNode(
`p`,
null,
`私钥仅显示一次。请保存到安全位置后关闭此窗口。`,
-1,
)),
unref(z)
? (openBlock(),
createElementBlock(
Fragment,
{
key: 0,
},
[
createElementVNode(`p`, null, [
(n[13] ||= createElementVNode(
`strong`,
null,
`Token:`,
-1,
)),
createElementVNode(
`code`,
ue,
toDisplayString(unref(z).token),
1,
),
]),
unref(z).api_url
? (openBlock(),
createElementBlock(`p`, de, [
(n[14] ||= createElementVNode(
`strong`,
null,
`API 地址:`,
-1,
)),
createTextVNode(
toDisplayString(unref(z).api_url),
1,
),
]))
: createCommentVNode(``, !0),
(n[15] ||= createElementVNode(
`label`,
{
for: `private-key`,
},
`私钥`,
-1,
)),
createVNode(
Q,
{
id: `private-key`,
"model-value": unref(z).private_key,
readonly: ``,
rows: `8`,
fluid: ``,
},
null,
8,
[`model-value`],
),
],
64,
))
: createCommentVNode(``, !0),
unref(J)
? (openBlock(),
createBlock(
y,
{
key: 1,
severity: `error`,
},
{
default: withCtx(() => [
createTextVNode(toDisplayString(unref(J)), 1),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
]),
_: 1,
},
8,
[`visible`],
),
createVNode(
$,
{
visible: unref(Y),
"onUpdate:visible": (n[5] ||= (e) =>
isRef(Y) ? (Y.value = e) : null),
header: `API 白名单`,
modal: ``,
closable: !unref(N),
draggable: !1,
style: {
width: `480px`,
maxWidth: `94vw`,
},
},
{
default: withCtx(() => [
createElementVNode(
`form`,
{
class: `whitelist-form`,
onSubmit: (n[4] ||= withModifiers(
(...e) => unref(K) && unref(K)(...e),
[`prevent`],
)),
},
[
createElementVNode(`div`, null, [
createVNode(
ve,
{
modelValue: unref(H).enabled,
"onUpdate:modelValue": (n[2] ||= (e) =>
(unref(H).enabled = e)),
"input-id": `api-whitelist-on`,
binary: ``,
disabled: unref(N),
},
null,
8,
[`modelValue`, `disabled`],
),
(n[17] ||= createTextVNode()),
(n[18] ||= createElementVNode(
`label`,
{
for: `api-whitelist-on`,
},
`启用 IP 白名单`,
-1,
)),
]),
(n[19] ||= createElementVNode(
`label`,
{
for: `api-whitelist-ip`,
},
`允许访问的 IP 地址`,
-1,
)),
createVNode(
Q,
{
id: `api-whitelist-ip`,
modelValue: unref(H).ip,
"onUpdate:modelValue": (n[3] ||= (e) =>
(unref(H).ip = e)),
disabled: unref(N) || !unref(H).enabled,
rows: `4`,
fluid: ``,
},
null,
8,
[`modelValue`, `disabled`],
),
unref(I)
? (openBlock(),
createBlock(
y,
{
key: 0,
severity: `error`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(
toDisplayString(unref(I)),
1,
),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
createVNode(
s,
{
type: `submit`,
label: `保存`,
loading: unref(N),
},
null,
8,
[`loading`],
),
],
32,
),
]),
_: 1,
},
8,
[`visible`, `closable`],
),
createVNode(
$,
{
visible: unref(X),
"onUpdate:visible": (n[7] ||= (e) =>
isRef(X) ? (X.value = e) : null),
header: `删除 API 密钥`,
modal: ``,
closable: !unref(N),
draggable: !1,
style: {
width: `450px`,
maxWidth: `94vw`,
},
},
{
footer: withCtx(() => [
createVNode(
s,
{
label: `取消`,
severity: `secondary`,
disabled: unref(N),
onClick: (n[6] ||= (e) => (U.value = null)),
},
null,
8,
[`disabled`],
),
createVNode(
s,
{
label: `确认删除`,
severity: `danger`,
loading: unref(N),
onClick: unref(pe),
},
null,
8,
[`loading`, `onClick`],
),
]),
default: withCtx(() => [
createElementVNode(
`p`,
null,
`确认删除「` +
toDisplayString(unref(U)?.name) +
`」?使用此密钥的程序将立即失去访问权限。`,
1,
),
unref(I)
? (openBlock(),
createBlock(
y,
{
key: 0,
severity: `error`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(toDisplayString(unref(I)), 1),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
]),
_: 1,
},
8,
[`visible`, `closable`],
),
])
);
}
);
},
}),
[[`__scopeId`, `data-v-76e29360`]],
);
export {};
export default M;
</script>