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/
.dev.vars
RELEASE_NOTES.md
# Build output
dist/
+87 -1
View File
@@ -329,6 +329,14 @@ const setupPageHTML = `<!DOCTYPE html>
If you forget it, you must redeploy and register again.
</p>
</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 class="footer">
@@ -345,6 +353,7 @@ const setupPageHTML = `<!DOCTYPE html>
<script>
const AUTHOR = { name: 'shuaiplus', website: 'https://shuai.plus', github: 'https://github.com/shuaiplus/nodewarden' };
let isRegistered = false;
function isChinese() {
const lang = (navigator.language || '').toLowerCase();
@@ -369,6 +378,13 @@ const setupPageHTML = `<!DOCTYPE html>
doneDesc: '服务已就绪。在 Bitwarden 客户端中填入以下服务器地址:',
important: '重要提示',
limitations: '本项目仅支持单用户:不能添加新用户;不支持修改主密码;如果忘记主密码,只能重新部署并重新注册。',
hideTitle: '隐藏初始化页',
hideDesc: '隐藏后,初始化页对任何人都会直接返回 404。你的密码库仍可正常使用。',
hideBtn: '隐藏初始化页',
hideWorking: '正在隐藏…',
hideDone: '已隐藏,此页面将返回 404。',
hideFailed: '隐藏失败',
hideConfirm: '确认隐藏初始化页?隐藏后页面将不可访问,但你的密码库不会受影响。',
errPwNotMatch: '两次输入的密码不一致',
errPwTooShort: '密码长度至少 12 位',
errGeneric: '发生错误:',
@@ -391,6 +407,13 @@ const setupPageHTML = `<!DOCTYPE html>
doneDesc: 'Your server is ready. Configure your Bitwarden client with this server URL:',
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.',
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',
errPwTooShort: 'Password must be at least 12 characters',
errGeneric: 'An error occurred: ',
@@ -419,6 +442,9 @@ const setupPageHTML = `<!DOCTYPE html>
document.getElementById('t_done_desc').textContent = t('doneDesc');
document.getElementById('t_important').textContent = t('important');
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
@@ -426,6 +452,7 @@ const setupPageHTML = `<!DOCTYPE html>
try {
const res = await fetch('/setup/status');
const data = await res.json();
isRegistered = !!data.registered;
if (data.registered) {
showRegisteredView();
}
@@ -435,10 +462,47 @@ const setupPageHTML = `<!DOCTYPE html>
}
function showRegisteredView() {
isRegistered = true;
document.getElementById('setup-form').style.display = 'none';
document.getElementById('registered-view').style.display = 'block';
document.getElementById('serverUrl').textContent = window.location.origin;
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) {
@@ -607,6 +671,11 @@ const setupPageHTML = `<!DOCTYPE html>
async function handleSubmit(event) {
event.preventDefault();
if (isRegistered) {
showMessage(t('doneTitle'), 'success');
return;
}
const name = document.getElementById('name').value;
const email = document.getElementById('email').value.toLowerCase();
const password = document.getElementById('password').value;
@@ -698,6 +767,11 @@ const setupPageHTML = `<!DOCTYPE html>
// GET / - Setup page
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);
}
@@ -705,5 +779,17 @@ export async function handleSetupPage(request: Request, env: Env): Promise<Respo
export async function handleSetupStatus(request: Request, env: Env): Promise<Response> {
const storage = new StorageService(env.VAULT);
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';
// Setup handlers
import { handleSetupPage, handleSetupStatus } from './handlers/setup';
import { handleSetupPage, handleSetupStatus, handleDisableSetup } from './handlers/setup';
// Import handler
import { handleCiphersImport } from './handlers/import';
@@ -99,6 +99,11 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
return handleSetupStatus(request, env);
}
// Disable setup page (one-way)
if (path === '/setup/disable' && method === 'POST') {
return handleDisableSetup(request, env);
}
// Favicon - return empty
if (path === '/favicon.ico') {
return new Response(null, { status: 204 });
@@ -209,6 +214,22 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
// Increment rate limit counter
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
if (path === '/api/accounts/profile') {
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 = {
CONFIG_REGISTERED: 'config:registered',
CONFIG_SETUP_DISABLED: 'config:setup_disabled',
USER_PREFIX: 'user:',
CIPHER_PREFIX: 'cipher:',
FOLDER_PREFIX: 'folder:',
@@ -26,6 +27,16 @@ export class StorageService {
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
async getUser(email: string): Promise<User | null> {
const data = await this.kv.get(`${KEYS.USER_PREFIX}${email.toLowerCase()}`);