Add functionality to hide setup page; implement disable setup endpoint and storage management

This commit is contained in:
shuaiplus
2026-02-06 01:12:01 +08:00
parent ef50f44a4e
commit 91800f41c5
4 changed files with 121 additions and 2 deletions
+1
View File
@@ -4,6 +4,7 @@ node_modules/
# Wrangler # Wrangler
.wrangler/ .wrangler/
.dev.vars .dev.vars
RELEASE_NOTES.md
# Build output # Build output
dist/ dist/
+87 -1
View File
@@ -329,6 +329,14 @@ const setupPageHTML = `<!DOCTYPE html>
If you forget it, you must redeploy and register again. If you forget it, you must redeploy and register again.
</p> </p>
</div> </div>
<div class="kv">
<h3 id="t_hide_title">Hide setup page</h3>
<p id="t_hide_desc">After hiding, this setup page will return 404 for everyone. Your vault will keep working.</p>
<div class="actions">
<button type="button" id="hideBtn" class="primary" onclick="disableSetupPage()">Hide setup page</button>
</div>
</div>
</div> </div>
<div class="footer"> <div class="footer">
@@ -345,6 +353,7 @@ const setupPageHTML = `<!DOCTYPE html>
<script> <script>
const AUTHOR = { name: 'shuaiplus', website: 'https://shuai.plus', github: 'https://github.com/shuaiplus/nodewarden' }; const AUTHOR = { name: 'shuaiplus', website: 'https://shuai.plus', github: 'https://github.com/shuaiplus/nodewarden' };
let isRegistered = false;
function isChinese() { function isChinese() {
const lang = (navigator.language || '').toLowerCase(); const lang = (navigator.language || '').toLowerCase();
@@ -369,6 +378,13 @@ const setupPageHTML = `<!DOCTYPE html>
doneDesc: '服务已就绪。在 Bitwarden 客户端中填入以下服务器地址:', doneDesc: '服务已就绪。在 Bitwarden 客户端中填入以下服务器地址:',
important: '重要提示', important: '重要提示',
limitations: '本项目仅支持单用户:不能添加新用户;不支持修改主密码;如果忘记主密码,只能重新部署并重新注册。', limitations: '本项目仅支持单用户:不能添加新用户;不支持修改主密码;如果忘记主密码,只能重新部署并重新注册。',
hideTitle: '隐藏初始化页',
hideDesc: '隐藏后,初始化页对任何人都会直接返回 404。你的密码库仍可正常使用。',
hideBtn: '隐藏初始化页',
hideWorking: '正在隐藏…',
hideDone: '已隐藏,此页面将返回 404。',
hideFailed: '隐藏失败',
hideConfirm: '确认隐藏初始化页?隐藏后页面将不可访问,但你的密码库不会受影响。',
errPwNotMatch: '两次输入的密码不一致', errPwNotMatch: '两次输入的密码不一致',
errPwTooShort: '密码长度至少 12 位', errPwTooShort: '密码长度至少 12 位',
errGeneric: '发生错误:', errGeneric: '发生错误:',
@@ -391,6 +407,13 @@ const setupPageHTML = `<!DOCTYPE html>
doneDesc: 'Your server is ready. Configure your Bitwarden client with this server URL:', doneDesc: 'Your server is ready. Configure your Bitwarden client with this server URL:',
important: 'Important', important: 'Important',
limitations: 'Single user only: you cannot add new users. Changing the master password is not supported. If you forget it, redeploy and register again.', limitations: 'Single user only: you cannot add new users. Changing the master password is not supported. If you forget it, redeploy and register again.',
hideTitle: 'Hide setup page',
hideDesc: 'After hiding, this setup page will return 404 for everyone. Your vault will keep working.',
hideBtn: 'Hide setup page',
hideWorking: 'Hiding…',
hideDone: 'Hidden. This page will now return 404.',
hideFailed: 'Failed to hide setup page',
hideConfirm: 'Hide the setup page? It will no longer be accessible, but your vault will keep working.',
errPwNotMatch: 'Passwords do not match', errPwNotMatch: 'Passwords do not match',
errPwTooShort: 'Password must be at least 12 characters', errPwTooShort: 'Password must be at least 12 characters',
errGeneric: 'An error occurred: ', errGeneric: 'An error occurred: ',
@@ -419,6 +442,9 @@ const setupPageHTML = `<!DOCTYPE html>
document.getElementById('t_done_desc').textContent = t('doneDesc'); document.getElementById('t_done_desc').textContent = t('doneDesc');
document.getElementById('t_important').textContent = t('important'); document.getElementById('t_important').textContent = t('important');
document.getElementById('t_limitations').textContent = t('limitations'); document.getElementById('t_limitations').textContent = t('limitations');
document.getElementById('t_hide_title').textContent = t('hideTitle');
document.getElementById('t_hide_desc').textContent = t('hideDesc');
document.getElementById('hideBtn').textContent = t('hideBtn');
} }
// Check if already registered // Check if already registered
@@ -426,6 +452,7 @@ const setupPageHTML = `<!DOCTYPE html>
try { try {
const res = await fetch('/setup/status'); const res = await fetch('/setup/status');
const data = await res.json(); const data = await res.json();
isRegistered = !!data.registered;
if (data.registered) { if (data.registered) {
showRegisteredView(); showRegisteredView();
} }
@@ -435,10 +462,47 @@ const setupPageHTML = `<!DOCTYPE html>
} }
function showRegisteredView() { function showRegisteredView() {
isRegistered = true;
document.getElementById('setup-form').style.display = 'none'; document.getElementById('setup-form').style.display = 'none';
document.getElementById('registered-view').style.display = 'block'; document.getElementById('registered-view').style.display = 'block';
document.getElementById('serverUrl').textContent = window.location.origin; document.getElementById('serverUrl').textContent = window.location.origin;
showMessage(t('doneTitle'), 'success'); showMessage(t('doneTitle'), 'success');
const form = document.getElementById('form');
if (form) {
const fields = form.querySelectorAll('input, button');
fields.forEach((el) => {
el.disabled = true;
});
}
}
async function disableSetupPage() {
if (!isRegistered) return;
if (!confirm(t('hideConfirm'))) return;
const btn = document.getElementById('hideBtn');
if (btn) {
btn.disabled = true;
btn.textContent = t('hideWorking');
}
try {
const res = await fetch('/setup/disable', { method: 'POST' });
const data = await res.json();
if (res.ok && data.success) {
showMessage(t('hideDone'), 'success');
setTimeout(() => window.location.reload(), 600);
return;
}
showMessage(data.error || t('hideFailed'), 'error');
} catch (e) {
showMessage(t('hideFailed'), 'error');
}
if (btn) {
btn.disabled = false;
btn.textContent = t('hideBtn');
}
} }
function showMessage(text, type) { function showMessage(text, type) {
@@ -606,6 +670,11 @@ const setupPageHTML = `<!DOCTYPE html>
async function handleSubmit(event) { async function handleSubmit(event) {
event.preventDefault(); event.preventDefault();
if (isRegistered) {
showMessage(t('doneTitle'), 'success');
return;
}
const name = document.getElementById('name').value; const name = document.getElementById('name').value;
const email = document.getElementById('email').value.toLowerCase(); const email = document.getElementById('email').value.toLowerCase();
@@ -698,6 +767,11 @@ const setupPageHTML = `<!DOCTYPE html>
// GET / - Setup page // GET / - Setup page
export async function handleSetupPage(request: Request, env: Env): Promise<Response> { export async function handleSetupPage(request: Request, env: Env): Promise<Response> {
const storage = new StorageService(env.VAULT);
const disabled = await storage.isSetupDisabled();
if (disabled) {
return new Response(null, { status: 404 });
}
return htmlResponse(setupPageHTML); return htmlResponse(setupPageHTML);
} }
@@ -705,5 +779,17 @@ export async function handleSetupPage(request: Request, env: Env): Promise<Respo
export async function handleSetupStatus(request: Request, env: Env): Promise<Response> { export async function handleSetupStatus(request: Request, env: Env): Promise<Response> {
const storage = new StorageService(env.VAULT); const storage = new StorageService(env.VAULT);
const registered = await storage.isRegistered(); const registered = await storage.isRegistered();
return jsonResponse({ registered }); const disabled = await storage.isSetupDisabled();
return jsonResponse({ registered, disabled });
}
// POST /setup/disable
export async function handleDisableSetup(request: Request, env: Env): Promise<Response> {
const storage = new StorageService(env.VAULT);
const registered = await storage.isRegistered();
if (!registered) {
return errorResponse('Registration required', 403);
}
await storage.setSetupDisabled();
return jsonResponse({ success: true });
} }
+22 -1
View File
@@ -35,7 +35,7 @@ import {
import { handleSync } from './handlers/sync'; import { handleSync } from './handlers/sync';
// Setup handlers // Setup handlers
import { handleSetupPage, handleSetupStatus } from './handlers/setup'; import { handleSetupPage, handleSetupStatus, handleDisableSetup } from './handlers/setup';
// Import handler // Import handler
import { handleCiphersImport } from './handlers/import'; import { handleCiphersImport } from './handlers/import';
@@ -99,6 +99,11 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
return handleSetupStatus(request, env); return handleSetupStatus(request, env);
} }
// Disable setup page (one-way)
if (path === '/setup/disable' && method === 'POST') {
return handleDisableSetup(request, env);
}
// Favicon - return empty // Favicon - return empty
if (path === '/favicon.ico') { if (path === '/favicon.ico') {
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
@@ -209,6 +214,22 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
// Increment rate limit counter // Increment rate limit counter
await rateLimit.incrementApiCount(userId + ':' + clientId); await rateLimit.incrementApiCount(userId + ':' + clientId);
// Block account operations that could change password or delete user
if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
const blockedAccountPaths = new Set([
'/api/accounts/password',
'/api/accounts/change-password',
'/api/accounts/set-password',
'/api/accounts/master-password',
'/api/accounts/delete',
'/api/accounts/delete-account',
'/api/accounts/delete-vault',
]);
if (blockedAccountPaths.has(path)) {
return errorResponse('This operation is disabled', 403);
}
}
// Account endpoints // Account endpoints
if (path === '/api/accounts/profile') { if (path === '/api/accounts/profile') {
if (method === 'GET') return handleGetProfile(request, env, userId); if (method === 'GET') return handleGetProfile(request, env, userId);
+11
View File
@@ -2,6 +2,7 @@ import { Env, User, Cipher, Folder, Attachment } from '../types';
const KEYS = { const KEYS = {
CONFIG_REGISTERED: 'config:registered', CONFIG_REGISTERED: 'config:registered',
CONFIG_SETUP_DISABLED: 'config:setup_disabled',
USER_PREFIX: 'user:', USER_PREFIX: 'user:',
CIPHER_PREFIX: 'cipher:', CIPHER_PREFIX: 'cipher:',
FOLDER_PREFIX: 'folder:', FOLDER_PREFIX: 'folder:',
@@ -26,6 +27,16 @@ export class StorageService {
await this.kv.put(KEYS.CONFIG_REGISTERED, 'true'); await this.kv.put(KEYS.CONFIG_REGISTERED, 'true');
} }
// Setup page visibility
async isSetupDisabled(): Promise<boolean> {
const value = await this.kv.get(KEYS.CONFIG_SETUP_DISABLED);
return value === 'true';
}
async setSetupDisabled(): Promise<void> {
await this.kv.put(KEYS.CONFIG_SETUP_DISABLED, 'true');
}
// User operations // User operations
async getUser(email: string): Promise<User | null> { async getUser(email: string): Promise<User | null> {
const data = await this.kv.get(`${KEYS.USER_PREFIX}${email.toLowerCase()}`); const data = await this.kv.get(`${KEYS.USER_PREFIX}${email.toLowerCase()}`);