(() => { const ACCESS_TOKEN_KEY = 'tiku_platform_access_token'; const REFRESH_TOKEN_KEY = 'tiku_platform_refresh_token'; const ACCESS_EXPIRES_KEY = 'tiku_platform_access_expires_at'; const USER_KEY = 'tiku_platform_user'; const runtime = window.GONGXUE_PLATFORM_RUNTIME_CONFIG || {}; let challengeToken = ''; let pendingResolve; function apiUrl(path) { return `${String(runtime.apiBaseUrl || '').replace(/\/+$/, '')}${path}`; } async function post(path, body) { const response = await fetch(apiUrl(path), { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, credentials: 'omit', body: JSON.stringify(body), }); const text = await response.text(); let payload = {}; if (text) { try { payload = JSON.parse(text); } catch { payload = { detail: text.slice(0, 500) }; } } if (!response.ok) throw new Error(payload.detail || payload.title || payload.message || `请求失败(HTTP ${response.status})`); return payload; } function gate() { return document.querySelector('#platformAuthGate'); } function escapeHtml(value) { return String(value ?? '').replace(/[&<>'"]/g, character => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"', })[character]); } function shell(title, description, content) { gate().innerHTML = ``; gate().hidden = false; document.querySelector('.platform-app')?.setAttribute('inert', ''); } function showError(error) { const target = document.querySelector('#platformAuthError'); if (target) target.textContent = error?.message || '操作失败,请重试'; } function setBusy(form, busy) { form.querySelectorAll('button,input').forEach(control => { control.disabled = busy; }); } function storeAuthenticated(result) { const user = result?.user; const tokens = user?.tokens; if (!tokens?.accessToken || !tokens?.refreshToken) throw new Error('登录响应缺少令牌'); sessionStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken); sessionStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken); sessionStorage.setItem(ACCESS_EXPIRES_KEY, tokens.accessTokenExpiresAt || ''); sessionStorage.setItem(USER_KEY, JSON.stringify({ userId: user.userId, email: user.email, name: user.name })); } function completeGate() { gate().hidden = true; gate().innerHTML = ''; document.querySelector('.platform-app')?.removeAttribute('inert'); pendingResolve?.(true); pendingResolve = null; } function finishAuthentication(result, recoveryCodes = []) { storeAuthenticated(result); if (recoveryCodes.length) { shell('保存恢复代码', '这些代码只显示一次,请保存到安全位置。', `
${recoveryCodes.map(code => `
${escapeHtml(code)}
`).join('')}

保存后再进入平台控制台。

`); document.querySelector('#platformAuthContinue').addEventListener('click', completeGate); return; } completeGate(); } async function handleAuthenticationResult(result) { challengeToken = result.challengeToken || ''; if (result.status === 'authenticated') { finishAuthentication(result); return; } if (result.status === 'password_change_required') { renderPasswordChange(); return; } if (result.status === 'mfa_enrollment_required') { await renderMfaEnrollment(); return; } if (result.status === 'mfa_required') { renderMfaVerification(); return; } throw new Error(`不支持的认证状态:${result.status || 'unknown'}`); } function renderLogin() { shell('平台管理员登录', '连接真实 PostgreSQL 与 ASP.NET Core API。', `
`); document.querySelector('#platformLoginForm').addEventListener('submit', async event => { event.preventDefault(); const form = event.currentTarget; setBusy(form, true); try { const result = await post('/api/auth/login/password', { realm: 'platform', identifier: form.elements.identifier.value.trim(), password: form.elements.password.value }); await handleAuthenticationResult(result); } catch (error) { showError(error); setBusy(form, false); } }); } function renderPasswordChange() { shell('设置正式密码', '首次登录必须先替换临时密码。', `
`); document.querySelector('#platformPasswordForm').addEventListener('submit', async event => { event.preventDefault(); const form = event.currentTarget; if (form.elements.newPassword.value !== form.elements.confirmPassword.value) { showError(new Error('两次输入的密码不一致')); return; } setBusy(form, true); try { await handleAuthenticationResult(await post('/api/auth/password/change-required', { challengeToken, newPassword: form.elements.newPassword.value })); } catch (error) { showError(error); setBusy(form, false); } }); } async function renderMfaEnrollment() { const setup = await post('/api/auth/mfa/totp/setup', { challengeToken }); shell('绑定双重验证', '在认证器中添加密钥,然后输入当前 6 位验证码。', `${escapeHtml(setup.sharedKey)}

也可在支持的认证器中导入:${escapeHtml(setup.authenticatorUri)}

`); document.querySelector('#platformMfaForm').addEventListener('submit', async event => { event.preventDefault(); const form = event.currentTarget; setBusy(form, true); try { const result = await post('/api/auth/mfa/totp/confirm', { challengeToken, code: form.elements.code.value.trim() }); finishAuthentication(result.authentication, result.recoveryCodes || []); } catch (error) { showError(error); setBusy(form, false); } }); } function renderMfaVerification() { shell('双重验证', '输入认证器中的当前 6 位验证码。', `
`); document.querySelector('#platformMfaForm').addEventListener('submit', async event => { event.preventDefault(); const form = event.currentTarget; setBusy(form, true); try { await handleAuthenticationResult(await post('/api/auth/mfa/totp/verify', { challengeToken, code: form.elements.code.value.trim() })); } catch (error) { showError(error); setBusy(form, false); } }); } function clearSession() { [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ACCESS_EXPIRES_KEY, USER_KEY].forEach(key => sessionStorage.removeItem(key)); } function hasSession() { const token = sessionStorage.getItem(ACCESS_TOKEN_KEY); const expiresAt = Date.parse(sessionStorage.getItem(ACCESS_EXPIRES_KEY) || ''); return Boolean(token) && Number.isFinite(expiresAt) && expiresAt > Date.now() + 15_000; } async function requireSession() { if (hasSession()) return true; clearSession(); renderLogin(); return new Promise(resolve => { pendingResolve = resolve; }); } async function logout() { const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY); try { if (refreshToken) await post('/api/auth/logout', { refreshToken }); } catch { /* Clear the browser session even when revocation cannot be reached. */ } clearSession(); location.reload(); } window.GongxuePlatformAuth = Object.freeze({ getAccessToken: () => sessionStorage.getItem(ACCESS_TOKEN_KEY) || '', getUser: () => { try { return JSON.parse(sessionStorage.getItem(USER_KEY) || 'null'); } catch { return null; } }, logout, requireSession, }); })();