网站首页 网站源码
website
站点相关全部源代码,隐藏了一些关于服务器的信息
// 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;
        }
    }
});
loading