143ad9a29d
Fully decoupled Tx OS from the Replit hosted environment so the project can be cloned and run on any Linux VPS with `docker compose up`. Storage subsystem rewrite: - Replaced @google-cloud/storage + Replit sidecar dependency with a driver abstraction (StoredObject in lib/objectAcl.ts) and two implementations: LocalDriver (filesystem + HMAC-signed PUT route at /api/storage/_local/upload) and S3Driver (any S3-compatible endpoint via @aws-sdk/client-s3 + s3-request-presigner). Driver auto-selected by STORAGE_DRIVER / S3_ENDPOINT env vars. - Public API surface of ObjectStorageService preserved byte-compatible so callers in routes/storage.ts and routes/executive-meetings.ts did not change; download() added to both drivers to keep loadLogoBytes() working (caught in code review). - Storage object-authz tests A-L (incl. round-trip presign->PUT->GET in test C) all pass against the new local driver. Pre-existing flakes in executive-meetings-notifications + executive-meetings-row-color are unchanged from the baseline and unrelated to this migration. Infrastructure: - Dockerfile (5 targets: deps/build/api/web/migrate). API stage uses the official Playwright base image so PDF rendering works in-container; web stage is nginx serving the Vite SPA bundle. - docker-compose.yml: postgres + minio + minio-init (creates buckets) + api + web + one-shot migrate runner. Healthchecks on every long-lived service. - docker/nginx.conf: SPA fallback, /api proxy, /api/socket.io websocket upgrade ordering. - .env.example: every runtime env var documented with comments. - README.md replaces replit.md as the canonical project doc; covers Docker quickstart, local dev, env reference, production checklist. - MIGRATION_REPORT.md: file-by-file diff of what changed and why. Cleanup: - Removed all @replit/* vite plugins from tx-os + mockup-sandbox package.json + vite.config.ts + pnpm-workspace.yaml catalog. - Removed @google-cloud/storage and google-auth-library from api-server. - Deleted attached_assets/ (23MB), sedMkjeJm temp file, stale dist/ and *.tsbuildinfo build artefacts, scripts/post-merge.sh. - Stripped Replit references from threat_model.md (now describes the self-hosted topology). - New comprehensive .gitignore: Replit configs (.replit, replit.nix, replit.md), agent state (.local/, .canvas/, .agents/, .cache/, .config/, .upm/), local storage/, .env*, build artefacts. .replit and replit.nix remain on disk (sandbox-protected) but will not ship to GitHub. - scripts/src/seed.ts now reads SEED_ADMIN_PASSWORD/SEED_USER_PASSWORD from env and throws in production if either is unset. Drift from plan: replit.md was deleted (per off-Replit scope) so its "do-not-touch files" preference list is moot. .replit/replit.nix kept on disk only because they are sandbox-protected from edit/delete in this environment, but they are git-ignored so they will not appear in a fresh clone.
43 lines
1.5 KiB
TypeScript
43 lines
1.5 KiB
TypeScript
import { Router, type IRouter, type Request, type Response } from "express";
|
|
import { verifyLocalUploadToken, writeLocalObject, ACTIVE_STORAGE_DRIVER } from "../lib/objectStorage";
|
|
|
|
const router: IRouter = Router();
|
|
|
|
/**
|
|
* PUT /storage/_local/upload?token=<HMAC>
|
|
*
|
|
* Receives the body of a presigned upload issued by the local-FS storage
|
|
* driver. The token is HMAC-signed with LOCAL_STORAGE_SIGNING_SECRET (or
|
|
* SESSION_SECRET as fallback) and carries the bucket+object key + expiry.
|
|
* Only mounted when STORAGE_DRIVER=local (or unset and S3 not configured);
|
|
* when S3 is the active driver, this endpoint refuses to run.
|
|
*/
|
|
router.put("/storage/_local/upload", async (req: Request, res: Response) => {
|
|
if (ACTIVE_STORAGE_DRIVER() !== "local") {
|
|
res.status(404).json({ error: "Not found" });
|
|
return;
|
|
}
|
|
const token = typeof req.query.token === "string" ? req.query.token : null;
|
|
if (!token) {
|
|
res.status(400).json({ error: "Missing token" });
|
|
return;
|
|
}
|
|
const verified = verifyLocalUploadToken(token);
|
|
if (!verified) {
|
|
res.status(403).json({ error: "Invalid or expired upload token" });
|
|
return;
|
|
}
|
|
try {
|
|
const contentType = typeof req.headers["content-type"] === "string"
|
|
? req.headers["content-type"]
|
|
: undefined;
|
|
await writeLocalObject(verified.b, verified.o, req, contentType);
|
|
res.status(200).json({ ok: true });
|
|
} catch (err) {
|
|
req.log.error({ err }, "local storage upload failed");
|
|
res.status(500).json({ error: "Upload failed" });
|
|
}
|
|
});
|
|
|
|
export default router;
|