fix: enhance attachment handling and folder deletion logic; improve error responses and rate limiting

This commit is contained in:
shuaiplus
2026-02-18 03:06:50 +08:00
parent 73db6c518b
commit e1f1c6f865
9 changed files with 166 additions and 52 deletions
+5 -1
View File
@@ -234,7 +234,6 @@ export async function handlePublicDownloadAttachment(
}
const storage = new StorageService(env.DB);
// Verify attachment exists
const attachment = await storage.getAttachment(attachmentId);
@@ -250,6 +249,11 @@ export async function handlePublicDownloadAttachment(
return errorResponse('Attachment file not found', 404);
}
const firstUse = await storage.consumeAttachmentDownloadToken(claims.jti, claims.exp);
if (!firstUse) {
return errorResponse('Invalid or expired token', 401);
}
return new Response(object.body, {
headers: {
'Content-Type': 'application/octet-stream',
+1
View File
@@ -103,6 +103,7 @@ export async function handleDeleteFolder(request: Request, env: Env, userId: str
return errorResponse('Folder not found', 404);
}
await storage.clearFolderFromCiphers(userId, id);
await storage.deleteFolder(id, userId);
await storage.updateRevisionDate(userId);
+12 -9
View File
@@ -12,12 +12,15 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
let body: Record<string, string>;
const contentType = request.headers.get('content-type') || '';
if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = await request.formData();
body = Object.fromEntries(formData.entries()) as Record<string, string>;
} else {
body = await request.json();
try {
if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = await request.formData();
body = Object.fromEntries(formData.entries()) as Record<string, string>;
} else {
body = await request.json();
}
} catch {
return identityErrorResponse('Invalid request payload', 'invalid_request', 400);
}
const grantType = body.grant_type;
@@ -108,12 +111,12 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
// Refresh token
const refreshToken = body.refresh_token;
if (!refreshToken) {
return errorResponse('Refresh token is required', 400);
return identityErrorResponse('Refresh token is required', 'invalid_request', 400);
}
const result = await auth.refreshAccessToken(refreshToken);
if (!result) {
return errorResponse('Invalid refresh token', 401);
return identityErrorResponse('Invalid refresh token', 'invalid_grant', 400);
}
// Revoke old refresh token (prevent reuse)
@@ -158,7 +161,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
return jsonResponse(response);
}
return errorResponse('Unsupported grant type', 400);
return identityErrorResponse('Unsupported grant type', 'unsupported_grant_type', 400);
}
// POST /identity/accounts/prelogin
-6
View File
@@ -649,8 +649,6 @@ function renderRegisterPageHTML(jwtState: JwtSecretState | null): string {
creating: '正在创建…',
doneTitle: '初始化完成',
doneDesc: '服务已就绪。在 Bitwarden 客户端中填入以下服务器地址:',
important: '重要提示',
limitations: '本项目仅支持单用户:不能添加新用户;不支持修改主密码;如果忘记主密码,只能重新部署并重新注册。',
hideTitle: '隐藏初始化页',
hideDesc: '隐藏后,初始化页对任何人都会返回 404。你的密码库仍可正常使用。',
hideBtn: '隐藏初始化页',
@@ -738,8 +736,6 @@ function renderRegisterPageHTML(jwtState: JwtSecretState | null): string {
creating: 'Creating…',
doneTitle: 'Setup complete',
doneDesc: 'Your server is ready. Use this URL in Bitwarden clients:',
important: 'Important',
limitations: 'Single user only: no additional users, no master password change. If forgotten, redeploy and register again.',
hideTitle: 'Hide setup page',
hideDesc: 'After hiding, this page returns 404 for everyone. Vault still works.',
hideBtn: 'Hide setup page',
@@ -843,8 +839,6 @@ function renderRegisterPageHTML(jwtState: JwtSecretState | null): string {
setText('submitBtn', t('create'));
setText('t_done_title', t('doneTitle'));
setText('t_done_desc', t('doneDesc'));
setText('t_important', t('important'));
setText('t_limitations', t('limitations'));
setText('t_hide_title', t('hideTitle'));
setText('t_hide_desc', t('hideDesc'));
setText('hideBtn', t('hideBtn'));
+12 -7
View File
@@ -6,6 +6,9 @@ import { cipherToResponse } from './ciphers';
// GET /api/sync
export async function handleSync(request: Request, env: Env, userId: string): Promise<Response> {
const storage = new StorageService(env.DB);
const url = new URL(request.url);
const excludeDomainsParam = url.searchParams.get('excludeDomains');
const excludeDomains = excludeDomainsParam !== null && /^(1|true|yes)$/i.test(excludeDomainsParam);
const user = await storage.getUserById(userId);
if (!user) {
@@ -61,11 +64,13 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
folders: folderResponses,
collections: [],
ciphers: cipherResponses,
domains: {
equivalentDomains: [],
globalEquivalentDomains: [],
object: 'domains',
},
domains: excludeDomains
? null
: {
equivalentDomains: [],
globalEquivalentDomains: [],
object: 'domains',
},
policies: [],
sends: [],
// PascalCase for desktop/browser clients
@@ -81,7 +86,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
},
MasterKeyEncryptedUserKey: user.key,
MasterKeyWrappedUserKey: user.key,
Salt: user.email,
Salt: user.email.toLowerCase(),
Object: 'masterPasswordUnlock',
},
},
@@ -96,7 +101,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
},
masterKeyWrappedUserKey: user.key,
masterKeyEncryptedUserKey: user.key,
salt: user.email,
salt: user.email.toLowerCase(),
},
},
object: 'sync',