a36adb5e9e
Users could upload photos into Protocol "تصوير" albums but had no way to
download them — clicking a photo only opened it inline. This adds per-photo
downloads and a whole-album ZIP download.
Backend:
- objectStorage.ts: new exported extensionForContentType() mapping content
types to file extensions (stored paths are extension-less UUIDs).
- storage.ts: opt-in ?download=1 mode on GET /storage/objects/*path sets
Content-Disposition: attachment (with optional ?name base + derived ext);
default inline behavior and per-entity authz unchanged.
- protocol.ts: new GET /protocol/photo-albums/:id/download streams all album
photos as a ZIP (guarded by requireProtocolAccess), named from the album;
unreadable objects are skipped rather than failing the archive. Empty album
returns 404.
- Added archiver@8 dependency (+@types/archiver dev).
Frontend (protocol.tsx):
- Per-photo download button (attachment, name = album-index) in album detail.
- "تحميل الكل" / "Download all" ZIP button in album header (hidden when empty).
- Added photoDownloadSrc/albumDownloadSrc helpers, Download icon.
- AR/EN locale strings protocol.photos.download / downloadAll.
Notes / deviations:
- archiver v8 is ESM-only with no callable default export; used
`new ZipArchive(...)` instead of `archiver("zip")`. Recorded in memory.
- Could not run authenticated e2e (seed admin password is a secret). Verified:
api-server typecheck + build pass, tx-os typecheck passes, server healthy,
new routes wired (401 auth-required not 404). Pre-existing typecheck errors
in executive-meetings.ts / push.ts are unrelated and untouched.
67 lines
2.2 KiB
JSON
67 lines
2.2 KiB
JSON
{
|
|
"name": "@workspace/api-server",
|
|
"version": "0.0.0",
|
|
"private": true,
|
|
"type": "module",
|
|
"scripts": {
|
|
"dev": "export NODE_ENV=development && pnpm run build && pnpm run start",
|
|
"build": "node ./build.mjs",
|
|
"start": "node --enable-source-maps ./dist/index.mjs",
|
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
"test:wait": "node ./scripts/wait-for-server.mjs",
|
|
"test:run": "node --test 'tests/*.test.mjs' && node --import tsx --test --experimental-test-module-mocks 'tests/*.test.ts' && node --test --test-concurrency=1 'tests/serial/**/*.test.mjs'",
|
|
"test": "node ./scripts/with-server.mjs -- pnpm run test:run",
|
|
"migrate": "pnpm --filter @workspace/db run push && pnpm --filter scripts run seed"
|
|
},
|
|
"dependencies": {
|
|
"@aws-sdk/client-s3": "^3.658.0",
|
|
"@aws-sdk/s3-request-presigner": "^3.658.0",
|
|
"@swc/helpers": "^0.5.21",
|
|
"@workspace/api-zod": "workspace:*",
|
|
"@workspace/db": "workspace:*",
|
|
"arabic-persian-reshaper": "1.0.1",
|
|
"archiver": "^8.0.0",
|
|
"bcryptjs": "^3.0.3",
|
|
"bidi-js": "^1.0.3",
|
|
"connect-pg-simple": "^10.0.0",
|
|
"cookie-parser": "^1.4.7",
|
|
"cors": "^2",
|
|
"drizzle-orm": "catalog:",
|
|
"express": "^5",
|
|
"express-rate-limit": "^8.5.1",
|
|
"express-session": "^1.19.0",
|
|
"nodemailer": "^8.0.7",
|
|
"pdfkit": "^0.18.0",
|
|
"pino": "^9",
|
|
"pino-http": "^10",
|
|
"playwright-core": "^1.59.1",
|
|
"sanitize-html": "^2.17.3",
|
|
"semver": "^7.6.3",
|
|
"socket.io": "^4.8.3",
|
|
"web-push": "^3.6.7",
|
|
"zod": "catalog:"
|
|
},
|
|
"devDependencies": {
|
|
"@types/archiver": "^8.0.0",
|
|
"@types/bcryptjs": "^3.0.0",
|
|
"@types/connect-pg-simple": "^7.0.3",
|
|
"@types/cookie-parser": "^1.4.10",
|
|
"@types/cors": "^2.8.19",
|
|
"@types/express": "^5.0.6",
|
|
"@types/express-session": "^1.19.0",
|
|
"@types/node": "catalog:",
|
|
"@types/nodemailer": "^8.0.0",
|
|
"@types/pdfkit": "^0.17.6",
|
|
"@types/sanitize-html": "^2.16.1",
|
|
"@types/semver": "^7.5.8",
|
|
"@types/web-push": "^3.6.4",
|
|
"esbuild": "^0.27.3",
|
|
"esbuild-plugin-pino": "^2.3.3",
|
|
"pg": "^8.20.0",
|
|
"pino-pretty": "^13",
|
|
"socket.io-client": "^4.8.3",
|
|
"thread-stream": "3.1.0",
|
|
"tsx": "catalog:"
|
|
}
|
|
}
|