Files
tiku-backend.net/Tiku.Api/wwwroot/platform-admin/platform-auth.js

142 lines
6.3 KiB
JavaScript
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.

(() => {
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 shell(title, description, content) {
gate().innerHTML = `<section class="platform-auth-card" role="dialog" aria-modal="true"><header><img src="./assets/logo.png" alt="" /><div><h1>${title}</h1><p>${description}</p></div></header>${content}<p class="platform-auth-error" id="platformAuthError" role="alert"></p></section>`;
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) {
storeAuthenticated(result);
completeGate();
}
async function handleAuthenticationResult(result) {
challengeToken = result.challengeToken || '';
if (result.status === 'authenticated') { finishAuthentication(result); return; }
if (result.status === 'password_change_required') { renderPasswordChange(); return; }
throw new Error(`不支持的认证状态:${result.status || 'unknown'}`);
}
function renderLogin() {
shell('平台管理员登录', '连接真实 PostgreSQL 与 ASP.NET Core API。', `<form id="platformLoginForm"><label>平台账号<input name="identifier" type="email" autocomplete="username" required /></label><label>密码<input name="password" type="password" autocomplete="current-password" minlength="8" required /></label><button type="submit">登录</button></form>`);
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('设置正式密码', '至少 8 位,且必须同时包含字母和数字。', `<form id="platformPasswordForm"><label>新密码<input name="newPassword" type="password" autocomplete="new-password" minlength="8" required /></label><label>确认新密码<input name="confirmPassword" type="password" autocomplete="new-password" minlength="8" required /></label><button type="submit">更新密码并继续</button></form>`);
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); }
});
}
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; });
}
function requireReauthentication() {
clearSession();
renderLogin();
pendingResolve = () => location.reload();
}
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,
requireReauthentication,
requireSession,
});
})();