mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
Add functionality to hide setup page; implement disable setup endpoint and storage management
This commit is contained in:
@@ -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
@@ -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) {
|
||||||
@@ -607,6 +671,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();
|
||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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()}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user