feat: 删除原系统中https的登陆逻辑
This commit is contained in:
parent
e9b9cc91ef
commit
29e11f8877
|
|
@ -85,7 +85,6 @@ services:
|
||||||
# 挂载前端与服务端源码为只读,确保容器运行当前工作区代码(避免旧镜像残留)
|
# 挂载前端与服务端源码为只读,确保容器运行当前工作区代码(避免旧镜像残留)
|
||||||
- ./server/src:/app/server/src:ro
|
- ./server/src:/app/server/src:ro
|
||||||
- ./index.html:/app/index.html:ro
|
- ./index.html:/app/index.html:ro
|
||||||
- ./login.html:/app/login.html:ro
|
|
||||||
- ./admin:/app/admin:ro
|
- ./admin:/app/admin:ro
|
||||||
- ./views:/app/views:ro
|
- ./views:/app/views:ro
|
||||||
- ./js:/app/js:ro
|
- ./js:/app/js:ro
|
||||||
|
|
|
||||||
36
index.html
36
index.html
|
|
@ -1440,42 +1440,6 @@
|
||||||
<script src="js/storage/storage.js"></script>
|
<script src="js/storage/storage.js"></script>
|
||||||
<!-- 先初始化存储适配器(提供 isFrontendMode 标记) -->
|
<!-- 先初始化存储适配器(提供 isFrontendMode 标记) -->
|
||||||
<script src="js/storage/storage-adapter.js"></script>
|
<script src="js/storage/storage-adapter.js"></script>
|
||||||
<script src="js/boot/backend-gate.js"></script>
|
|
||||||
<!-- 后端模式访问信任门禁:后端模式且未登录时,跳转到登录页 -->
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
function gateIfBackend() {
|
|
||||||
try {
|
|
||||||
if (window.DEPLOYMENT_MODE === 'backend') {
|
|
||||||
var token = localStorage.getItem('auth_token');
|
|
||||||
if (!token) {
|
|
||||||
// 允许 ?mode=frontend 显式绕过(用于演示/调试)
|
|
||||||
var m = (new URLSearchParams(window.location.search).get('mode') || '').toLowerCase();
|
|
||||||
if (m === 'frontend') return;
|
|
||||||
var redirect = encodeURIComponent(window.location.href);
|
|
||||||
window.location.replace('/login.html?redirect=' + redirect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 若显式强制 backend,则立即校验;否则等待自动探测事件
|
|
||||||
try {
|
|
||||||
var forced = (new URLSearchParams(window.location.search).get('mode') || '').toLowerCase();
|
|
||||||
var envMode = (window.ENV_DEPLOYMENT_MODE || '').toLowerCase();
|
|
||||||
if (forced === 'backend' || envMode === 'backend') {
|
|
||||||
gateIfBackend();
|
|
||||||
}
|
|
||||||
} catch(_) {}
|
|
||||||
|
|
||||||
// 监听存储模式自动切换事件
|
|
||||||
window.addEventListener('pb:storage-mode-changed', function(evt) {
|
|
||||||
if (evt && evt.detail && evt.detail.mode === 'backend') {
|
|
||||||
gateIfBackend();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<!-- 再加载术语库存储(会依据适配器模式决定是否探测后端) -->
|
<!-- 再加载术语库存储(会依据适配器模式决定是否探测后端) -->
|
||||||
<script src="js/storage/glossary-storage.js"></script>
|
<script src="js/storage/glossary-storage.js"></script>
|
||||||
<script src="https://gcore.jsdelivr.net/npm/mammoth@1.4.21/mammoth.browser.min.js"></script>
|
<script src="https://gcore.jsdelivr.net/npm/mammoth@1.4.21/mammoth.browser.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
// backend-gate.js — 在“后端模式”未登录时,拦截并跳转到登录页
|
|
||||||
// 适用范围:主站/落地页/其他静态页面(不影响 /admin 管理台)
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
try {
|
|
||||||
var loc = window.location;
|
|
||||||
var path = loc.pathname || '';
|
|
||||||
|
|
||||||
// 纯本地文件访问(file://)时,强制视为前端模式:不做任何后端探测或跳转
|
|
||||||
if (loc.protocol === 'file:') return;
|
|
||||||
|
|
||||||
// 排除管理台与登录页自身
|
|
||||||
if (path.startsWith('/admin') || path.endsWith('/login.html')) return;
|
|
||||||
|
|
||||||
function q(key) {
|
|
||||||
try { return new URLSearchParams(loc.search).get(key); } catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRedirectParam(urlObj) {
|
|
||||||
try {
|
|
||||||
var red = urlObj.searchParams.get('redirect');
|
|
||||||
if (!red) return;
|
|
||||||
function deepDecode(s, limit) {
|
|
||||||
var i = 0, prev = s;
|
|
||||||
while (i++ < (limit || 8)) {
|
|
||||||
try {
|
|
||||||
var next = decodeURIComponent(prev);
|
|
||||||
if (next === prev) break;
|
|
||||||
prev = next;
|
|
||||||
} catch { break; }
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
var decoded = deepDecode(red, 8);
|
|
||||||
// 连续嵌套或超长,直接归一为首页
|
|
||||||
if (red.length > 512 || decoded.length > 512 || decoded.indexOf('/login.html?redirect=') !== -1) {
|
|
||||||
urlObj.searchParams.set('redirect', '/');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var target = null;
|
|
||||||
try { target = new URL(decoded, urlObj.origin); } catch {}
|
|
||||||
if (!target || target.origin !== urlObj.origin || target.pathname.endsWith('/login.html')) {
|
|
||||||
urlObj.searchParams.set('redirect', '/');
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function modeForced() {
|
|
||||||
var m = (q('mode') || '').toLowerCase();
|
|
||||||
if (m === 'backend') return true;
|
|
||||||
var env = (window.ENV_DEPLOYMENT_MODE || '').toLowerCase();
|
|
||||||
return env === 'backend';
|
|
||||||
}
|
|
||||||
|
|
||||||
function modeFrontendForced() {
|
|
||||||
var m = (q('mode') || '').toLowerCase();
|
|
||||||
return m === 'frontend';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasToken() {
|
|
||||||
try { return !!localStorage.getItem('auth_token'); } catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSafeRedirectTarget() {
|
|
||||||
try {
|
|
||||||
var u = new URL(window.location.href);
|
|
||||||
// 避免递归嵌套:去除已有的 redirect 参数
|
|
||||||
u.searchParams.delete('redirect');
|
|
||||||
// 若当前已是登录页,则回首页
|
|
||||||
if (u.pathname.endsWith('/login.html')) {
|
|
||||||
u.pathname = '/';
|
|
||||||
u.search = '';
|
|
||||||
}
|
|
||||||
// 限制同源
|
|
||||||
if (u.origin !== window.location.origin) return '/';
|
|
||||||
return u.toString();
|
|
||||||
} catch { return '/'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectToLogin() {
|
|
||||||
// 如果当前 URL 已包含 redirect 指向 login.html(异常嵌套),则强制回首页
|
|
||||||
try {
|
|
||||||
var current = new URL(window.location.href);
|
|
||||||
normalizeRedirectParam(current);
|
|
||||||
// 将标准化后的当前 URL 写回,避免下次读取到异常 redirect
|
|
||||||
history.replaceState(null, '', current.toString());
|
|
||||||
} catch {}
|
|
||||||
var safe = encodeURIComponent(buildSafeRedirectTarget());
|
|
||||||
window.location.replace('/login.html?redirect=' + safe);
|
|
||||||
}
|
|
||||||
|
|
||||||
function gateIfBackendKnown() {
|
|
||||||
// 若明确为后端模式,且未登录且未显式前端绕过 → 跳转登录
|
|
||||||
if (modeFrontendForced()) return; // 允许 ?mode=frontend 绕过
|
|
||||||
if (hasToken()) return;
|
|
||||||
redirectToLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function healthCheck(timeoutMs) {
|
|
||||||
try {
|
|
||||||
var ctrl = new AbortController();
|
|
||||||
var id = setTimeout(function(){ try{ctrl.abort();}catch{} }, timeoutMs);
|
|
||||||
var base = window.ENV_API_BASE_URL || '/api';
|
|
||||||
var res = await fetch(base + '/health', { signal: ctrl.signal, cache: 'no-store' });
|
|
||||||
clearTimeout(id);
|
|
||||||
return !!(res && res.ok);
|
|
||||||
} catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 情况 1:已强制后端 → 立即门禁
|
|
||||||
if (modeForced()) {
|
|
||||||
gateIfBackendKnown();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 情况 2:依赖 storage-adapter 的自动切换事件(若其已加载)
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('pb:storage-mode-changed', function (evt) {
|
|
||||||
if (evt && evt.detail && evt.detail.mode === 'backend') gateIfBackendKnown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 情况 3:页面未加载 storage-adapter(例如落地页)→ 自行做一次短健康检查
|
|
||||||
// 仅当未显式前端绕过时执行
|
|
||||||
if (!modeFrontendForced()) {
|
|
||||||
healthCheck(700).then(function (hasBackend) {
|
|
||||||
if (hasBackend) gateIfBackendKnown();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略所有门禁过程中的异常,避免影响前端模式体验
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -82,9 +82,8 @@ class BackendStorage {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// Token 过期,需要重新登录
|
// Token 过期,清除token
|
||||||
AuthManager.removeToken();
|
AuthManager.removeToken();
|
||||||
window.location.href = '/login.html';
|
|
||||||
}
|
}
|
||||||
throw new Error(`API Error: ${response.status}`);
|
throw new Error(`API Error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
132
login.html
132
login.html
|
|
@ -1,132 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>登录 - Paper Burner X</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script src="https://gcore.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
|
|
||||||
<script>
|
|
||||||
// 防止登录页 redirect 参数递归叠加/循环
|
|
||||||
(function () {
|
|
||||||
try {
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
var red = url.searchParams.get('redirect');
|
|
||||||
if (!red) return;
|
|
||||||
// 多层解码,防止 %25 级联编码导致的嵌套绕过
|
|
||||||
function deepDecode(s, limit) {
|
|
||||||
var i = 0, prev = s;
|
|
||||||
while (i++ < (limit || 8)) {
|
|
||||||
try {
|
|
||||||
var next = decodeURIComponent(prev);
|
|
||||||
if (next === prev) break;
|
|
||||||
prev = next;
|
|
||||||
} catch { break; }
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
var decoded = deepDecode(red, 8);
|
|
||||||
// 规则:
|
|
||||||
// 1) 任意层包含 login.html?redirect= 视为递归 → 归一 '/'
|
|
||||||
// 2) 过长(>512)视为异常 → 归一 '/'
|
|
||||||
var shouldClamp = (red.length > 512) || (decoded.length > 512) || (decoded.indexOf('/login.html?redirect=') !== -1);
|
|
||||||
if (shouldClamp) {
|
|
||||||
url.searchParams.set('redirect', '/');
|
|
||||||
history.replaceState(null, '', url.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 仅允许同源,且目标不能是登录页自身
|
|
||||||
var target;
|
|
||||||
try { target = new URL(decoded, window.location.origin); } catch { target = null; }
|
|
||||||
if (!target || target.origin !== window.location.origin || target.pathname.endsWith('/login.html')) {
|
|
||||||
url.searchParams.set('redirect', '/');
|
|
||||||
history.replaceState(null, '', url.toString());
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<div class="bg-white p-8 rounded-lg shadow w-full max-w-md">
|
|
||||||
<h1 class="text-2xl font-bold mb-6 text-center">登录到 Paper Burner X</h1>
|
|
||||||
<form id="loginForm" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">邮箱</label>
|
|
||||||
<input id="email" type="email" required class="mt-1 block w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="you@example.com" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">密码</label>
|
|
||||||
<input id="password" type="password" required class="mt-1 block w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="••••••••" />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700">登录</button>
|
|
||||||
<p id="error" class="text-red-600 text-sm hidden"></p>
|
|
||||||
</form>
|
|
||||||
<div class="text-sm text-gray-500 mt-4">
|
|
||||||
<p>默认管理员:<code>admin@paperburner.local / admin123456</code></p>
|
|
||||||
<p class="mt-1">管理员入口:<a href="/admin" class="text-blue-600 hover:underline">/admin</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const API_BASE = window.location.origin + '/api';
|
|
||||||
|
|
||||||
function getSafeRedirect() {
|
|
||||||
try {
|
|
||||||
const p = new URLSearchParams(window.location.search);
|
|
||||||
const raw = p.get('redirect');
|
|
||||||
if (!raw) return '/';
|
|
||||||
// 多层解码,长度限制
|
|
||||||
function deepDecode(s, limit) {
|
|
||||||
let i = 0, prev = s;
|
|
||||||
while (i++ < (limit || 8)) {
|
|
||||||
try {
|
|
||||||
const next = decodeURIComponent(prev);
|
|
||||||
if (next === prev) break;
|
|
||||||
prev = next;
|
|
||||||
} catch { break; }
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
const decoded = deepDecode(raw, 8);
|
|
||||||
if (decoded.length > 512 || decoded.indexOf('/login.html?redirect=') !== -1) return '/';
|
|
||||||
const u = new URL(decoded, window.location.origin);
|
|
||||||
// 仅允许同源回跳,且不指向 login.html 防止循环
|
|
||||||
if (u.origin !== window.location.origin) return '/';
|
|
||||||
if (u.pathname.endsWith('/login.html')) return '/';
|
|
||||||
u.searchParams.delete('redirect');
|
|
||||||
return u.toString();
|
|
||||||
} catch { return '/'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRedirect() {
|
|
||||||
try {
|
|
||||||
const p = new URLSearchParams(window.location.search);
|
|
||||||
return p.get('redirect') || '/';
|
|
||||||
} catch { return '/'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const email = document.getElementById('email').value.trim();
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
const err = document.getElementById('error');
|
|
||||||
err.classList.add('hidden');
|
|
||||||
err.textContent = '';
|
|
||||||
try {
|
|
||||||
const res = await axios.post(`${API_BASE}/auth/login`, { email, password });
|
|
||||||
if (res.data && res.data.token) {
|
|
||||||
// 与前端适配器一致的存储键
|
|
||||||
localStorage.setItem('auth_token', res.data.token);
|
|
||||||
window.location.href = getSafeRedirect();
|
|
||||||
} else {
|
|
||||||
throw new Error('登录响应异常');
|
|
||||||
}
|
|
||||||
} catch (e2) {
|
|
||||||
err.textContent = e2?.response?.data?.error || e2.message || '登录失败';
|
|
||||||
err.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
|
@ -9,9 +9,6 @@ async function main() {
|
||||||
const admin = await request(app).get('/admin');
|
const admin = await request(app).get('/admin');
|
||||||
console.log('ADMIN', admin.status, admin.headers['content-type']);
|
console.log('ADMIN', admin.status, admin.headers['content-type']);
|
||||||
|
|
||||||
const login = await request(app).get('/login.html');
|
|
||||||
console.log('LOGIN', login.status, login.headers['content-type']);
|
|
||||||
|
|
||||||
if (health.status !== 200) {
|
if (health.status !== 200) {
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -344,11 +344,6 @@ app.use('/api/prompt-pool', promptPoolRoutes);
|
||||||
|
|
||||||
// ==================== 前端路由(SPA) ====================
|
// ==================== 前端路由(SPA) ====================
|
||||||
|
|
||||||
// 登录页需显式返回 login.html,避免被通配符 * 误回退到 index.html
|
|
||||||
app.get('/login.html', (req, res) => {
|
|
||||||
res.sendFile(join(rootPath, 'login.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 管理员面板
|
// 管理员面板
|
||||||
app.get('/admin*', (req, res) => {
|
app.get('/admin*', (req, res) => {
|
||||||
res.sendFile(join(rootPath, 'admin/index.html'));
|
res.sendFile(join(rootPath, 'admin/index.html'));
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@
|
||||||
<script src="https://gcore.jsdelivr.net/npm/iconify-icon@2.0.0/dist/iconify-icon.min.js"></script>
|
<script src="https://gcore.jsdelivr.net/npm/iconify-icon@2.0.0/dist/iconify-icon.min.js"></script>
|
||||||
<!-- GitHub Stars 统一获取模块 -->
|
<!-- GitHub Stars 统一获取模块 -->
|
||||||
<script src="../../js/utils/github-stars.js"></script>
|
<script src="../../js/utils/github-stars.js"></script>
|
||||||
<!-- 后端模式未登录门禁(在有后端部署时,直接要求登录) -->
|
|
||||||
<script src="/js/boot/backend-gate.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
/* 谷歌字体 Inter (更专业、现代) */
|
/* 谷歌字体 Inter (更专业、现代) */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue