登录页去掉订阅口令:订阅接口认后台会话 + editor-api 反代

- Worker 侧 requireAuth 新增两条通道:① Authorization 管理员会话(CF 版后台
  登录后订阅模块免口令);② X-Editor-Token 共享令牌(服务端到服务端)。
  原 ?token=/Cookie 通道保留,RSS 阅读器与友圈页不受影响
- editor-api 新增订阅反代(/api/feeds|links|link-apply):国内机后台的
  订阅源/友链由本机带共享令牌去 CF 取,前端不再持有任何口令;GET 幂等重试
  抗跨境抖动
- 登录页只留账号+密码(CF 版与国内机版统一);国内机后台侧栏新增订阅中心
  入口;RSS_TOKEN / LS_RSS 前端残留全部清理
This commit is contained in:
zqlit committed 2026-10-04 23:57:02 +08:00
1 parent a11631faa6
commit 18a4882577
4 files changed
+114 -51

No files matched your search

+19 -41
View File
@@ -13,7 +13,6 @@ const API = location.origin + '/api/v2';
const RSS_API = location.origin;
const LS_T = 'atk.token';
const LS_U = 'atk.user';
const LS_RSS = 'atk.rss.token'; // 订阅口令
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.prototype.slice.call(r.querySelectorAll(s));
@@ -160,18 +159,16 @@ async function api(path, opt) {
return data;
}
// 订阅后端请求:URL 带 ?token=<订阅口令>
let RSS_TOKEN = localStorage.getItem(LS_RSS) || '';
// 订阅后端请求。凭据不再单独让用户输 —— 见下方注释。
async function rssApi(path, opt) {
opt = opt || {};
const u = new URL(RSS_API + path);
// 调用方显式带了 token 就用它,否则补本地记住的口令。
// (以前无条件 set 会覆盖调用方传的值,导致「刚输入的口令被旧口令盖掉」)
if (!u.searchParams.get('token') && RSS_TOKEN) u.searchParams.set('token', RSS_TOKEN);
const headers = {};
if (opt.body !== undefined) headers['Content-Type'] = 'application/json';
// Cloudflare 版:带后台会话,订阅接口的 requireAuth 直接认(不再要「订阅口令」)
// 国内机版:TOKEN 为空,请求由本机 editor-api 反代时注入共享令牌
if (TOKEN) headers['Authorization'] = 'Bearer ' + TOKEN;
let res;
try {
@@ -191,7 +188,7 @@ async function rssApi(path, opt) {
if (!res.ok) {
if (res.status === 401 || res.status === 403) {
const e = new Error((data && (data.error || data.msg)) || '订阅口令未授权');
const e = new Error((data && (data.error || data.msg)) || '订阅模块未授权');
e.status = res.status;
throw e;
}
@@ -239,9 +236,6 @@ function showGate() {
$('#app').hidden = true;
$('#gate').hidden = false;
// 国内机模式下登录的是本机 Cookie 会话,订阅口令(走 Cloudflare)在这里没意义
const rssField = $('#lgRssField');
if (rssField) rssField.hidden = !!state.direct;
const foot = $('#lgFoot');
if (foot) {
foot.textContent = state.direct
@@ -317,12 +311,18 @@ async function directLogin(user, password) {
// ============================================================ 骨架
function renderTabs() {
// 直连模式只放「文章编辑」——其它模块(评论/设置/订阅)必须走 Cloudflare 的 D1,
// 在国内访问那一跳很慢,单独留一个入口按需登录。
// 国内机模式:文章编辑走本机直连(不出境);订阅源/友链由本机后端反代到
// Cloudflare 的订阅服务(带了共享令牌,前端不用口令)。评论/设置这些还
// 必须走 Cloudflare 会话(D1 数据在国内访问那一跳很慢),单独留个入口。
if (state.direct) {
const t = (id, label) =>
'<button class="tab' + (state.tab === id ? ' on' : '') + '" data-act="tab" data-id="' + id + '">' +
(ICONS[id] || '') + '<span>' + label + '</span></button>';
$('#tabs').innerHTML =
'<button class="tab' + (state.tab === 'posts' ? ' on' : '') + '" data-act="tab" data-id="posts">'
+ (ICONS.posts || '') + '<span>文章编辑</span></button>' +
t('posts', '文章编辑') +
'<div class="nav-sep"></div>' +
'<div class="nav-group">订阅中心</div>' +
t('feeds', '订阅源') + t('links', '友链') +
'<div class="nav-sep"></div>' +
'<div class="nav-group">需要 Cloudflare</div>' +
'<button class="tab" data-act="relogin">' + I('<path d="M12 3v9"/><path d="M7 7.5a7 7 0 1 0 10 0"/>') + '<span>登录完整后台</span></button>';
@@ -931,8 +931,8 @@ async function viewFeeds() {
data = await rssApi('/api/feeds');
} catch (e) {
if (e.status === 401 || e.status === 403) {
$('#view').innerHTML = head('订阅源管理', 'FEEDS', '需要订阅口令') +
'<div class="empty">未配置订阅口令 —— 请退出后重新登录,并在登录页填写「订阅口令」。</div>';
$('#view').innerHTML = head('订阅源管理', 'FEEDS', '未授权') +
'<div class="empty">登录状态已失效 —— 请退出后重新登录后台。</div>';
return;
}
$('#view').innerHTML += '<div class="empty">加载失败:' + esc(e.message) + '</div>';
@@ -1008,8 +1008,8 @@ async function viewLinks() {
} catch (e) { /* 申请列表加载失败不阻塞友链展示 */ }
} catch (e) {
if (e.status === 401 || e.status === 403) {
$('#view').innerHTML = head('友链管理', 'LINKS', '需要订阅口令') +
'<div class="empty">未配置订阅口令 —— 请退出后重新登录。</div>';
$('#view').innerHTML = head('友链管理', 'LINKS', '未授权') +
'<div class="empty">登录状态已失效 —— 请退出后重新登录后台。</div>';
return;
}
$('#view').innerHTML += '<div class="empty">加载失败:' + esc(e.message) + '</div>';
@@ -1986,28 +1986,6 @@ $('#loginForm').addEventListener('submit', async (e) => {
state.me = state.me || {};
if (state.me.is_admin === false) throw new Error('该账号不是管理员');
// 订阅口令是**可选**的:填了就验证,错了只影响订阅模块,绝不阻断登录
const rssPwd = $('#lgRss') ? $('#lgRss').value.trim() : '';
if (rssPwd) {
// 直接验证刚输入的口令(不走 rssApi,避免被本地记住的旧口令覆盖)
let rssOk = false;
try {
const r = await fetch(RSS_API + '/api/auth?token=' + encodeURIComponent(rssPwd));
const d = await r.json().catch(() => null);
rssOk = !!(d && d.ok === true);
} catch (e) { rssOk = false; }
if (rssOk) {
RSS_TOKEN = rssPwd;
localStorage.setItem(LS_RSS, rssPwd);
} else {
RSS_TOKEN = '';
localStorage.removeItem(LS_RSS);
setTimeout(() => toast('订阅口令不正确,已跳过订阅模块(不影响其它功能)', true), 400);
}
} else {
RSS_TOKEN = localStorage.getItem(LS_RSS) || '';
}
await bootstrap();
} catch (ex) {
err.textContent = '✕ ' + ex.message;