// noinspection JSUnresolvedReference
function timeTemplet(context) {
}
layui.use(['table'], function () {
let util = layui.util;
let table = layui.table;
let layer = layui.layer;
table.render({
elem: '#tokenTable',
url: '/token/page',
page: true,
limit: 20,
cols: [[
{field: 'subject', title: '用户(Subject)', width: 160},
{field: 'applicationName', title: '授权应用', width: 160},
{field: 'typeName', title: '类型',},
{field: 'statusName', title: '状态',},
{field: 'creationDate', title: '创建时间', templet: x => util.toDateString(new Date(x.creationDate))},
{field: 'expirationDate', title: '过期时间', templet: x => util.toDateString(new Date(x.expirationDate))},
{field: 'referenceId', title: '引用ID',}
]]
});
function filter() {
table.reload('tokenTable', {
page: {
curr: 1
},
where: {
userId: document.getElementById('userFilter')?.value,
clientId: document.getElementById('clientFilter')?.value
}
});
}
document.getElementById('btnSearch')?.addEventListener('click', filter);
document.getElementById('btnClear').addEventListener('click', function () {
document.getElementById('userFilter').value = '';
document.getElementById('clientFilter').value = '';
filter();
});
function openActionDialog(config) {
const {
title,
description = '',
fields = [],
confirmText = '确认',
cancelText = '取消',
onConfirm,
} = config || {};
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
background: rgba(15, 23, 42, .35); z-index: 9999; padding: 16px;`;
const safeTitle = util.escape(title ?? '操作确认');
const fieldsHtml = fields
.map(field => {
const id = field.id;
const label = util.escape(field.label ?? '');
const placeholder = util.escape(field.placeholder ?? '');
const value = field.value ?? '';
switch (field.type) {
case 'select': {
const optionsHtml = (field.options ?? [])
.map(option => {
const optionValue = option.value ?? '';
const optionLabel = option.label ?? option.text ?? optionValue;
const selected =
String(optionValue) === String(field.value ?? '')
? 'selected'
: '';
return `<option value="${util.escape(optionValue)}" ${selected}>
${util.escape(optionLabel)}
</option>`;
})
.join('');
return `
<div style="display:flex; flex-direction:column; gap:10px;">
<label for="${id}" style="font-size: 13px; color: var(--bs-secondary-color, #64748b);">${label}</label>
<select id="${id}" data-field="${id}" class="form-select" style="
padding: 10px 12px;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, .45);
outline: none; font-size: 15px;">
${optionsHtml}
</select>
</div>`;
}
case 'pin':
return `
<div style="display:flex; flex-direction:column; gap:10px;">
<label for="${id}" style="font-size: 13px; color: var(--bs-secondary-color, #64748b);">${label}</label>
<input id="${id}" data-field="${id}" placeholder="${placeholder}"
inputmode="numeric" pattern="[0-9]{6}" maxlength="6"
style="
padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(99,102,241,.35);
background: rgba(99,102,241,.06); outline: none; font-size: 16px; letter-spacing: 3px;">
<div style="font-size: 12px; color: var(--bs-secondary-color, #94a3b8);">
为安全起见,需验证双因素 PIN
</div>
</div>`;
case 'text':
default:
return `
<div style="display:flex; flex-direction:column; gap:10px;">
<label for="${id}" style="font-size: 13px; color: var(--bs-secondary-color, #64748b);">${label}</label>
<input id="${id}" data-field="${id}" placeholder="${placeholder}"
value="${util.escape(value)}"
style="
padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(148, 163, 184, .45);
background: rgba(15, 23, 42, .02); outline: none; font-size: 15px;">
</div>`;
}
})
.join('');
overlay.innerHTML = `
<div style="
max-width: 480px; width: 100%; background: var(--bg-card, #ffffff);
border-radius: 16px; box-shadow: 0 22px 55px rgba(15, 23, 42, .28); padding: 28px;">
<div style="margin-bottom: 18px;">
<h2 style="font-size: 20px; margin: 0 0 8px 0;">${safeTitle}</h2>
<p style="font-size: 14px; margin: 0; color: var(--bs-secondary-color, #64748b);">${description}</p>
</div>
<div style="display:flex; flex-direction:column; gap: 14px;">
${fieldsHtml}
</div>
<div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 24px;">
<button type="button" data-role="cancel"
style="padding: 8px 18px; border-radius: 10px; border: 1px solid rgba(148, 163, 184, .45); background: transparent;">
${cancelText}
</button>
<button type="button" data-role="confirm"
style="padding: 9px 20px; border-radius: 10px; border: none;
background: linear-gradient(135deg, #4f46e5, #6366f1); color: #fff;">
${confirmText}
</button>
</div>
</div>`;
document.body.appendChild(overlay);
const cancelBtn = overlay.querySelector('[data-role="cancel"]');
const confirmBtn = overlay.querySelector('[data-role="confirm"]');
function close() {
if (overlay.parentElement) {
overlay.parentElement.removeChild(overlay);
}
}
cancelBtn?.addEventListener('click', close);
overlay.addEventListener('click', event => {
if (event.target === overlay) {
close();
}
});
confirmBtn?.addEventListener('click', async () => {
const values = {};
for (const field of fields) {
const input = overlay.querySelector(`[data-field="${field.id}"]`);
if (!input) {
continue;
}
const rawValue =
field.type === 'select' ? input.value : input.value?.trim();
const value = rawValue ?? '';
if (field.required && !value) {
layer.msg(field.requiredMessage || `请填写${field.label ?? ''}`);
input.focus();
return;
}
if (typeof field.validator === 'function') {
const result = field.validator(value);
if (result !== true) {
layer.msg(result || '输入不合法');
input.focus();
return;
}
}
values[field.id] = value;
}
if (typeof onConfirm !== 'function') {
close();
return;
}
confirmBtn.disabled = true;
const originalText = confirmBtn.textContent;
confirmBtn.textContent = '处理中...';
try {
const success = await onConfirm(values);
if (success !== false) {
close();
} else {
confirmBtn.disabled = false;
confirmBtn.textContent = originalText;
const firstField = fields[0];
const firstInput = firstField
? overlay.querySelector(`[data-field="${firstField.id}"]`)
: null;
firstInput?.focus();
}
} catch (error) {
console.warn('token dialog confirm error', error);
confirmBtn.disabled = false;
confirmBtn.textContent = originalText;
}
});
const firstInput = fields[0]
? overlay.querySelector(`[data-field="${fields[0].id}"]`)
: null;
firstInput?.focus();
}
function openRevokeByUser() {
openActionDialog({
title: '按用户撤销令牌',
description: '将撤销指定用户 Subject 的所有访问令牌,用户需重新登录并授权。',
confirmText: '撤销令牌',
cancelText: '取消',
fields: [
{
id: 'userId',
type: 'text',
label: '用户 Subject',
placeholder: '输入用户 ID/Subject',
required: true,
requiredMessage: '请输入用户 ID/Subject',
},
{
id: 'pinCode',
type: 'pin',
label: '2FA PIN',
placeholder: '6位验证码',
required: true,
validator: value =>
/^\d{6}$/.test(value) || '请输入 6 位 PIN',
},
],
onConfirm: values =>
postForm('/Token/RevokeByUser', {
userId: values.userId,
pinCode: values.pinCode,
}),
});
}
function openRevokeByClient() {
const optionElements = Array.from(
document.querySelectorAll('#clientFilter option')
);
const clientOptions = optionElements
.filter(option => option.value)
.map(option => ({
value: option.value,
label: option.textContent?.trim() || option.value,
}));
if (clientOptions.length === 0) {
layer.msg('没有可用的客户端列表');
return;
}
openActionDialog({
title: '按客户端撤销令牌',
description: '将撤销选定客户端的全部访问令牌,关联用户需再次授权。',
confirmText: '撤销令牌',
cancelText: '取消',
fields: [
{
id: 'clientId',
type: 'select',
label: '授权客户端',
options: [{value: '', label: '选择客户端'}, ...clientOptions],
required: true,
requiredMessage: '请选择客户端',
},
{
id: 'pinCode',
type: 'pin',
label: '2FA PIN',
placeholder: '6位验证码',
required: true,
validator: value =>
/^\d{6}$/.test(value) || '请输入 6 位 PIN',
},
],
onConfirm: values =>
postForm('/Token/RevokeByClient', {
clientId: values.clientId,
pinCode: values.pinCode,
}),
});
}
document.getElementById('btnRevokeByUser').addEventListener('click', openRevokeByUser);
document.getElementById('btnRevokeByClient').addEventListener('click', openRevokeByClient);
async function postForm(action, fields) {
const formData = new FormData();
for (const k in fields) {
formData.append(k, fields[k]);
}
try {
const response = await fetch(action, {
method: 'post',
body: formData
});
const result = await response.json();
if (result['success']) {
filter();
layer.msg(result['message']);
return true;
}
layer.alert(result['message']);
return false;
} catch (e) {
console.warn(`get user info fail. error:${e}`);
layer.alert('请求失败,请稍后再试');
return false;
}
}
});