From 3575398ab532a77d5e1a7f871404e8b5654e655a Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 8 Oct 2025 18:25:51 +1100 Subject: [PATCH 01/14] Add Minio packages and container --- server/docker-compose.yml | 22 +++ server/package.json | 2 + server/pnpm-lock.yaml | 351 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 375 insertions(+) diff --git a/server/docker-compose.yml b/server/docker-compose.yml index a1717e0a3..ee37419fe 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -17,9 +17,31 @@ services: networks: - notangles_network_new + minio: + container_name: notangles-images + hostname: notangles_usr_images + restart: always # Is this necessary for pfps? + image: minio/minio:latest + ports: + - "9000:9000" + - "9001:9001" + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + volumes: + - minio:/data + command: server /data --console-address ":9001" + networks: + - notangles_usr_images + volumes: postgres: name: server-new + minio: + name: usr-images + networks: notangles_network_new: driver: bridge + notangles_usr_images: + driver: bridge diff --git a/server/package.json b/server/package.json index 9ccf1186c..2717caf0c 100644 --- a/server/package.json +++ b/server/package.json @@ -33,6 +33,8 @@ "graphql": "16.11.0", "graphql-request": "7.2.0", "graphql-tag": "2.12.6", + "minio": "^8.0.5", + "nestjs-minio-client": "^2.2.0", "openid-client": "6.6.2", "passport": "0.7.0", "passport-custom": "1.1.1", diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index cad18c966..8f2b09c56 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -38,6 +38,12 @@ importers: graphql-tag: specifier: 2.12.6 version: 2.12.6(graphql@16.11.0) + minio: + specifier: ^8.0.5 + version: 8.0.5 + nestjs-minio-client: + specifier: ^2.2.0 + version: 2.2.0(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) openid-client: specifier: 6.6.2 version: 6.6.2 @@ -2077,6 +2083,9 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2215,6 +2224,10 @@ packages: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -2275,6 +2288,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + body-parser@2.2.0: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} @@ -2289,6 +2305,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + browserslist@4.24.5: resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2304,6 +2323,10 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2330,6 +2353,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -2577,6 +2604,10 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -2607,6 +2638,10 @@ packages: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2822,6 +2857,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2887,6 +2925,10 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-xml-parser@4.5.3: + resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -2937,6 +2979,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -2960,6 +3006,10 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -3152,6 +3202,9 @@ packages: resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -3258,13 +3311,25 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + is-absolute@1.0.0: resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} engines: {node: '>=0.10.0'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3277,6 +3342,10 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -3299,6 +3368,10 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + is-relative@1.0.0: resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} engines: {node: '>=0.10.0'} @@ -3311,6 +3384,10 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unc-path@1.0.0: resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} engines: {node: '>=0.10.0'} @@ -3555,6 +3632,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stream@1.0.0: + resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} + json-to-pretty-yaml@1.2.2: resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} engines: {node: '>= 0.2.0'} @@ -3769,6 +3849,14 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minio@7.1.3: + resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} + engines: {node: ^16 || ^18 || >=20} + + minio@8.0.5: + resolution: {integrity: sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==} + engines: {node: ^16 || ^18 || >=20} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -3809,6 +3897,12 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-minio-client@2.2.0: + resolution: {integrity: sha512-mz1vfJq/7YfSyVCIeZwOCfIfBz+msI9QynHS2QGO9GB+tVNnQOYta8PxFsH9tMxN7gNrjrf5jXsEIpgBB1oTeA==} + peerDependencies: + '@nestjs/common': '>=9.0.0' + '@nestjs/core': '>=9.0.0' + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -4064,6 +4158,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4109,6 +4207,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4142,6 +4244,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -4222,9 +4327,16 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} @@ -4274,6 +4386,10 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -4355,6 +4471,10 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + sponge-case@1.0.1: resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} @@ -4369,6 +4489,12 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -4376,6 +4502,10 @@ packages: streamx@2.22.0: resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==} + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + string-env-interpolation@1.0.1: resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} @@ -4421,6 +4551,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + strtok3@10.2.2: resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==} engines: {node: '>=18'} @@ -4495,6 +4628,9 @@ packages: text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -4709,6 +4845,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -4734,6 +4873,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -4769,6 +4911,10 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4809,6 +4955,17 @@ packages: utf-8-validate: optional: true + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7311,6 +7468,9 @@ snapshots: '@xtuc/long@4.2.2': {} + '@zxing/text-encoding@0.9.0': + optional: true + accepts@2.0.0: dependencies: mime-types: 3.0.1 @@ -7421,6 +7581,10 @@ snapshots: auto-bind@4.0.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + b4a@1.6.7: {} babel-jest@30.0.4(@babel/core@7.27.4): @@ -7538,6 +7702,10 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + body-parser@2.2.0: dependencies: bytes: 3.1.2 @@ -7565,6 +7733,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-or-node@2.1.1: {} + browserslist@4.24.5: dependencies: caniuse-lite: 1.0.30001718 @@ -7582,6 +7752,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -7612,6 +7784,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7843,6 +8022,8 @@ snapshots: decamelize@1.2.0: {} + decode-uri-component@0.2.2: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -7861,6 +8042,12 @@ snapshots: defer-to-connect@2.0.1: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + delayed-stream@1.0.0: {} depd@2.0.0: {} @@ -8059,6 +8246,8 @@ snapshots: etag@1.8.1: {} + eventemitter3@5.0.1: {} + events@3.3.0: {} execa@5.1.1: @@ -8175,6 +8364,10 @@ snapshots: fast-uri@3.0.6: {} + fast-xml-parser@4.5.3: + dependencies: + strnum: 1.1.2 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -8242,6 +8435,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + filter-obj@1.1.0: {} + finalhandler@2.1.0: dependencies: debug: 4.4.1 @@ -8274,6 +8469,10 @@ snapshots: flatted@3.3.3: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -8490,6 +8689,10 @@ snapshots: has-own-prop@2.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -8605,19 +8808,35 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.2.0: {} + is-absolute@1.0.0: dependencies: is-relative: 1.0.0 is-windows: 1.0.2 + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-arrayish@0.2.1: {} + is-callable@1.2.7: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -8634,6 +8853,13 @@ snapshots: is-promise@4.0.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + is-relative@1.0.0: dependencies: is-unc-path: 1.0.0 @@ -8642,6 +8868,10 @@ snapshots: is-stream@4.0.1: {} + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + is-unc-path@1.0.0: dependencies: unc-path-regex: 0.1.2 @@ -9073,6 +9303,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stream@1.0.0: {} + json-to-pretty-yaml@1.2.2: dependencies: remedial: 1.0.8 @@ -9249,6 +9481,40 @@ snapshots: minimist@1.2.8: {} + minio@7.1.3: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 0.2.13 + fast-xml-parser: 4.5.3 + ipaddr.js: 2.2.0 + json-stream: 1.0.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + through2: 4.0.2 + web-encoding: 1.1.5 + xml: 1.0.1 + xml2js: 0.5.0 + + minio@8.0.5: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 1.0.0 + eventemitter3: 5.0.1 + fast-xml-parser: 4.5.3 + ipaddr.js: 2.2.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + stream-json: 1.9.1 + through2: 4.0.2 + web-encoding: 1.1.5 + xml2js: 0.5.0 + minipass@7.1.2: {} mkdirp@0.5.6: @@ -9281,6 +9547,14 @@ snapshots: neo-async@2.6.2: {} + nestjs-minio-client@2.2.0(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3): + dependencies: + '@nestjs/common': 11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.3(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + minio: 7.1.3 + reflect-metadata: 0.1.14 + rxjs: 7.8.2 + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -9514,6 +9788,8 @@ snapshots: pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -9552,6 +9828,13 @@ snapshots: dependencies: side-channel: 1.1.0 + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + queue-microtask@1.2.3: {} quick-lru@5.1.1: {} @@ -9581,6 +9864,8 @@ snapshots: readdirp@4.1.2: {} + reflect-metadata@0.1.14: {} + reflect-metadata@0.2.2: {} relay-runtime@12.0.0: @@ -9654,8 +9939,16 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} + sax@1.4.1: {} + schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 @@ -9722,6 +10015,15 @@ snapshots: set-blocking@2.0.0: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setimmediate@1.0.5: {} setprototypeof@1.2.0: {} @@ -9809,6 +10111,8 @@ snapshots: source-map@0.7.4: {} + split-on-first@1.1.0: {} + sponge-case@1.0.1: dependencies: tslib: 2.8.1 @@ -9821,6 +10125,12 @@ snapshots: statuses@2.0.1: {} + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + streamsearch@1.1.0: {} streamx@2.22.0: @@ -9830,6 +10140,8 @@ snapshots: optionalDependencies: bare-events: 2.5.4 + strict-uri-encode@2.0.0: {} + string-env-interpolation@1.0.1: {} string-length@4.0.2: @@ -9874,6 +10186,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@1.1.2: {} + strtok3@10.2.2: dependencies: '@tokenizer/token': 0.3.0 @@ -9965,6 +10279,10 @@ snapshots: dependencies: b4a: 1.6.7 + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + through@2.3.8: {} timeout-signal@2.0.0: {} @@ -10181,6 +10499,14 @@ snapshots: util-deprecate@1.0.2: {} + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + utils-merge@1.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -10206,6 +10532,12 @@ snapshots: dependencies: defaults: 1.0.4 + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + web-streams-polyfill@3.3.3: {} webidl-conversions@3.0.1: {} @@ -10253,6 +10585,16 @@ snapshots: which-module@2.0.1: {} + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -10286,6 +10628,15 @@ snapshots: ws@8.18.2: {} + xml2js@0.5.0: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xml@1.0.1: {} + + xmlbuilder@11.0.1: {} + xtend@4.0.2: {} y18n@4.0.3: {} From e72e304f6da260532496ae5624dc6c75581bd16d Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 8 Oct 2025 18:27:35 +1100 Subject: [PATCH 02/14] Add file-upload conponents --- server/src/file-upload/file-upload.controller.ts | 12 ++++++++++++ server/src/file-upload/file-upload.module.ts | 11 +++++++++++ server/src/file-upload/file-upload.service.ts | 16 ++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 server/src/file-upload/file-upload.controller.ts create mode 100644 server/src/file-upload/file-upload.module.ts create mode 100644 server/src/file-upload/file-upload.service.ts diff --git a/server/src/file-upload/file-upload.controller.ts b/server/src/file-upload/file-upload.controller.ts new file mode 100644 index 000000000..5aa72156b --- /dev/null +++ b/server/src/file-upload/file-upload.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Post, Body } from '@nestjs/common'; +import { FileUploadService } from './file-upload.service'; + +@Controller('file-upload') +export class FileUploadController { + constructor(private fileUploadService: FileUploadService) {} + + @Post('single') + async uploadSingle(@Body('image') image: string) { + return await this.fileUploadService.uploadSingle(image); + } +} diff --git a/server/src/file-upload/file-upload.module.ts b/server/src/file-upload/file-upload.module.ts new file mode 100644 index 000000000..1ddca01b2 --- /dev/null +++ b/server/src/file-upload/file-upload.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UserModule } from 'src/user/user.module'; +import { FileUploadService } from './file-upload.service'; +import { FileUploadController } from './file-upload.controller'; + +@Module({ + imports: [UserModule], + providers: [FileUploadService], + controllers: [FileUploadController], +}) +export class FileUploadModule {} diff --git a/server/src/file-upload/file-upload.service.ts b/server/src/file-upload/file-upload.service.ts new file mode 100644 index 000000000..b9335f91d --- /dev/null +++ b/server/src/file-upload/file-upload.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { UserService } from 'src/user/user.service'; + +@Injectable() +export class FileUploadService { + constructor(private minioClientService: UserService) {} + + async uploadSingle(image: string) { + const uploadedImage = await this.minioClientService.uploadImage(image); + + return { + imageUrl: uploadedImage.url, + message: 'Successfully uploaded to MinIO', + }; + } +} From 270677073ca71268d64835a96ecdccef132b97c1 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 8 Oct 2025 18:28:24 +1100 Subject: [PATCH 03/14] Integrate pfp upload implementation --- server/src/app.module.ts | 2 ++ server/src/user/user.module.ts | 11 +++++++ server/src/user/user.service.ts | 55 +++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 6654b8744..6e612241d 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config'; import config from './config'; import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module'; +import { FileUploadModule } from './file-upload/file-upload.module'; import { TimetableModule } from './timetable/timetable.module'; @Module({ @@ -17,6 +18,7 @@ import { TimetableModule } from './timetable/timetable.module'; UserModule, TimetableModule, AuthModule, + FileUploadModule, ], controllers: [AppController], providers: [AppService], diff --git a/server/src/user/user.module.ts b/server/src/user/user.module.ts index e93cb4341..d44d8a580 100644 --- a/server/src/user/user.module.ts +++ b/server/src/user/user.module.ts @@ -3,9 +3,20 @@ import { GraphqlService } from 'src/graphql/graphql.service'; import { PrismaService } from 'src/prisma/prisma.service'; import { UserService } from './user.service'; import { UserController } from './user.controller'; +import { MinioModule } from 'nestjs-minio-client'; @Module({ + imports: [ + MinioModule.register({ + endPoint: process.env.MINIO_ENDPOINT ?? 'localhost', + port: 9000, + useSSL: false, + accessKey: process.env.MINIO_ACCESSKEY!, + secretKey: process.env.MINIO_SECRETKEY!, + }), + ], providers: [UserService, PrismaService, GraphqlService], controllers: [UserController], + exports: [UserService], }) export class UserModule {} diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 2df6f138a..fcc627422 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -1,10 +1,15 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; import { UserInfo, UserSettings } from './types'; +import { MinioService } from 'nestjs-minio-client'; +import * as crypto from 'crypto'; @Injectable({}) export class UserService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly minio: MinioService, + ) {} async deleteUser(userId: string): Promise { await this.prisma.user.delete({ @@ -29,16 +34,62 @@ export class UserService { }; } + async uploadImage(image: string): Promise<{ url: string }> { + // Basic base64 string validation (does not check for file type) + const base64regex = + /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; + + if (!base64regex.test(image)) { + throw new Error('Invalid string'); + } + + const temp_filename = Date.now().toString(); + const hash = crypto.createHash('md5'); + hash.update(temp_filename); + const hashedFileName = hash.digest('hex'); + const filename = hashedFileName; + + const buffer = Buffer.from(image, 'base64'); + + const bucketName = process.env.MINIO_BUCKET_NAME; + + try { + await this.minio.client.putObject(bucketName!, filename, buffer); + } catch (err) { + throw new Error('Error uploading file to MinIO: ' + err); + } + + return { + url: `${process.env.MINIO_PUBLIC_URL}/${bucketName}/${filename}`, + }; + } + async setProfilePicture( userId: string, profilePictureUrl: string, ): Promise { + if (!profilePictureUrl) { + // null is passed in, set the field to null (client will show the default image) + await this.prisma.user.update({ + where: { + id: userId, + }, + data: { + profilePictureUrl: null, + }, + }); + return; + } + + const minioImageLink = await this.uploadImage(profilePictureUrl); + await this.prisma.user.update({ where: { id: userId, }, data: { - profilePictureUrl, + // consider for empty string being passed in -> set to default image + profilePictureUrl: minioImageLink.url, }, }); } From c35a512b6c4605f6ee82cd0cfea6895cee9bfcd6 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 15 Oct 2025 18:21:34 +1100 Subject: [PATCH 04/14] Resolve routing errors --- server/src/auth/auth.module.ts | 8 ++++++++ server/src/user/user.service.ts | 11 ++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index aedc63d95..b2f4e3cf6 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -10,6 +10,7 @@ import { GuestStrategy } from './guest.strategy'; import { getConfig, OidcStrategy } from './oidc.strategy'; import { SessionSerializer } from './session.serializer'; import { GraphqlService } from 'src/graphql/graphql.service'; +import { MinioModule } from 'nestjs-minio-client'; const OidcStrategyFactory = { provide: 'OidcStrategy', @@ -22,6 +23,13 @@ const OidcStrategyFactory = { @Module({ imports: [ + MinioModule.register({ + endPoint: process.env.MINIO_ENDPOINT ?? 'localhost', + port: 9000, + useSSL: false, + accessKey: process.env.MINIO_ACCESSKEY!, + secretKey: process.env.MINIO_SECRETKEY!, + }), PassportModule.register({ session: true, defaultStrategy: 'oidc' }), ], controllers: [AuthController], diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index fcc627422..f89f6dd58 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -35,13 +35,14 @@ export class UserService { } async uploadImage(image: string): Promise<{ url: string }> { + // This is not workinggg lol // Basic base64 string validation (does not check for file type) - const base64regex = - /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; + // const base64regex = + // /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; - if (!base64regex.test(image)) { - throw new Error('Invalid string'); - } + // if (!base64regex.test(image)) { + // throw new Error('Invalid string'); + // } const temp_filename = Date.now().toString(); const hash = crypto.createHash('md5'); From f918030a558198b2a24ae23e90334de6852d75fe Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 22 Oct 2025 15:18:39 +1100 Subject: [PATCH 05/14] Change minio docker image to the version where the admin controls still exist in console --- server/docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/docker-compose.yml b/server/docker-compose.yml index ee37419fe..eb3455e4f 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -21,10 +21,10 @@ services: container_name: notangles-images hostname: notangles_usr_images restart: always # Is this necessary for pfps? - image: minio/minio:latest + image: minio/minio:RELEASE.2025-04-22T22-12-26Z ports: - - "9000:9000" - - "9001:9001" + - '9000:9000' + - '9001:9001' environment: - MINIO_ROOT_USER=${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} From 3340252119ac063fff8d739719e25338928e5abb Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 22 Oct 2025 15:20:48 +1100 Subject: [PATCH 06/14] Add bucket check and fix key typos --- server/src/auth/auth.module.ts | 4 ++-- server/src/user/user.module.ts | 4 ++-- server/src/user/user.service.ts | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/auth/auth.module.ts b/server/src/auth/auth.module.ts index b2f4e3cf6..ed71d22ca 100644 --- a/server/src/auth/auth.module.ts +++ b/server/src/auth/auth.module.ts @@ -27,8 +27,8 @@ const OidcStrategyFactory = { endPoint: process.env.MINIO_ENDPOINT ?? 'localhost', port: 9000, useSSL: false, - accessKey: process.env.MINIO_ACCESSKEY!, - secretKey: process.env.MINIO_SECRETKEY!, + accessKey: process.env.MINIO_ACCESS_KEY!, + secretKey: process.env.MINIO_SECRET_KEY!, }), PassportModule.register({ session: true, defaultStrategy: 'oidc' }), ], diff --git a/server/src/user/user.module.ts b/server/src/user/user.module.ts index d44d8a580..0314ed661 100644 --- a/server/src/user/user.module.ts +++ b/server/src/user/user.module.ts @@ -11,8 +11,8 @@ import { MinioModule } from 'nestjs-minio-client'; endPoint: process.env.MINIO_ENDPOINT ?? 'localhost', port: 9000, useSSL: false, - accessKey: process.env.MINIO_ACCESSKEY!, - secretKey: process.env.MINIO_SECRETKEY!, + accessKey: process.env.MINIO_ACCESS_KEY!, + secretKey: process.env.MINIO_SECRET_KEY!, }), ], providers: [UserService, PrismaService, GraphqlService], diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index f89f6dd58..8922ea463 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -54,6 +54,12 @@ export class UserService { const bucketName = process.env.MINIO_BUCKET_NAME; + try { + const bucketExists = await this.minio.client.bucketExists(bucketName!); + } catch (err) { + throw new Error('Error checking if bucket exists: ' + err); + } + try { await this.minio.client.putObject(bucketName!, filename, buffer); } catch (err) { From a148c3a4299666f761cb231036d23c7b53d77026 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 22 Oct 2025 15:26:33 +1100 Subject: [PATCH 07/14] Remove unused constant --- server/src/user/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 8922ea463..123ef85d9 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -55,7 +55,7 @@ export class UserService { const bucketName = process.env.MINIO_BUCKET_NAME; try { - const bucketExists = await this.minio.client.bucketExists(bucketName!); + await this.minio.client.bucketExists(bucketName!); } catch (err) { throw new Error('Error checking if bucket exists: ' + err); } From dbb4a4d3a7d5837f9eb2a7136c37ad11be0cb513 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 29 Oct 2025 19:16:16 +1100 Subject: [PATCH 08/14] Add base64 encoding validation and upload image instead of buffer --- server/src/user/user.service.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 123ef85d9..59175d800 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -35,14 +35,13 @@ export class UserService { } async uploadImage(image: string): Promise<{ url: string }> { - // This is not workinggg lol - // Basic base64 string validation (does not check for file type) - // const base64regex = - // /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; + // expected image format: data:;base64, + const validMimeTypes = ['image/jpeg', 'image/png', 'image/jpg']; + const mimeType = image.match(/[^:]\w+\/[\w-+\d.]+(?=;|,)/); - // if (!base64regex.test(image)) { - // throw new Error('Invalid string'); - // } + if (!mimeType || !validMimeTypes.includes(mimeType[0])) { + throw new Error('Invalid image mime type'); + } const temp_filename = Date.now().toString(); const hash = crypto.createHash('md5'); @@ -50,8 +49,6 @@ export class UserService { const hashedFileName = hash.digest('hex'); const filename = hashedFileName; - const buffer = Buffer.from(image, 'base64'); - const bucketName = process.env.MINIO_BUCKET_NAME; try { @@ -61,7 +58,7 @@ export class UserService { } try { - await this.minio.client.putObject(bucketName!, filename, buffer); + await this.minio.client.putObject(bucketName!, filename, image); } catch (err) { throw new Error('Error uploading file to MinIO: ' + err); } @@ -95,7 +92,6 @@ export class UserService { id: userId, }, data: { - // consider for empty string being passed in -> set to default image profilePictureUrl: minioImageLink.url, }, }); From 308181f1ca97da4b85a2ebef86862c138be9309b Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 5 Nov 2025 14:46:13 +1100 Subject: [PATCH 09/14] Change null expectation in setProfilePicture to empty string --- server/src/user/user.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 4d049702c..802866480 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -74,13 +74,13 @@ export class UserService { profilePictureUrl: string, ): Promise { if (!profilePictureUrl) { - // null is passed in, set the field to null (client will show the default image) + // empty string is passed in, set the field to null (client will show the default image) await this.prisma.user.update({ where: { id: userId, }, data: { - profilePictureUrl: null, + profilePictureUrl: '', }, }); return; From 9caac050412dcc3b091e23771464b167e036f689 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 5 Nov 2025 18:58:04 +1100 Subject: [PATCH 10/14] Add remove old pfp before setting new pfp --- server/src/user/user.service.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 802866480..e9351ce56 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -73,6 +73,28 @@ export class UserService { userId: string, profilePictureUrl: string, ): Promise { + const data = await this.prisma.user.findUniqueOrThrow({ + where: { + id: userId, + }, + select: { + profilePictureUrl: true, + }, + }); + + const bucketName = process.env.MINIO_BUCKET_NAME; + + if (data.profilePictureUrl) { + try { + await this.minio.client.removeObject( + bucketName!, + data.profilePictureUrl, + ); + } catch (err) { + throw new Error('Error removing object from MinIO: ' + err); + } + } + if (!profilePictureUrl) { // empty string is passed in, set the field to null (client will show the default image) await this.prisma.user.update({ From e4a00542d0ca6c1a50c327fe863de5cde0fdb9b9 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 5 Nov 2025 11:42:18 +1100 Subject: [PATCH 11/14] Fix backend GraphQL year field --- server/src/auth/auth.service.ts | 5 +++-- server/src/graphql/graphql.service.ts | 2 +- server/src/graphql/queries.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 9c6d9d696..2595ec321 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -35,7 +35,7 @@ export class AuthService { if (provider === undefined || subject === undefined) { throw new Error('Provider and subject must be defined for auth users'); } - const currentYear = new Date().getFullYear().toString(); + const currentYear = new Date().getFullYear(); const { availableTerms } = await this.graphql.getAvailableTermsFrom(currentYear); if (availableTerms.length === 0) { @@ -75,7 +75,7 @@ export class AuthService { } private async createGuest(params: OnboardParams): Promise { - const currentYear = new Date().getFullYear().toString(); + const currentYear = new Date().getFullYear(); const { availableTerms } = await this.graphql.getAvailableTermsFrom(currentYear); if (availableTerms.length === 0) { @@ -96,6 +96,7 @@ export class AuthService { name: this.TIMETABLE_DEFAULT_NAME, year, term, + primary: true, }; }), }, diff --git a/server/src/graphql/graphql.service.ts b/server/src/graphql/graphql.service.ts index 1d0168d36..78779eae5 100644 --- a/server/src/graphql/graphql.service.ts +++ b/server/src/graphql/graphql.service.ts @@ -29,7 +29,7 @@ export class GraphqlService { } async getAvailableTermsFrom( - currentYear: string = new Date().getFullYear().toString(), + currentYear: number = new Date().getFullYear(), ): Promise<{ availableTerms: string[] }> { const result = await this.sdk.GetAvailableTerms({ currentYear: currentYear, diff --git a/server/src/graphql/queries.ts b/server/src/graphql/queries.ts index 52f8c31f8..3adeb9e7c 100644 --- a/server/src/graphql/queries.ts +++ b/server/src/graphql/queries.ts @@ -22,7 +22,7 @@ export const CLASS_DETAILS = gql(` `); export const GET_AVAILABLE_TERMS = gql(` - query GetAvailableTerms($currentYear: String!) { + query GetAvailableTerms($currentYear: Int!) { classes( where: {term: {_in: ["T1", "T2", "T3", "U1"]}, year: {_gte: $currentYear}} distinct_on: offering_period From 9abbc0cc56251206428203003cc3f1f4c6dfd402 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 5 Nov 2025 19:12:42 +1100 Subject: [PATCH 12/14] Add extracting file name from url --- server/src/user/user.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index e9351ce56..2c1c7cb55 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -83,13 +83,13 @@ export class UserService { }); const bucketName = process.env.MINIO_BUCKET_NAME; + const fullUrl = data.profilePictureUrl; + const temp = fullUrl?.lastIndexOf('/'); + const fileName = fullUrl?.substring(temp! + 1); if (data.profilePictureUrl) { try { - await this.minio.client.removeObject( - bucketName!, - data.profilePictureUrl, - ); + await this.minio.client.removeObject(bucketName!, fileName!); } catch (err) { throw new Error('Error removing object from MinIO: ' + err); } From 4c237e0436db433bed74f9a10018308b6c594254 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Wed, 5 Nov 2025 19:15:28 +1100 Subject: [PATCH 13/14] Fix style issues --- server/src/user/user.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 2c1c7cb55..c411c8d04 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -44,11 +44,11 @@ export class UserService { throw new Error('Invalid image mime type'); } - const temp_filename = Date.now().toString(); + const tempFileName = Date.now().toString(); const hash = crypto.createHash('md5'); - hash.update(temp_filename); + hash.update(tempFileName); const hashedFileName = hash.digest('hex'); - const filename = hashedFileName; + const fileName = hashedFileName; const bucketName = process.env.MINIO_BUCKET_NAME; @@ -59,13 +59,13 @@ export class UserService { } try { - await this.minio.client.putObject(bucketName!, filename, image); + await this.minio.client.putObject(bucketName!, fileName, image); } catch (err) { throw new Error('Error uploading file to MinIO: ' + err); } return { - url: `${process.env.MINIO_PUBLIC_URL}/${bucketName}/${filename}`, + url: `${process.env.MINIO_PUBLIC_URL}/${bucketName}/${fileName}`, }; } From 3ca1afa2480a80a43e6ac7300503de60f518c047 Mon Sep 17 00:00:00 2001 From: Tet Nay Lin Date: Thu, 6 Nov 2025 02:01:42 +1100 Subject: [PATCH 14/14] Remove file-upload --- server/src/app.module.ts | 2 -- server/src/file-upload/file-upload.controller.ts | 12 ------------ server/src/file-upload/file-upload.module.ts | 11 ----------- server/src/file-upload/file-upload.service.ts | 16 ---------------- 4 files changed, 41 deletions(-) delete mode 100644 server/src/file-upload/file-upload.controller.ts delete mode 100644 server/src/file-upload/file-upload.module.ts delete mode 100644 server/src/file-upload/file-upload.service.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 6e612241d..6654b8744 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -5,7 +5,6 @@ import { ConfigModule } from '@nestjs/config'; import config from './config'; import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module'; -import { FileUploadModule } from './file-upload/file-upload.module'; import { TimetableModule } from './timetable/timetable.module'; @Module({ @@ -18,7 +17,6 @@ import { TimetableModule } from './timetable/timetable.module'; UserModule, TimetableModule, AuthModule, - FileUploadModule, ], controllers: [AppController], providers: [AppService], diff --git a/server/src/file-upload/file-upload.controller.ts b/server/src/file-upload/file-upload.controller.ts deleted file mode 100644 index 5aa72156b..000000000 --- a/server/src/file-upload/file-upload.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Post, Body } from '@nestjs/common'; -import { FileUploadService } from './file-upload.service'; - -@Controller('file-upload') -export class FileUploadController { - constructor(private fileUploadService: FileUploadService) {} - - @Post('single') - async uploadSingle(@Body('image') image: string) { - return await this.fileUploadService.uploadSingle(image); - } -} diff --git a/server/src/file-upload/file-upload.module.ts b/server/src/file-upload/file-upload.module.ts deleted file mode 100644 index 1ddca01b2..000000000 --- a/server/src/file-upload/file-upload.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserModule } from 'src/user/user.module'; -import { FileUploadService } from './file-upload.service'; -import { FileUploadController } from './file-upload.controller'; - -@Module({ - imports: [UserModule], - providers: [FileUploadService], - controllers: [FileUploadController], -}) -export class FileUploadModule {} diff --git a/server/src/file-upload/file-upload.service.ts b/server/src/file-upload/file-upload.service.ts deleted file mode 100644 index b9335f91d..000000000 --- a/server/src/file-upload/file-upload.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { UserService } from 'src/user/user.service'; - -@Injectable() -export class FileUploadService { - constructor(private minioClientService: UserService) {} - - async uploadSingle(image: string) { - const uploadedImage = await this.minioClientService.uploadImage(image); - - return { - imageUrl: uploadedImage.url, - message: 'Successfully uploaded to MinIO', - }; - } -}