paper-burner/login.html

133 lines
5.3 KiB
HTML
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.

<!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>