mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-04 22:40:11 +00:00
fix(security): scope storage reads by user
This commit is contained in:
@@ -1,151 +0,0 @@
|
||||
name: Sync upstream
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_commit:
|
||||
description: 'Commit hash (leave blank to use latest commit)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Add upstream
|
||||
run: |
|
||||
git remote add upstream https://github.com/shuaiplus/NodeWarden.git || true
|
||||
git fetch upstream --tags
|
||||
|
||||
- name: Resolve target commit
|
||||
id: resolve
|
||||
run: |
|
||||
TRIGGER="${{ github.event_name }}"
|
||||
MANUAL_INPUT="${{ github.event.inputs.target_commit }}"
|
||||
|
||||
if [ "$TRIGGER" = "schedule" ]; then
|
||||
# Auto mode: resolve latest upstream release tag
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/shuaiplus/NodeWarden/releases/latest | jq -r .tag_name)
|
||||
if [ "$LATEST_TAG" = "null" ] || [ -z "$LATEST_TAG" ]; then
|
||||
echo "No release found in upstream."
|
||||
exit 1
|
||||
fi
|
||||
TARGET_SHA=$(git rev-list -n 1 "$LATEST_TAG" 2>/dev/null)
|
||||
if [ -z "$TARGET_SHA" ]; then
|
||||
echo "Tag '$LATEST_TAG' not found after fetch."
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "mode=auto"
|
||||
echo "latest_tag=$LATEST_TAG"
|
||||
echo "target_sha=$TARGET_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Auto mode — latest release: $LATEST_TAG ($TARGET_SHA)"
|
||||
|
||||
elif [ -n "$MANUAL_INPUT" ]; then
|
||||
# Manual mode: use provided commit hash or tag
|
||||
TARGET_SHA=$(git rev-parse "$MANUAL_INPUT" 2>/dev/null)
|
||||
if [ -z "$TARGET_SHA" ]; then
|
||||
echo "Cannot resolve '$MANUAL_INPUT' to a commit."
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "mode=manual"
|
||||
echo "target_sha=$TARGET_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Manual mode — target: $MANUAL_INPUT ($TARGET_SHA)"
|
||||
|
||||
else
|
||||
# Manual mode, blank input: use latest commit on upstream/main
|
||||
TARGET_SHA=$(git rev-parse upstream/main)
|
||||
{
|
||||
echo "mode=manual"
|
||||
echo "target_sha=$TARGET_SHA"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Manual mode — latest commit: $TARGET_SHA"
|
||||
fi
|
||||
|
||||
- name: Check if update is needed
|
||||
id: check
|
||||
run: |
|
||||
TARGET_SHA="${{ steps.resolve.outputs.target_sha }}"
|
||||
MODE="${{ steps.resolve.outputs.mode }}"
|
||||
|
||||
if [ "$MODE" = "manual" ]; then
|
||||
# Manual: skip only if HEAD is exactly this commit
|
||||
CURRENT_SHA=$(git rev-parse HEAD)
|
||||
if [ "$CURRENT_SHA" = "$TARGET_SHA" ]; then
|
||||
echo "Already at $TARGET_SHA — skipping."
|
||||
echo "needs_update=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Switching to $TARGET_SHA"
|
||||
echo "needs_update=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
# Auto: skip if target is already in ancestry
|
||||
if git merge-base --is-ancestor "$TARGET_SHA" HEAD 2>/dev/null; then
|
||||
echo "Already up to date with $TARGET_SHA — skipping."
|
||||
echo "needs_update=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Update needed — target: $TARGET_SHA"
|
||||
echo "needs_update=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Apply update
|
||||
if: steps.check.outputs.needs_update == 'true'
|
||||
run: |
|
||||
TARGET_SHA="${{ steps.resolve.outputs.target_sha }}"
|
||||
MODE="${{ steps.resolve.outputs.mode }}"
|
||||
git checkout main
|
||||
if [ "$MODE" = "manual" ]; then
|
||||
# Hard reset allows both upgrade and rollback
|
||||
git reset --hard "$TARGET_SHA"
|
||||
else
|
||||
git merge "$TARGET_SHA" --no-edit
|
||||
fi
|
||||
|
||||
- name: Restore workflow file
|
||||
if: steps.check.outputs.needs_update == 'true'
|
||||
run: |
|
||||
# Always keep our own workflow file, never let upstream overwrite it
|
||||
git checkout 'HEAD@{1}' -- .github/workflows/sync-upstream.yml 2>/dev/null || true
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit -m "chore: restore sync-upstream workflow after sync"
|
||||
fi
|
||||
|
||||
- name: Push
|
||||
if: steps.check.outputs.needs_update == 'true'
|
||||
run: |
|
||||
if [ "${{ steps.resolve.outputs.mode }}" = "manual" ]; then
|
||||
git push origin main --force
|
||||
else
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.needs_update }}" = "true" ]; then
|
||||
{
|
||||
echo "### Synced successfully"
|
||||
echo "- **Mode:** ${{ steps.resolve.outputs.mode }}"
|
||||
echo "- **Tag:** ${{ steps.resolve.outputs.latest_tag || 'N/A (manual)' }}"
|
||||
echo "- **Commit:** \`${{ steps.resolve.outputs.target_sha }}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "### Nothing to update" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
@@ -60,6 +60,7 @@ NodeWarden-compat/
|
||||
|
||||
# Compatibility analysis documents
|
||||
BITWARDEN_COMPATIBILITY_ANALYSIS.md
|
||||
security-audits/
|
||||
.mcp.json
|
||||
opencode.jsonc
|
||||
.cursor/
|
||||
|
||||
+15
-15
@@ -167,7 +167,7 @@ export async function handleCreateAttachment(
|
||||
const storage = new StorageService(env.DB);
|
||||
|
||||
// Verify cipher exists and belongs to user
|
||||
const cipher = await storage.getCipher(cipherId);
|
||||
const cipher = await storage.getCipherForUser(cipherId, userId);
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
}
|
||||
@@ -205,7 +205,7 @@ export async function handleCreateAttachment(
|
||||
await storage.saveAttachment(attachment);
|
||||
|
||||
// Add attachment to cipher
|
||||
await storage.addAttachmentToCipher(cipherId, attachmentId);
|
||||
await storage.addAttachmentToCipherForUser(cipherId, attachmentId, userId);
|
||||
|
||||
// Update cipher revision date
|
||||
const revisionInfo = await storage.updateCipherRevisionDate(cipherId);
|
||||
@@ -215,7 +215,7 @@ export async function handleCreateAttachment(
|
||||
}
|
||||
|
||||
// Get updated cipher for response
|
||||
const updatedCipher = await storage.getCipher(cipherId);
|
||||
const updatedCipher = await storage.getCipherForUser(cipherId, userId);
|
||||
const attachments = await storage.getAttachmentsByCipher(cipherId);
|
||||
const jwtSecret = getSafeJwtSecret(env);
|
||||
if (!jwtSecret) {
|
||||
@@ -244,13 +244,13 @@ export async function handleUploadAttachment(
|
||||
const storage = new StorageService(env.DB);
|
||||
|
||||
// Verify cipher exists and belongs to user
|
||||
const cipher = await storage.getCipher(cipherId);
|
||||
const cipher = await storage.getCipherForUser(cipherId, userId);
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
}
|
||||
|
||||
// Verify attachment exists
|
||||
const attachment = await storage.getAttachment(attachmentId);
|
||||
const attachment = await storage.getAttachmentForUser(attachmentId, userId);
|
||||
if (!attachment || attachment.cipherId !== cipherId) {
|
||||
return errorResponse('Attachment not found', 404);
|
||||
}
|
||||
@@ -283,12 +283,12 @@ export async function handlePublicUploadAttachment(
|
||||
}
|
||||
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(cipherId);
|
||||
const cipher = await storage.getCipherForUser(cipherId, claims.userId);
|
||||
if (!cipher || cipher.userId !== claims.userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
}
|
||||
|
||||
const attachment = await storage.getAttachment(attachmentId);
|
||||
const attachment = await storage.getAttachmentForUser(attachmentId, claims.userId);
|
||||
if (!attachment || attachment.cipherId !== cipherId) {
|
||||
return errorResponse('Attachment not found', 404);
|
||||
}
|
||||
@@ -308,13 +308,13 @@ export async function handleGetAttachment(
|
||||
const storage = new StorageService(env.DB);
|
||||
|
||||
// Verify cipher exists and belongs to user
|
||||
const cipher = await storage.getCipher(cipherId);
|
||||
const cipher = await storage.getCipherForUser(cipherId, userId);
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
}
|
||||
|
||||
// Verify attachment exists
|
||||
const attachment = await storage.getAttachment(attachmentId);
|
||||
const attachment = await storage.getAttachmentForUser(attachmentId, userId);
|
||||
if (!attachment || attachment.cipherId !== cipherId) {
|
||||
return errorResponse('Attachment not found', 404);
|
||||
}
|
||||
@@ -349,12 +349,12 @@ export async function handleUpdateAttachmentMetadata(
|
||||
): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
|
||||
const cipher = await storage.getCipher(cipherId);
|
||||
const cipher = await storage.getCipherForUser(cipherId, userId);
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
}
|
||||
|
||||
const attachment = await storage.getAttachment(attachmentId);
|
||||
const attachment = await storage.getAttachmentForUser(attachmentId, userId);
|
||||
if (!attachment || attachment.cipherId !== cipherId) {
|
||||
return errorResponse('Attachment not found', 404);
|
||||
}
|
||||
@@ -471,13 +471,13 @@ export async function handleDeleteAttachment(
|
||||
const storage = new StorageService(env.DB);
|
||||
|
||||
// Verify cipher exists and belongs to user
|
||||
const cipher = await storage.getCipher(cipherId);
|
||||
const cipher = await storage.getCipherForUser(cipherId, userId);
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
}
|
||||
|
||||
// Verify attachment exists
|
||||
const attachment = await storage.getAttachment(attachmentId);
|
||||
const attachment = await storage.getAttachmentForUser(attachmentId, userId);
|
||||
if (!attachment || attachment.cipherId !== cipherId) {
|
||||
return errorResponse('Attachment not found', 404);
|
||||
}
|
||||
@@ -486,7 +486,7 @@ export async function handleDeleteAttachment(
|
||||
await deleteBlobObject(env, path);
|
||||
|
||||
// Delete attachment metadata
|
||||
await storage.deleteAttachment(attachmentId);
|
||||
await storage.deleteAttachmentForUser(attachmentId, userId);
|
||||
|
||||
// Update cipher revision date
|
||||
const revisionInfo = await storage.updateCipherRevisionDate(cipherId);
|
||||
@@ -501,7 +501,7 @@ export async function handleDeleteAttachment(
|
||||
}
|
||||
|
||||
// Get updated cipher for response
|
||||
const updatedCipher = await storage.getCipher(cipherId);
|
||||
const updatedCipher = await storage.getCipherForUser(cipherId, userId);
|
||||
const attachments = await storage.getAttachmentsByCipher(cipherId);
|
||||
const cipherResponse = cipherToResponse(updatedCipher!, attachments);
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ export async function handleCreateAuthRequest(request: Request, env: Env): Promi
|
||||
|
||||
export async function handleGetAuthRequest(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const authRequest = await storage.getAuthRequestById(id);
|
||||
const authRequest = await storage.getAuthRequestByIdForUser(id, userId);
|
||||
if (!authRequest || authRequest.userId !== userId) return errorResponse('Not found', 404);
|
||||
return jsonResponse(toAuthRequestResponse(request, authRequest));
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export async function handleUpdateAuthRequest(request: Request, env: Env, userId
|
||||
const body = await readJsonBody(request);
|
||||
if (!body) return errorResponse('Invalid request payload', 400);
|
||||
|
||||
const authRequest = await storage.getAuthRequestById(id);
|
||||
const authRequest = await storage.getAuthRequestByIdForUser(id, userId);
|
||||
if (!authRequest || authRequest.userId !== userId || isAuthRequestExpired(authRequest)) {
|
||||
return errorResponse('Not found', 404);
|
||||
}
|
||||
@@ -275,7 +275,7 @@ export async function handleUpdateAuthRequest(request: Request, env: Env, userId
|
||||
masterPasswordHash,
|
||||
});
|
||||
if (!updated) return errorResponse('Auth request has already been answered.', 409);
|
||||
const updatedRequest = await storage.getAuthRequestById(id);
|
||||
const updatedRequest = await storage.getAuthRequestByIdForUser(id, userId);
|
||||
// Match Bitwarden upstream behavior: only approval wakes the originating anonymous
|
||||
// client. Denials are not pushed to avoid leaking that a login attempt was rejected.
|
||||
if (approved) {
|
||||
|
||||
+11
-11
@@ -812,7 +812,7 @@ export async function handleGetCiphers(request: Request, env: Env, userId: strin
|
||||
// GET /api/ciphers/:id
|
||||
export async function handleGetCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -827,8 +827,8 @@ export async function handleGetCipher(request: Request, env: Env, userId: string
|
||||
|
||||
async function verifyFolderOwnership(storage: StorageService, folderId: string | null | undefined, userId: string): Promise<boolean> {
|
||||
if (!folderId) return true;
|
||||
const folder = await storage.getFolder(folderId);
|
||||
return !!(folder && folder.userId === userId);
|
||||
const folder = await storage.getFolderForUser(folderId, userId);
|
||||
return !!folder;
|
||||
}
|
||||
|
||||
// POST /api/ciphers
|
||||
@@ -909,7 +909,7 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
||||
// PUT /api/ciphers/:id
|
||||
export async function handleUpdateCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const existingCipher = await storage.getCipher(id);
|
||||
const existingCipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!existingCipher || existingCipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -1020,7 +1020,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
// DELETE /api/ciphers/:id
|
||||
export async function handleDeleteCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -1052,7 +1052,7 @@ export async function handleDeleteCipher(request: Request, env: Env, userId: str
|
||||
// - If item is already soft-deleted -> hard delete.
|
||||
export async function handleDeleteCipherCompat(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -1079,7 +1079,7 @@ export async function handleDeleteCipherCompat(request: Request, env: Env, userI
|
||||
// DELETE /api/ciphers/:id (permanent)
|
||||
export async function handlePermanentDeleteCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -1104,7 +1104,7 @@ export async function handlePermanentDeleteCipher(request: Request, env: Env, us
|
||||
// PUT /api/ciphers/:id/restore
|
||||
export async function handleRestoreCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -1126,7 +1126,7 @@ export async function handleRestoreCipher(request: Request, env: Env, userId: st
|
||||
// PUT /api/ciphers/:id/partial - Update only favorite/folderId
|
||||
export async function handlePartialUpdateCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -1218,7 +1218,7 @@ function parseCipherIdList(body: { ids?: unknown }): string[] | null {
|
||||
// PUT/POST /api/ciphers/:id/archive
|
||||
export async function handleArchiveCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
@@ -1244,7 +1244,7 @@ export async function handleArchiveCipher(request: Request, env: Env, userId: st
|
||||
// PUT/POST /api/ciphers/:id/unarchive
|
||||
export async function handleUnarchiveCipher(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const cipher = await storage.getCipher(id);
|
||||
const cipher = await storage.getCipherForUser(id, userId);
|
||||
|
||||
if (!cipher || cipher.userId !== userId) {
|
||||
return errorResponse('Cipher not found', 404);
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function handleGetFolders(request: Request, env: Env, userId: strin
|
||||
// GET /api/folders/:id
|
||||
export async function handleGetFolder(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const folder = await storage.getFolder(id);
|
||||
const folder = await storage.getFolderForUser(id, userId);
|
||||
|
||||
if (!folder || folder.userId !== userId) {
|
||||
return errorResponse('Folder not found', 404);
|
||||
@@ -129,7 +129,7 @@ export async function handleCreateFolder(request: Request, env: Env, userId: str
|
||||
// PUT /api/folders/:id
|
||||
export async function handleUpdateFolder(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const folder = await storage.getFolder(id);
|
||||
const folder = await storage.getFolderForUser(id, userId);
|
||||
|
||||
if (!folder || folder.userId !== userId) {
|
||||
return errorResponse('Folder not found', 404);
|
||||
@@ -163,7 +163,7 @@ export async function handleUpdateFolder(request: Request, env: Env, userId: str
|
||||
// DELETE /api/folders/:id
|
||||
export async function handleDeleteFolder(request: Request, env: Env, userId: string, id: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const folder = await storage.getFolder(id);
|
||||
const folder = await storage.getFolderForUser(id, userId);
|
||||
|
||||
if (!folder || folder.userId !== userId) {
|
||||
return errorResponse('Folder not found', 404);
|
||||
@@ -204,8 +204,8 @@ export async function handleBulkDeleteFolders(request: Request, env: Env, userId
|
||||
|
||||
const folders = (
|
||||
await Promise.all(ids.map(async (id) => {
|
||||
const folder = await storage.getFolder(id);
|
||||
return folder && folder.userId === userId ? folder : null;
|
||||
const folder = await storage.getFolderForUser(id, userId);
|
||||
return folder;
|
||||
}))
|
||||
).filter((folder): folder is Folder => !!folder);
|
||||
const revisionDate = await storage.bulkDeleteFolders(ids, userId);
|
||||
|
||||
@@ -341,7 +341,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
||||
let valid = false;
|
||||
const normalizedAuthRequestId = String(authRequestId || '').trim();
|
||||
if (normalizedAuthRequestId) {
|
||||
const authRequest = await storage.getAuthRequestById(normalizedAuthRequestId);
|
||||
const authRequest = await storage.getAuthRequestByIdForUser(normalizedAuthRequestId, user.id);
|
||||
valid = !!(
|
||||
authRequest &&
|
||||
authRequest.userId === user.id &&
|
||||
|
||||
@@ -134,7 +134,7 @@ export async function handleGetSends(request: Request, env: Env, userId: string)
|
||||
export async function handleGetSend(request: Request, env: Env, userId: string, sendId: string): Promise<Response> {
|
||||
void request;
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, userId);
|
||||
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found', 404);
|
||||
@@ -401,7 +401,7 @@ export async function handleGetSendFileUpload(
|
||||
): Promise<Response> {
|
||||
void request;
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, userId);
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found', 404);
|
||||
}
|
||||
@@ -436,7 +436,7 @@ export async function handleUploadSendFile(
|
||||
fileId: string
|
||||
): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, userId);
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found. Unable to save the file.', 404);
|
||||
}
|
||||
@@ -472,7 +472,7 @@ export async function handlePublicUploadSendFile(
|
||||
}
|
||||
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, claims.userId);
|
||||
if (!send || send.userId !== claims.userId) {
|
||||
return errorResponse('Send not found. Unable to save the file.', 404);
|
||||
}
|
||||
@@ -485,7 +485,7 @@ export async function handlePublicUploadSendFile(
|
||||
|
||||
export async function handleUpdateSend(request: Request, env: Env, userId: string, sendId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, userId);
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found', 404);
|
||||
}
|
||||
@@ -632,7 +632,7 @@ export async function handleUpdateSend(request: Request, env: Env, userId: strin
|
||||
|
||||
export async function handleDeleteSend(request: Request, env: Env, userId: string, sendId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, userId);
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found', 404);
|
||||
}
|
||||
@@ -698,7 +698,7 @@ export async function handleBulkDeleteSends(request: Request, env: Env, userId:
|
||||
|
||||
export async function handleRemoveSendPassword(request: Request, env: Env, userId: string, sendId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, userId);
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found', 404);
|
||||
}
|
||||
@@ -719,7 +719,7 @@ export async function handleRemoveSendPassword(request: Request, env: Env, userI
|
||||
|
||||
export async function handleRemoveSendAuth(request: Request, env: Env, userId: string, sendId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const send = await storage.getSend(sendId);
|
||||
const send = await storage.getSendForUser(sendId, userId);
|
||||
if (!send || send.userId !== userId) {
|
||||
return errorResponse('Send not found', 404);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,35 @@ export async function getAttachment(db: D1Database, id: string): Promise<Attachm
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAttachmentForUser(db: D1Database, id: string, userId: string): Promise<Attachment | null> {
|
||||
const row = await db
|
||||
.prepare(
|
||||
`SELECT a.id, a.cipher_id, a.file_name, a.size, a.size_name, a.key
|
||||
FROM attachments a
|
||||
INNER JOIN ciphers c ON c.id = a.cipher_id
|
||||
WHERE a.id = ? AND c.user_id = ?`
|
||||
)
|
||||
.bind(id, userId)
|
||||
.first<any>();
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
cipherId: row.cipher_id,
|
||||
fileName: row.file_name,
|
||||
size: row.size,
|
||||
sizeName: row.size_name,
|
||||
key: row.key,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveAttachment(db: D1Database, safeBind: SafeBind, attachment: Attachment): Promise<void> {
|
||||
const stmt = db.prepare(
|
||||
'INSERT INTO attachments(id, cipher_id, file_name, size, size_name, key) VALUES(?, ?, ?, ?, ?, ?) ' +
|
||||
'ON CONFLICT(id) DO UPDATE SET cipher_id=excluded.cipher_id, file_name=excluded.file_name, size=excluded.size, size_name=excluded.size_name, key=excluded.key'
|
||||
'ON CONFLICT(id) DO UPDATE SET cipher_id=excluded.cipher_id, file_name=excluded.file_name, size=excluded.size, size_name=excluded.size_name, key=excluded.key ' +
|
||||
'WHERE EXISTS (' +
|
||||
'SELECT 1 FROM ciphers current_cipher INNER JOIN ciphers next_cipher ON next_cipher.id = excluded.cipher_id ' +
|
||||
'WHERE current_cipher.id = attachments.cipher_id AND current_cipher.user_id = next_cipher.user_id' +
|
||||
')'
|
||||
);
|
||||
await safeBind(stmt, attachment.id, attachment.cipherId, attachment.fileName, attachment.size, attachment.sizeName, attachment.key).run();
|
||||
}
|
||||
@@ -34,6 +59,20 @@ export async function deleteAttachment(db: D1Database, id: string): Promise<void
|
||||
await db.prepare('DELETE FROM attachments WHERE id = ?').bind(id).run();
|
||||
}
|
||||
|
||||
export async function deleteAttachmentForUser(db: D1Database, id: string, userId: string): Promise<void> {
|
||||
await db
|
||||
.prepare(
|
||||
`DELETE FROM attachments
|
||||
WHERE id = ?
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ciphers c
|
||||
WHERE c.id = attachments.cipher_id AND c.user_id = ?
|
||||
)`
|
||||
)
|
||||
.bind(id, userId)
|
||||
.run();
|
||||
}
|
||||
|
||||
export async function bulkDeleteAttachmentsByIds(
|
||||
db: D1Database,
|
||||
sqlChunkSize: SqlChunkSize,
|
||||
@@ -135,6 +174,30 @@ export async function addAttachmentToCipher(db: D1Database, cipherId: string, at
|
||||
await db.prepare('UPDATE attachments SET cipher_id = ? WHERE id = ?').bind(cipherId, attachmentId).run();
|
||||
}
|
||||
|
||||
export async function addAttachmentToCipherForUser(
|
||||
db: D1Database,
|
||||
cipherId: string,
|
||||
attachmentId: string,
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE attachments
|
||||
SET cipher_id = ?
|
||||
WHERE id = ?
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ciphers target_cipher
|
||||
WHERE target_cipher.id = ? AND target_cipher.user_id = ?
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ciphers current_cipher
|
||||
WHERE current_cipher.id = attachments.cipher_id AND current_cipher.user_id = ?
|
||||
)`
|
||||
)
|
||||
.bind(cipherId, attachmentId, cipherId, userId, userId)
|
||||
.run();
|
||||
}
|
||||
|
||||
export async function deleteAllAttachmentsByCipher(db: D1Database, cipherId: string): Promise<void> {
|
||||
await db.prepare('DELETE FROM attachments WHERE cipher_id = ?').bind(cipherId).run();
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ export async function getAuthRequestById(db: D1Database, id: string): Promise<Au
|
||||
return row ? mapAuthRequestRow(row) : null;
|
||||
}
|
||||
|
||||
export async function getAuthRequestByIdForUser(db: D1Database, id: string, userId: string): Promise<AuthRequestRecord | null> {
|
||||
const row = await db.prepare(`${AUTH_REQUEST_SELECT} WHERE id = ? AND user_id = ? LIMIT 1`).bind(id, userId).first<any>();
|
||||
return row ? mapAuthRequestRow(row) : null;
|
||||
}
|
||||
|
||||
export async function listAuthRequestsByUserId(db: D1Database, userId: string): Promise<AuthRequestRecord[]> {
|
||||
const res = await db.prepare(`${AUTH_REQUEST_SELECT} WHERE user_id = ? ORDER BY creation_date DESC`).bind(userId).all<any>();
|
||||
return (res.results || []).map(mapAuthRequestRow);
|
||||
|
||||
@@ -107,6 +107,14 @@ export async function getCipher(db: D1Database, id: string): Promise<Cipher | nu
|
||||
return parseCipherRow(row);
|
||||
}
|
||||
|
||||
export async function getCipherForUser(db: D1Database, id: string, userId: string): Promise<Cipher | null> {
|
||||
const row = await db
|
||||
.prepare(`SELECT ${selectCipherColumns()} FROM ciphers WHERE id = ? AND user_id = ?`)
|
||||
.bind(id, userId)
|
||||
.first<CipherRow>();
|
||||
return parseCipherRow(row);
|
||||
}
|
||||
|
||||
export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cipher): Promise<void> {
|
||||
const folderId = normalizeOptionalId(cipher.folderId);
|
||||
const data = buildCipherData(cipher, folderId);
|
||||
@@ -114,7 +122,8 @@ export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cip
|
||||
'INSERT INTO ciphers(id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at) ' +
|
||||
'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' +
|
||||
'ON CONFLICT(id) DO UPDATE SET ' +
|
||||
'user_id=excluded.user_id, type=excluded.type, folder_id=excluded.folder_id, name=excluded.name, notes=excluded.notes, favorite=excluded.favorite, data=excluded.data, reprompt=excluded.reprompt, key=excluded.key, updated_at=excluded.updated_at, archived_at=excluded.archived_at, deleted_at=excluded.deleted_at'
|
||||
'type=excluded.type, folder_id=excluded.folder_id, name=excluded.name, notes=excluded.notes, favorite=excluded.favorite, data=excluded.data, reprompt=excluded.reprompt, key=excluded.key, updated_at=excluded.updated_at, archived_at=excluded.archived_at, deleted_at=excluded.deleted_at ' +
|
||||
'WHERE user_id=excluded.user_id'
|
||||
);
|
||||
await safeBind(
|
||||
stmt,
|
||||
|
||||
@@ -19,11 +19,20 @@ export async function getFolder(db: D1Database, id: string): Promise<Folder | nu
|
||||
return mapFolderRow(row);
|
||||
}
|
||||
|
||||
export async function getFolderForUser(db: D1Database, id: string, userId: string): Promise<Folder | null> {
|
||||
const row = await db
|
||||
.prepare('SELECT id, user_id, name, created_at, updated_at FROM folders WHERE id = ? AND user_id = ?')
|
||||
.bind(id, userId)
|
||||
.first<any>();
|
||||
if (!row) return null;
|
||||
return mapFolderRow(row);
|
||||
}
|
||||
|
||||
export async function saveFolder(db: D1Database, folder: Folder): Promise<void> {
|
||||
await db
|
||||
.prepare(
|
||||
'INSERT INTO folders(id, user_id, name, created_at, updated_at) VALUES(?, ?, ?, ?, ?) ' +
|
||||
'ON CONFLICT(id) DO UPDATE SET user_id=excluded.user_id, name=excluded.name, updated_at=excluded.updated_at'
|
||||
'ON CONFLICT(id) DO UPDATE SET name=excluded.name, updated_at=excluded.updated_at WHERE user_id=excluded.user_id'
|
||||
)
|
||||
.bind(folder.id, folder.userId, folder.name, folder.createdAt, folder.updatedAt)
|
||||
.run();
|
||||
|
||||
@@ -40,15 +40,27 @@ export async function getSend(db: D1Database, id: string): Promise<Send | null>
|
||||
return mapSendRow(row);
|
||||
}
|
||||
|
||||
export async function getSendForUser(db: D1Database, id: string, userId: string): Promise<Send | null> {
|
||||
const row = await db
|
||||
.prepare(
|
||||
'SELECT id, user_id, type, name, notes, data, key, password_hash, password_salt, password_iterations, auth_type, emails, max_access_count, access_count, disabled, hide_email, created_at, updated_at, expiration_date, deletion_date FROM sends WHERE id = ? AND user_id = ?'
|
||||
)
|
||||
.bind(id, userId)
|
||||
.first<any>();
|
||||
if (!row) return null;
|
||||
return mapSendRow(row);
|
||||
}
|
||||
|
||||
export async function saveSend(db: D1Database, safeBind: SafeBind, send: Send): Promise<void> {
|
||||
const stmt = db.prepare(
|
||||
'INSERT INTO sends(id, user_id, type, name, notes, data, key, password_hash, password_salt, password_iterations, auth_type, emails, max_access_count, access_count, disabled, hide_email, created_at, updated_at, expiration_date, deletion_date) ' +
|
||||
'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' +
|
||||
'ON CONFLICT(id) DO UPDATE SET ' +
|
||||
'user_id=excluded.user_id, type=excluded.type, name=excluded.name, notes=excluded.notes, data=excluded.data, key=excluded.key, ' +
|
||||
'type=excluded.type, name=excluded.name, notes=excluded.notes, data=excluded.data, key=excluded.key, ' +
|
||||
'password_hash=excluded.password_hash, password_salt=excluded.password_salt, password_iterations=excluded.password_iterations, auth_type=excluded.auth_type, emails=excluded.emails, ' +
|
||||
'max_access_count=excluded.max_access_count, access_count=excluded.access_count, disabled=excluded.disabled, hide_email=excluded.hide_email, ' +
|
||||
'updated_at=excluded.updated_at, expiration_date=excluded.expiration_date, deletion_date=excluded.deletion_date'
|
||||
'updated_at=excluded.updated_at, expiration_date=excluded.expiration_date, deletion_date=excluded.deletion_date ' +
|
||||
'WHERE user_id=excluded.user_id'
|
||||
);
|
||||
|
||||
await safeBind(
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
deleteFolder as deleteStoredFolder,
|
||||
getAllFolders as listStoredFolders,
|
||||
getFolder as findStoredFolder,
|
||||
getFolderForUser as findStoredFolderForUser,
|
||||
getFoldersPage as listStoredFoldersPage,
|
||||
saveFolder as saveStoredFolder,
|
||||
} from './storage-folder-repo';
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
bulkUnarchiveCiphers as unarchiveStoredCiphers,
|
||||
getAllCiphers as listStoredCiphers,
|
||||
getCipher as findStoredCipher,
|
||||
getCipherForUser as findStoredCipherForUser,
|
||||
getCiphersByIds as listStoredCiphersByIds,
|
||||
getCiphersPage as listStoredCiphersPage,
|
||||
saveCipher as saveStoredCipher,
|
||||
@@ -60,10 +62,13 @@ import {
|
||||
} from './storage-cipher-repo';
|
||||
import {
|
||||
addAttachmentToCipher as attachStoredAttachmentToCipher,
|
||||
addAttachmentToCipherForUser as attachStoredAttachmentToCipherForUser,
|
||||
bulkDeleteAttachmentsByIds as deleteStoredAttachmentsByIds,
|
||||
deleteAllAttachmentsByCipher as deleteStoredAttachmentsByCipher,
|
||||
deleteAttachment as deleteStoredAttachment,
|
||||
deleteAttachmentForUser as deleteStoredAttachmentForUser,
|
||||
getAttachment as findStoredAttachment,
|
||||
getAttachmentForUser as findStoredAttachmentForUser,
|
||||
getAttachmentsByCipher as listStoredAttachmentsByCipher,
|
||||
getAttachmentsByCipherIds as listStoredAttachmentsByCipherIds,
|
||||
getAttachmentsByUserId as listStoredAttachmentsByUserId,
|
||||
@@ -75,6 +80,7 @@ import {
|
||||
deleteSend as deleteStoredSend,
|
||||
getAllSends as listStoredSends,
|
||||
getSend as findStoredSend,
|
||||
getSendForUser as findStoredSendForUser,
|
||||
getSendsByIds as listStoredSendsByIds,
|
||||
getSendsPage as listStoredSendsPage,
|
||||
incrementSendAccessCount as incrementStoredSendAccessCount,
|
||||
@@ -114,6 +120,7 @@ import {
|
||||
import {
|
||||
createAuthRequest as createStoredAuthRequest,
|
||||
getAuthRequestById as findStoredAuthRequestById,
|
||||
getAuthRequestByIdForUser as findStoredAuthRequestByIdForUser,
|
||||
listAuthRequestsByUserId as listStoredAuthRequestsByUserId,
|
||||
listPendingAuthRequestsByUserId as listStoredPendingAuthRequestsByUserId,
|
||||
markAuthRequestAuthenticated as markStoredAuthRequestAuthenticated,
|
||||
@@ -458,6 +465,10 @@ export class StorageService {
|
||||
return findStoredCipher(this.db, id);
|
||||
}
|
||||
|
||||
async getCipherForUser(id: string, userId: string): Promise<Cipher | null> {
|
||||
return findStoredCipherForUser(this.db, id, userId);
|
||||
}
|
||||
|
||||
async saveCipher(cipher: Cipher): Promise<void> {
|
||||
await saveStoredCipher(this.db, this.safeBind.bind(this), cipher);
|
||||
}
|
||||
@@ -508,6 +519,10 @@ export class StorageService {
|
||||
return findStoredFolder(this.db, id);
|
||||
}
|
||||
|
||||
async getFolderForUser(id: string, userId: string): Promise<Folder | null> {
|
||||
return findStoredFolderForUser(this.db, id, userId);
|
||||
}
|
||||
|
||||
async saveFolder(folder: Folder): Promise<void> {
|
||||
await saveStoredFolder(this.db, folder);
|
||||
}
|
||||
@@ -546,6 +561,10 @@ export class StorageService {
|
||||
return findStoredAttachment(this.db, id);
|
||||
}
|
||||
|
||||
async getAttachmentForUser(id: string, userId: string): Promise<Attachment | null> {
|
||||
return findStoredAttachmentForUser(this.db, id, userId);
|
||||
}
|
||||
|
||||
async saveAttachment(attachment: Attachment): Promise<void> {
|
||||
await saveStoredAttachment(this.db, this.safeBind.bind(this), attachment);
|
||||
}
|
||||
@@ -554,6 +573,10 @@ export class StorageService {
|
||||
await deleteStoredAttachment(this.db, id);
|
||||
}
|
||||
|
||||
async deleteAttachmentForUser(id: string, userId: string): Promise<void> {
|
||||
await deleteStoredAttachmentForUser(this.db, id, userId);
|
||||
}
|
||||
|
||||
async bulkDeleteAttachmentsByIds(ids: string[]): Promise<void> {
|
||||
await deleteStoredAttachmentsByIds(this.db, this.sqlChunkSize.bind(this), ids);
|
||||
}
|
||||
@@ -574,6 +597,10 @@ export class StorageService {
|
||||
await attachStoredAttachmentToCipher(this.db, cipherId, attachmentId);
|
||||
}
|
||||
|
||||
async addAttachmentToCipherForUser(cipherId: string, attachmentId: string, userId: string): Promise<void> {
|
||||
await attachStoredAttachmentToCipherForUser(this.db, cipherId, attachmentId, userId);
|
||||
}
|
||||
|
||||
async deleteAllAttachmentsByCipher(cipherId: string): Promise<void> {
|
||||
await deleteStoredAttachmentsByCipher(this.db, cipherId);
|
||||
}
|
||||
@@ -634,6 +661,10 @@ export class StorageService {
|
||||
return findStoredSend(this.db, id);
|
||||
}
|
||||
|
||||
async getSendForUser(id: string, userId: string): Promise<Send | null> {
|
||||
return findStoredSendForUser(this.db, id, userId);
|
||||
}
|
||||
|
||||
async saveSend(send: Send): Promise<void> {
|
||||
await saveStoredSend(this.db, this.safeBind.bind(this), send);
|
||||
}
|
||||
@@ -783,6 +814,10 @@ export class StorageService {
|
||||
return findStoredAuthRequestById(this.db, id);
|
||||
}
|
||||
|
||||
async getAuthRequestByIdForUser(id: string, userId: string): Promise<AuthRequestRecord | null> {
|
||||
return findStoredAuthRequestByIdForUser(this.db, id, userId);
|
||||
}
|
||||
|
||||
async listAuthRequestsByUserId(userId: string): Promise<AuthRequestRecord[]> {
|
||||
return listStoredAuthRequestsByUserId(this.db, userId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user