diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml deleted file mode 100644 index db3c70b..0000000 --- a/.github/workflows/sync-upstream.yml +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore index dd4875c..e57bfb1 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ NodeWarden-compat/ # Compatibility analysis documents BITWARDEN_COMPATIBILITY_ANALYSIS.md +security-audits/ .mcp.json opencode.jsonc .cursor/ diff --git a/src/handlers/attachments.ts b/src/handlers/attachments.ts index 259fcce..35e906e 100644 --- a/src/handlers/attachments.ts +++ b/src/handlers/attachments.ts @@ -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 { 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); diff --git a/src/handlers/auth-requests.ts b/src/handlers/auth-requests.ts index e29a8ca..953f60b 100644 --- a/src/handlers/auth-requests.ts +++ b/src/handlers/auth-requests.ts @@ -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 { 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) { diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 73a8653..7a4ecd3 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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); diff --git a/src/handlers/folders.ts b/src/handlers/folders.ts index c7826c2..d89dbb9 100644 --- a/src/handlers/folders.ts +++ b/src/handlers/folders.ts @@ -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 { 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 { 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 { 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); diff --git a/src/handlers/identity.ts b/src/handlers/identity.ts index 0fd843d..00baa80 100644 --- a/src/handlers/identity.ts +++ b/src/handlers/identity.ts @@ -341,7 +341,7 @@ export async function handleToken(request: Request, env: Env): Promise 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 && diff --git a/src/handlers/sends-private.ts b/src/handlers/sends-private.ts index 15ac586..ddd91a4 100644 --- a/src/handlers/sends-private.ts +++ b/src/handlers/sends-private.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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); } diff --git a/src/services/storage-attachment-repo.ts b/src/services/storage-attachment-repo.ts index 1650acf..d0f9ca2 100644 --- a/src/services/storage-attachment-repo.ts +++ b/src/services/storage-attachment-repo.ts @@ -22,10 +22,35 @@ export async function getAttachment(db: D1Database, id: string): Promise { + 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(); + 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 { 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 { + 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 { + 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 { await db.prepare('DELETE FROM attachments WHERE cipher_id = ?').bind(cipherId).run(); } diff --git a/src/services/storage-auth-request-repo.ts b/src/services/storage-auth-request-repo.ts index 8d0676d..93af7b8 100644 --- a/src/services/storage-auth-request-repo.ts +++ b/src/services/storage-auth-request-repo.ts @@ -68,6 +68,11 @@ export async function getAuthRequestById(db: D1Database, id: string): Promise { + const row = await db.prepare(`${AUTH_REQUEST_SELECT} WHERE id = ? AND user_id = ? LIMIT 1`).bind(id, userId).first(); + return row ? mapAuthRequestRow(row) : null; +} + export async function listAuthRequestsByUserId(db: D1Database, userId: string): Promise { const res = await db.prepare(`${AUTH_REQUEST_SELECT} WHERE user_id = ? ORDER BY creation_date DESC`).bind(userId).all(); return (res.results || []).map(mapAuthRequestRow); diff --git a/src/services/storage-cipher-repo.ts b/src/services/storage-cipher-repo.ts index e118cbe..21303cd 100644 --- a/src/services/storage-cipher-repo.ts +++ b/src/services/storage-cipher-repo.ts @@ -107,6 +107,14 @@ export async function getCipher(db: D1Database, id: string): Promise { + const row = await db + .prepare(`SELECT ${selectCipherColumns()} FROM ciphers WHERE id = ? AND user_id = ?`) + .bind(id, userId) + .first(); + return parseCipherRow(row); +} + export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cipher): Promise { 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, diff --git a/src/services/storage-folder-repo.ts b/src/services/storage-folder-repo.ts index 02c7660..72dd251 100644 --- a/src/services/storage-folder-repo.ts +++ b/src/services/storage-folder-repo.ts @@ -19,11 +19,20 @@ export async function getFolder(db: D1Database, id: string): Promise { + 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(); + if (!row) return null; + return mapFolderRow(row); +} + export async function saveFolder(db: D1Database, folder: Folder): Promise { 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(); diff --git a/src/services/storage-send-repo.ts b/src/services/storage-send-repo.ts index 590cb0f..f02a72a 100644 --- a/src/services/storage-send-repo.ts +++ b/src/services/storage-send-repo.ts @@ -40,15 +40,27 @@ export async function getSend(db: D1Database, id: string): Promise return mapSendRow(row); } +export async function getSendForUser(db: D1Database, id: string, userId: string): Promise { + 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(); + if (!row) return null; + return mapSendRow(row); +} + export async function saveSend(db: D1Database, safeBind: SafeBind, send: Send): Promise { 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( diff --git a/src/services/storage.ts b/src/services/storage.ts index 5ab76fa..cbbd232 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -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 { + return findStoredCipherForUser(this.db, id, userId); + } + async saveCipher(cipher: Cipher): Promise { 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 { + return findStoredFolderForUser(this.db, id, userId); + } + async saveFolder(folder: Folder): Promise { await saveStoredFolder(this.db, folder); } @@ -546,6 +561,10 @@ export class StorageService { return findStoredAttachment(this.db, id); } + async getAttachmentForUser(id: string, userId: string): Promise { + return findStoredAttachmentForUser(this.db, id, userId); + } + async saveAttachment(attachment: Attachment): Promise { 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 { + await deleteStoredAttachmentForUser(this.db, id, userId); + } + async bulkDeleteAttachmentsByIds(ids: string[]): Promise { 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 { + await attachStoredAttachmentToCipherForUser(this.db, cipherId, attachmentId, userId); + } + async deleteAllAttachmentsByCipher(cipherId: string): Promise { await deleteStoredAttachmentsByCipher(this.db, cipherId); } @@ -634,6 +661,10 @@ export class StorageService { return findStoredSend(this.db, id); } + async getSendForUser(id: string, userId: string): Promise { + return findStoredSendForUser(this.db, id, userId); + } + async saveSend(send: Send): Promise { 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 { + return findStoredAuthRequestByIdForUser(this.db, id, userId); + } + async listAuthRequestsByUserId(userId: string): Promise { return listStoredAuthRequestsByUserId(this.db, userId); }