mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-04 22:40:11 +00:00
feat: add Web Vault visibility switch
This commit is contained in:
@@ -78,6 +78,8 @@
|
|||||||
|
|
||||||
- If the site reports a missing `JWT_SECRET`, add it as a **Secret** in Workers settings. In production use a random string of at least 32 characters; do not use temporary or example values.
|
- If the site reports a missing `JWT_SECRET`, add it as a **Secret** in Workers settings. In production use a random string of at least 32 characters; do not use temporary or example values.
|
||||||
|
|
||||||
|
- To hide the Web Vault, add a text variable named `HIDE_WEB_VAULT` with the value `1` under **Workers settings → Variables and Secrets**. While enabled, server-hosted frontend pages and static assets return `404 Not Found`, while the login, sync, attachment, icon, notification, and other server endpoints used by Bitwarden clients remain available; an already installed or cached PWA can continue using its local frontend. Delete the variable (or change it to anything other than `1`) to restore the server-hosted Web Vault.
|
||||||
|
|
||||||
- In this flow you hand code to Cloudflare to build and deploy. `wrangler.toml` or `wrangler.kv.toml` in the repo defines binding names; the Worker initializes the D1 schema on first request—no manual SQL upload.
|
- In this flow you hand code to Cloudflare to build and deploy. `wrangler.toml` or `wrangler.kv.toml` in the repo defines binding names; the Worker initializes the D1 schema on first request—no manual SQL upload.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,8 @@
|
|||||||
|
|
||||||
- 页面提示缺少 `JWT_SECRET` 时,到 Workers 设置里添加 Secret。正式环境至少使用 32 个字符以上的随机字符串,不要使用临时值或示例值。
|
- 页面提示缺少 `JWT_SECRET` 时,到 Workers 设置里添加 Secret。正式环境至少使用 32 个字符以上的随机字符串,不要使用临时值或示例值。
|
||||||
|
|
||||||
|
- 如需隐藏 Web Vault,在 Workers 的“设置 → 变量和机密”中添加文本变量 `HIDE_WEB_VAULT`,值设为 `1`。启用后,服务器上的前端页面和静态资源统一返回 `404 Not Found`,Bitwarden 客户端所需的登录、同步、附件、图标、通知等服务端接口仍可使用;已经安装或缓存的 PWA 可以继续使用本地前端。删除该变量(或将值改为非 `1`)即可恢复服务器上的 Web Vault。
|
||||||
|
|
||||||
- 这套流程里,用户实际做的是把代码交给 Cloudflare 构建并部署。代码里的 `wrangler.toml` 或 `wrangler.kv.toml` 决定绑定名,Worker 第一次处理请求时会自动初始化 D1 schema,不需要用户上传 SQL。
|
- 这套流程里,用户实际做的是把代码交给 Cloudflare 构建并部署。代码里的 `wrangler.toml` 或 `wrangler.kv.toml` 决定绑定名,Worker 第一次处理请求时会自动初始化 D1 schema,不需要用户上传 SQL。
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-17
@@ -5,6 +5,11 @@ import { handleRequest } from './router';
|
|||||||
import { StorageService } from './services/storage';
|
import { StorageService } from './services/storage';
|
||||||
import { applyCors, jsonResponse } from './utils/response';
|
import { applyCors, jsonResponse } from './utils/response';
|
||||||
import { runScheduledBackupIfDue } from './handlers/backup';
|
import { runScheduledBackupIfDue } from './handlers/backup';
|
||||||
|
import {
|
||||||
|
isBackendRequestPath,
|
||||||
|
isWebVaultHidden,
|
||||||
|
webVaultNotFoundResponse,
|
||||||
|
} from './web-vault-visibility';
|
||||||
|
|
||||||
let dbInitialized = false;
|
let dbInitialized = false;
|
||||||
let dbInitError: string | null = null;
|
let dbInitError: string | null = null;
|
||||||
@@ -19,22 +24,6 @@ function normalizeRequestUrl(request: Request): Request {
|
|||||||
return new Request(url.toString(), request);
|
return new Request(url.toString(), request);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWorkerHandledPath(path: string): boolean {
|
|
||||||
return (
|
|
||||||
path.startsWith('/api/') ||
|
|
||||||
path.startsWith('/identity/') ||
|
|
||||||
path.startsWith('/icons/') ||
|
|
||||||
path.startsWith('/fill-assist/') ||
|
|
||||||
path.startsWith('/notifications/') ||
|
|
||||||
path.startsWith('/.well-known/') ||
|
|
||||||
path === '/v1/assetlinks:check' ||
|
|
||||||
path === '/web-bootstrap' ||
|
|
||||||
path === '/config' ||
|
|
||||||
path === '/api/config' ||
|
|
||||||
path === '/api/version'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSearchIndexHeaders(request: Request, response: Response): Response {
|
function addSearchIndexHeaders(request: Request, response: Response): Response {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const contentType = String(response.headers.get('Content-Type') || '').toLowerCase();
|
const contentType = String(response.headers.get('Content-Type') || '').toLowerCase();
|
||||||
@@ -58,7 +47,7 @@ async function maybeServeAsset(request: Request, env: Env): Promise<Response | n
|
|||||||
if (!env.ASSETS) return null;
|
if (!env.ASSETS) return null;
|
||||||
if (request.method !== 'GET' && request.method !== 'HEAD') return null;
|
if (request.method !== 'GET' && request.method !== 'HEAD') return null;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
if (isWorkerHandledPath(url.pathname)) return null;
|
if (isBackendRequestPath(url.pathname)) return null;
|
||||||
|
|
||||||
const response = await env.ASSETS.fetch(request);
|
const response = await env.ASSETS.fetch(request);
|
||||||
return addSearchIndexHeaders(request, response);
|
return addSearchIndexHeaders(request, response);
|
||||||
@@ -90,6 +79,12 @@ export default {
|
|||||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||||
void ctx;
|
void ctx;
|
||||||
const normalizedRequest = normalizeRequestUrl(request);
|
const normalizedRequest = normalizeRequestUrl(request);
|
||||||
|
const requestPath = new URL(normalizedRequest.url).pathname;
|
||||||
|
|
||||||
|
if (isWebVaultHidden(env) && !isBackendRequestPath(requestPath)) {
|
||||||
|
return webVaultNotFoundResponse(normalizedRequest);
|
||||||
|
}
|
||||||
|
|
||||||
const assetResponse = await maybeServeAsset(normalizedRequest, env);
|
const assetResponse = await maybeServeAsset(normalizedRequest, env);
|
||||||
if (assetResponse) {
|
if (assetResponse) {
|
||||||
return applyCors(normalizedRequest, assetResponse, env);
|
return applyCors(normalizedRequest, assetResponse, env);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface Env {
|
|||||||
ASSETS?: {
|
ASSETS?: {
|
||||||
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
||||||
};
|
};
|
||||||
|
// Set to "1" to return 404 for the Web Vault while keeping client APIs available.
|
||||||
|
HIDE_WEB_VAULT?: string;
|
||||||
// Prefer R2 when available. Optional to support KV-only deployments.
|
// Prefer R2 when available. Optional to support KV-only deployments.
|
||||||
ATTACHMENTS?: R2Bucket;
|
ATTACHMENTS?: R2Bucket;
|
||||||
// Optional fallback for attachment/send file storage (no credit card required).
|
// Optional fallback for attachment/send file storage (no credit card required).
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Env } from './types';
|
||||||
|
|
||||||
|
const BACKEND_PATH_PREFIXES = [
|
||||||
|
'/api',
|
||||||
|
'/identity',
|
||||||
|
'/icons',
|
||||||
|
'/fill-assist',
|
||||||
|
'/notifications',
|
||||||
|
'/.well-known',
|
||||||
|
// Compatibility aliases retained for older Bitwarden clients.
|
||||||
|
'/devices',
|
||||||
|
'/auth-requests',
|
||||||
|
'/webauthn',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const BACKEND_EXACT_PATHS = new Set([
|
||||||
|
'/v1/assetlinks:check',
|
||||||
|
'/web-bootstrap',
|
||||||
|
'/config',
|
||||||
|
'/accounts/kdf',
|
||||||
|
'/settings/domains',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isBackendRequestPath(pathname: string): boolean {
|
||||||
|
const path = pathname.toLowerCase();
|
||||||
|
if (BACKEND_EXACT_PATHS.has(path)) return true;
|
||||||
|
|
||||||
|
return BACKEND_PATH_PREFIXES.some((prefix) => (
|
||||||
|
path === prefix || path.startsWith(`${prefix}/`)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWebVaultHidden(env: Env): boolean {
|
||||||
|
return String(env.HIDE_WEB_VAULT || '').trim() === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function webVaultNotFoundResponse(request: Request): Response {
|
||||||
|
const body = request.method === 'HEAD' ? null : 'Not Found';
|
||||||
|
return new Response(body, {
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-store, max-age=0',
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
'X-Robots-Tag': 'noindex, nofollow, noarchive, nosnippet',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
+1
-1
@@ -9,7 +9,7 @@ command = "npm run build"
|
|||||||
binding = "ASSETS"
|
binding = "ASSETS"
|
||||||
directory = "./dist"
|
directory = "./dist"
|
||||||
not_found_handling = "single-page-application"
|
not_found_handling = "single-page-application"
|
||||||
run_worker_first = false
|
run_worker_first = true
|
||||||
|
|
||||||
[triggers]
|
[triggers]
|
||||||
crons = [ "*/5 * * * *" ]
|
crons = [ "*/5 * * * *" ]
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ command = "npm run build"
|
|||||||
binding = "ASSETS"
|
binding = "ASSETS"
|
||||||
directory = "./dist"
|
directory = "./dist"
|
||||||
not_found_handling = "single-page-application"
|
not_found_handling = "single-page-application"
|
||||||
run_worker_first = false
|
run_worker_first = true
|
||||||
|
|
||||||
[triggers]
|
[triggers]
|
||||||
crons = [ "*/5 * * * *" ]
|
crons = [ "*/5 * * * *" ]
|
||||||
|
|||||||
Reference in New Issue
Block a user