diff --git a/Channel-Backend/package-lock.json b/Channel-Backend/package-lock.json index 94208ae..6e5a1de 100644 --- a/Channel-Backend/package-lock.json +++ b/Channel-Backend/package-lock.json @@ -8,6 +8,8 @@ "name": "channel-server", "version": "1.0.0", "dependencies": { + "@aws-sdk/client-s3": "^3.1083.0", + "@aws-sdk/s3-request-presigner": "^3.1083.0", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "bcrypt": "^6.0.0", @@ -36,6 +38,331 @@ "typescript": "^5.4.5" } }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1000.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.16.tgz", + "integrity": "sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1083.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1083.0.tgz", + "integrity": "sha512-3YOicHy6qjexsy4rHk5jPvtLgg6Ho1u+ycOWtJcVTAIMlMq32MRtnhllAyFSezhAXeBnnLLtKtAGXim7vV8oog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1000.16", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-node": "^3.972.66", + "@aws-sdk/middleware-sdk-s3": "^3.972.62", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.975.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.1.tgz", + "integrity": "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.0", + "@aws-sdk/xml-builder": "^3.972.34", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.29.2", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.57.tgz", + "integrity": "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.59", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.59.tgz", + "integrity": "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.1.tgz", + "integrity": "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-login": "^3.972.63", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.63.tgz", + "integrity": "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.66", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.66.tgz", + "integrity": "sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-ini": "^3.973.1", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.57.tgz", + "integrity": "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.1.tgz", + "integrity": "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/token-providers": "3.1083.0", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.63.tgz", + "integrity": "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.62", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.62.tgz", + "integrity": "sha512-k8JJwYXVYlOOjWnPZDThQS1xDFJgi5Dokt73qFlDtrZAbdcint5aIdjB9XgJAAQVP5OoqcefQmh1FYXiPpvsvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.31.tgz", + "integrity": "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1083.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1083.0.tgz", + "integrity": "sha512-gVyNRD1b3R2egLMsCR9WGRghNtesNGXEojBOUdCsi/tnR1zlEDyxJCVBCIyvsN2Yxre0+CUt0RSZmtD7SpJ3Fg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.39.tgz", + "integrity": "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.0", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1083.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1083.0.tgz", + "integrity": "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.0.tgz", + "integrity": "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.34.tgz", + "integrity": "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -484,6 +811,87 @@ } } }, + "node_modules/@smithy/core": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.2.tgz", + "integrity": "sha512-DXUk6yU0C1Q1tYvJh1VCtl8QOBcSoZpKwjTPkxT6A4MUQYHvgeKGByL8mrEdxnvhdf9nq5GyzmRb5n/vPgu3Lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.7.tgz", + "integrity": "sha512-UEMLOoA0Fl4uYBxh6l0uN0H6EJe/A89OGeDNTteQeXpJ20BcpfIr4wlCY9pel1jEAUHAxaYwuqrYlrKdXE1GKQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.4.tgz", + "integrity": "sha512-psnst7NZWdAEvJvyW8YZEE7xNVMyLrQFfHtyrVFrxNyy+dKWkQ+rqC6oI5ZhxThpUy9RSfEshgm34zqbOxzsRw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.4.tgz", + "integrity": "sha512-BNTop/fSOptmoVk8g+efwHCofFh37g70OWGAFES1TeAAJja1K5aAI8rTE26ETSc5k8IQuWY2kAIoPla01NgYrA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.3.tgz", + "integrity": "sha512-8qVKKzqh7naF27ePmx0SkUfnGP/wBI9dyaeAmhHvopnbIlItUAmB/e6PkPCU3rRb2v9BY8D4EZXSoydSibatvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.0.tgz", + "integrity": "sha512-aVUabzlBBmY0PfvVgLKQSOGFIL5/7R54JE3uD9a5Ay/jSED61SkuAcCYENNXJzYUvJ1NPrWO0P+rAXHCkbBUKw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -883,6 +1291,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -3114,6 +3528,12 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/Channel-Backend/package.json b/Channel-Backend/package.json index 08832f5..655c6e7 100644 --- a/Channel-Backend/package.json +++ b/Channel-Backend/package.json @@ -9,6 +9,8 @@ "build": "tsc" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1083.0", + "@aws-sdk/s3-request-presigner": "^3.1083.0", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "bcrypt": "^6.0.0", diff --git a/Channel-Backend/src/app.ts b/Channel-Backend/src/app.ts index e900b90..7d5da48 100644 --- a/Channel-Backend/src/app.ts +++ b/Channel-Backend/src/app.ts @@ -1,9 +1,12 @@ -import express, { Express, Request, Response } from 'express'; +import express, { Express, Request, Response, NextFunction } from 'express'; import cors from 'cors'; import helmet from 'helmet'; import dotenv from 'dotenv'; import path from 'path'; import cookieParser from 'cookie-parser'; +import { Readable } from 'stream'; +import { GetObjectCommand } from '@aws-sdk/client-s3'; +import { s3Client, BUCKET_NAME } from './utils/s3'; dotenv.config(); @@ -13,6 +16,8 @@ import assetRoutes from './routes/asset.routes'; import orgRoutes from './routes/organization.routes'; import legalRoutes from './routes/legal.routes'; +import { ensureBucketExists } from './utils/s3'; + const app: Express = express(); const PORT = process.env.PORT || 5000; @@ -32,8 +37,45 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); -// Static uploads directory -app.use('/uploads', express.static(path.join(__dirname, '../../uploads'))); +// Dynamic MinIO object delivery handler +app.get('/uploads/:filename', async (req: Request, res: Response, next: NextFunction) => { + try { + const filename = req.params.filename; + const command = new GetObjectCommand({ + Bucket: BUCKET_NAME, + Key: filename, + }); + + const s3Response = await s3Client.send(command); + + if (s3Response.ContentType) { + res.setHeader('Content-Type', s3Response.ContentType); + } + if (s3Response.ContentLength) { + res.setHeader('Content-Length', s3Response.ContentLength); + } + res.setHeader('Accept-Ranges', 'bytes'); + + if (s3Response.Body instanceof Readable) { + s3Response.Body.pipe(res); + } else if (s3Response.Body) { + const body = s3Response.Body as any; + if (typeof body.pipe === 'function') { + body.pipe(res); + } else { + res.send(await s3Response.Body.transformToByteArray()); + } + } else { + res.status(404).send('Not found'); + } + } catch (err: any) { + if (err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) { + res.status(404).send('File not found in storage'); + } else { + next(err); + } + } +}); // API Routes app.use('/api/v1/auth', authRoutes); @@ -57,6 +99,7 @@ if (process.env.NODE_ENV === 'production') { }); } -app.listen(PORT, () => { +app.listen(PORT, async () => { console.log(`[Server]: API is running on http://localhost:${PORT}`); + await ensureBucketExists(); }); diff --git a/Channel-Backend/src/controllers/asset.controller.ts b/Channel-Backend/src/controllers/asset.controller.ts index 2856b5b..6f7979f 100644 --- a/Channel-Backend/src/controllers/asset.controller.ts +++ b/Channel-Backend/src/controllers/asset.controller.ts @@ -1,4 +1,7 @@ import { Response, NextFunction } from 'express'; +import path from 'path'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { s3Client, BUCKET_NAME } from '../utils/s3'; import { AssetService } from '../services/asset.service'; import { AuthRequest } from '../middleware/auth.middleware'; @@ -21,11 +24,28 @@ export class AssetController { try { shares = JSON.parse(shares); } catch { shares = undefined; } } + let fileUrl = ''; + if (!isUrlAsset && req.file) { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + const filename = uniqueSuffix + path.extname(req.file.originalname); + + await s3Client.send(new PutObjectCommand({ + Bucket: BUCKET_NAME, + Key: filename, + Body: req.file.buffer, + ContentType: req.file.mimetype, + })); + + fileUrl = `/uploads/${filename}`; + } else { + fileUrl = req.body.url; + } + const assetData = { title: req.body.title || (req.file ? req.file.originalname : 'URL Asset'), type: isUrlAsset ? 'url' : req.file!.mimetype, size: isUrlAsset ? 0 : req.file!.size, - url: isUrlAsset ? req.body.url : `/uploads/${req.file!.filename}`, + url: fileUrl, uploadedBy: uploaderId, description: req.body.description || null, categoryId: req.body.categoryId || null, diff --git a/Channel-Backend/src/middleware/upload.middleware.ts b/Channel-Backend/src/middleware/upload.middleware.ts index 9e7bdd9..c9bfdb7 100644 --- a/Channel-Backend/src/middleware/upload.middleware.ts +++ b/Channel-Backend/src/middleware/upload.middleware.ts @@ -1,19 +1,5 @@ import multer from 'multer'; -import path from 'path'; -import fs from 'fs'; -const storage = multer.diskStorage({ - destination: (req, file, cb) => { - const uploadDir = path.join(__dirname, '../../../uploads'); - if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); - } - cb(null, uploadDir); - }, - filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); - cb(null, uniqueSuffix + path.extname(file.originalname)); - }, -}); +const storage = multer.memoryStorage(); export const upload = multer({ storage }); diff --git a/Channel-Backend/src/seed-minio.ts b/Channel-Backend/src/seed-minio.ts new file mode 100644 index 0000000..20125aa --- /dev/null +++ b/Channel-Backend/src/seed-minio.ts @@ -0,0 +1,57 @@ +import fs from 'fs'; +import path from 'path'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { s3Client, BUCKET_NAME, ensureBucketExists } from './utils/s3'; + +async function migrateLocalUploads() { + console.log('[Migration] Starting local uploads migration to MinIO...'); + await ensureBucketExists(); + + const localUploadsDir = path.join(__dirname, '../../uploads'); + if (!fs.existsSync(localUploadsDir)) { + console.log('[Migration] No local uploads directory found.'); + return; + } + + const files = fs.readdirSync(localUploadsDir); + console.log(`[Migration] Found ${files.length} local files.`); + + for (const file of files) { + const filePath = path.join(localUploadsDir, file); + const stat = fs.statSync(filePath); + + if (stat.isFile()) { + console.log(`[Migration] Uploading "${file}" to MinIO bucket "${BUCKET_NAME}"...`); + const fileBuffer = fs.readFileSync(filePath); + + let contentType = 'application/octet-stream'; + if (file.endsWith('.pdf')) { + contentType = 'application/pdf'; + } else if (file.endsWith('.png')) { + contentType = 'image/png'; + } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) { + contentType = 'image/jpeg'; + } else if (file.endsWith('.md')) { + contentType = 'text/markdown'; + } else if (file.endsWith('.docx')) { + contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + } + + try { + await s3Client.send(new PutObjectCommand({ + Bucket: BUCKET_NAME, + Key: file, + Body: fileBuffer, + ContentType: contentType, + })); + console.log(`[Migration] Successfully uploaded "${file}".`); + } catch (err) { + console.error(`[Migration] Failed to upload "${file}":`, err); + } + } + } + + console.log('[Migration] Migration complete.'); +} + +migrateLocalUploads().catch(console.error); diff --git a/Channel-Backend/src/utils/s3.ts b/Channel-Backend/src/utils/s3.ts new file mode 100644 index 0000000..9467cf5 --- /dev/null +++ b/Channel-Backend/src/utils/s3.ts @@ -0,0 +1,36 @@ +import { S3Client, CreateBucketCommand, HeadBucketCommand } from '@aws-sdk/client-s3'; + +const minioEndpoint = process.env.MINIO_ENDPOINT || 'http://localhost:9000'; +const accessKeyId = process.env.MINIO_ACCESS_KEY || 'minio_admin'; +const secretAccessKey = process.env.MINIO_SECRET_KEY || 'minio_password_2026'; +export const BUCKET_NAME = process.env.MINIO_BUCKET || 'secure-assets'; + +export const s3Client = new S3Client({ + endpoint: minioEndpoint, + region: 'us-east-1', + credentials: { + accessKeyId, + secretAccessKey, + }, + forcePathStyle: true, +}); + +export async function ensureBucketExists() { + try { + await s3Client.send(new HeadBucketCommand({ Bucket: BUCKET_NAME })); + console.log(`[MinIO] Bucket "${BUCKET_NAME}" already exists.`); + } catch (err: any) { + // If the bucket doesn't exist, create it + if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) { + console.log(`[MinIO] Bucket "${BUCKET_NAME}" not found. Creating it...`); + try { + await s3Client.send(new CreateBucketCommand({ Bucket: BUCKET_NAME })); + console.log(`[MinIO] Bucket "${BUCKET_NAME}" created successfully.`); + } catch (createErr) { + console.error(`[MinIO] Failed to create bucket "${BUCKET_NAME}":`, createErr); + } + } else { + console.error('[MinIO] HeadBucket error:', err); + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 99eecf4..c6cea6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,5 +14,20 @@ services: - postgres_data:/var/lib/postgresql/data restart: unless-stopped + minio: + image: minio/minio:latest + container_name: channel_minio + ports: + - "9000:9000" + - "9090:9001" + environment: + MINIO_ROOT_USER: minio_admin + MINIO_ROOT_PASSWORD: minio_password_2026 + volumes: + - minio_data:/data + command: server /data --console-address ":9001" + restart: unless-stopped + volumes: postgres_data: + minio_data: diff --git a/run-backend.sh b/run-backend.sh index 4b0d55d..b70f3c3 100755 --- a/run-backend.sh +++ b/run-backend.sh @@ -24,9 +24,9 @@ if ! docker info &> /dev/null; then exit 1 fi -# 3. Spin up PostgreSQL container -info "Starting PostgreSQL container via docker-compose..." -docker compose up -d postgres || docker-compose up -d postgres +# 3. Spin up PostgreSQL & MinIO containers +info "Starting database and storage services via docker-compose..." +docker compose up -d || docker-compose up -d # 4. Wait for PostgreSQL to be ready inside container info "Waiting for database to accept connections..." @@ -50,6 +50,10 @@ PORT=5000 DATABASE_URL="postgresql://pipeline_admin:secure_pipeline_2024@localhost:5433/backend_channel?schema=public" JWT_SECRET="super-secret-jwt-key-2026-world-class" NODE_ENV="development" +MINIO_ENDPOINT="http://localhost:9000" +MINIO_ACCESS_KEY="minio_admin" +MINIO_SECRET_KEY="minio_password_2026" +MINIO_BUCKET="secure-assets" EOT else info "Channel-Backend/.env file already exists."