Add password-protected access flow

This commit is contained in:
zeer
2026-04-25 20:57:24 +08:00
parent 37907dd2f5
commit 7b8f4aae06
7 changed files with 275 additions and 4 deletions

View File

@@ -4,8 +4,14 @@ const state = {
selectedAccountId: null,
messages: [],
selectedMessageUid: null,
authenticated: false,
};
const appShell = document.getElementById('app-shell');
const loginScreen = document.getElementById('login-screen');
const loginForm = document.getElementById('login-form');
const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn');
const channelTree = document.getElementById('channel-tree');
const channelForm = document.getElementById('channel-form');
const accountForm = document.getElementById('account-form');
@@ -35,10 +41,12 @@ boot();
async function boot() {
bindEvents();
await reloadBaseData();
await restoreSession();
}
function bindEvents() {
loginForm.addEventListener('submit', onLogin);
logoutBtn.addEventListener('click', onLogout);
composeBtn.addEventListener('click', () => openModal(composeModal));
settingsBtn.addEventListener('click', () => openModal(settingsModal));
closeCompose.addEventListener('click', () => closeModal(composeModal));
@@ -56,6 +64,56 @@ function bindEvents() {
copyExportBtn.addEventListener('click', onCopyExportAccounts);
}
async function restoreSession() {
const result = await request('/api/auth/status', {}, false);
state.authenticated = Boolean(result.authenticated);
updateAuthView();
if (state.authenticated) {
await reloadBaseData();
}
}
function updateAuthView() {
loginScreen.classList.toggle('hidden', state.authenticated);
appShell.classList.toggle('hidden', !state.authenticated);
}
async function onLogin(event) {
event.preventDefault();
const payload = Object.fromEntries(new FormData(loginForm).entries());
loginBtn.disabled = true;
loginBtn.textContent = '验证中...';
try {
await request('/api/auth/login', {
method: 'POST',
body: JSON.stringify(payload),
}, false);
state.authenticated = true;
updateAuthView();
loginForm.reset();
await reloadBaseData();
showToast('登录成功');
} finally {
loginBtn.disabled = false;
loginBtn.textContent = '进入系统';
}
}
async function onLogout() {
await request('/api/auth/logout', { method: 'POST' }, false);
state.authenticated = false;
state.selectedAccountId = null;
state.selectedMessageUid = null;
state.messages = [];
syncSelectionState();
renderChannelTree();
renderMessages();
updateAuthView();
showToast('已退出登录');
}
function openModal(modal) {
modal.classList.remove('hidden');
}
@@ -441,8 +499,9 @@ async function copyText(value) {
document.body.removeChild(input);
}
async function request(url, options = {}) {
async function request(url, options = {}, useToast = true) {
const response = await fetch(url, {
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
...(options.headers || {}),
@@ -452,7 +511,9 @@ async function request(url, options = {}) {
const data = await response.json().catch(() => ({}));
if (!response.ok) {
showToast(data.error || '请求失败', true);
if (useToast) {
showToast(data.error || '请求失败', true);
}
throw new Error(data.error || 'Request failed');
}
return data;

View File

@@ -10,7 +10,29 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="app-container">
<div id="login-screen" class="login-screen hidden">
<div class="login-card">
<div class="login-brand">
<div class="logo">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M2 8L12 14L22 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span>Mail SR</span>
</div>
<p>请输入访问密码后进入系统</p>
</div>
<form id="login-form" class="login-form">
<div class="form-row compact">
<label for="login-password">访问密码</label>
<input id="login-password" name="password" type="password" placeholder="请输入访问密码" required>
</div>
<button type="submit" class="compose-btn" id="login-btn">进入系统</button>
</form>
</div>
</div>
<div id="app-shell" class="app-container hidden">
<!-- 左侧边栏 -->
<aside class="sidebar">
<div class="sidebar-header">
@@ -38,6 +60,13 @@
</div>
<div class="sidebar-footer">
<button id="logout-btn" class="icon-btn" title="退出登录">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
</button>
<button id="settings-btn" class="icon-btn" title="设置">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>

View File

@@ -37,6 +37,47 @@ body {
-webkit-font-smoothing: antialiased;
}
.hidden {
display: none !important;
}
.login-screen {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background:
radial-gradient(circle at top, rgba(59, 130, 246, 0.18), transparent 32%),
linear-gradient(180deg, #0b1220 0%, #0f172a 100%);
}
.login-card {
width: 100%;
max-width: 420px;
background: rgba(15, 23, 42, 0.92);
border: 1px solid var(--border);
border-radius: 20px;
box-shadow: var(--shadow-md), 0 24px 60px rgba(0, 0, 0, 0.35);
padding: 32px;
}
.login-brand {
margin-bottom: 22px;
}
.login-brand p {
margin-top: 12px;
color: var(--text-secondary);
font-size: 14px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 14px;
}
button,
input,
textarea,