feat: move logo and avatars to local upload

This commit is contained in:
Daniel Luiz Alves
2025-03-14 13:14:30 -03:00
parent fb4ef5438d
commit 168eae3294
22 changed files with 571 additions and 154 deletions

View File

@@ -10,3 +10,4 @@ MINIO_REGION="sa-east-1"
MINIO_BUCKET_NAME="files"
PORT=3333
BASE_URL=http://localhost:3333

View File

@@ -1,4 +1,5 @@
services:
minio:
image: minio/minio:latest
container_name: minio
@@ -53,10 +54,4 @@ services:
retries: 5
volumes:
minio_data:
postgres_data:
networks:
default:
name: palmr-network
driver: bridge

View File

@@ -21,14 +21,17 @@
"@fastify/cors": "^10.0.2",
"@fastify/jwt": "^9.0.3",
"@fastify/multipart": "^9.0.3",
"@fastify/static": "^8.1.1",
"@fastify/swagger": "^9.4.2",
"@fastify/swagger-ui": "^5.2.1",
"@prisma/client": "^6.3.1",
"@scalar/fastify-api-reference": "^1.25.116",
"@types/multer": "^1.4.12",
"bcryptjs": "^2.4.3",
"fastify": "^5.2.1",
"fastify-type-provider-zod": "^4.0.2",
"minio": "^8.0.4",
"multer": "1.4.5-lts.1",
"nodemailer": "^6.10.0",
"sharp": "^0.33.5",
"zod": "^3.24.1"
@@ -37,6 +40,7 @@
"@eslint/js": "^9.19.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^5.0.0",
"@types/node": "^22.13.4",
"@types/nodemailer": "^6.4.17",
"eslint": "^9.19.0",

View File

@@ -20,6 +20,9 @@ importers:
'@fastify/multipart':
specifier: ^9.0.3
version: 9.0.3
'@fastify/static':
specifier: ^8.1.1
version: 8.1.1
'@fastify/swagger':
specifier: ^9.4.2
version: 9.4.2
@@ -32,6 +35,9 @@ importers:
'@scalar/fastify-api-reference':
specifier: ^1.25.116
version: 1.25.122
'@types/multer':
specifier: ^1.4.12
version: 1.4.12
bcryptjs:
specifier: ^2.4.3
version: 2.4.3
@@ -44,6 +50,9 @@ importers:
minio:
specifier: ^8.0.4
version: 8.0.4
multer:
specifier: 1.4.5-lts.1
version: 1.4.5-lts.1
nodemailer:
specifier: ^6.10.0
version: 6.10.0
@@ -63,6 +72,9 @@ importers:
'@types/bcryptjs':
specifier: ^2.4.6
version: 2.4.6
'@types/express':
specifier: ^5.0.0
version: 5.0.0
'@types/node':
specifier: ^22.13.4
version: 22.13.4
@@ -620,21 +632,54 @@ packages:
'@types/bcryptjs@2.4.6':
resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
'@types/body-parser@1.19.5':
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
'@types/express-serve-static-core@5.0.6':
resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==}
'@types/express@5.0.0':
resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==}
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/multer@1.4.12':
resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==}
'@types/node@22.13.4':
resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==}
'@types/nodemailer@6.4.17':
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
'@types/qs@6.9.18':
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/send@0.17.4':
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
'@types/serve-static@1.15.7':
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
'@typescript-eslint/eslint-plugin@8.24.1':
resolution: {integrity: sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -735,6 +780,9 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
append-field@1.0.0:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@@ -815,6 +863,13 @@ packages:
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==}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -852,6 +907,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
@@ -860,6 +919,9 @@ packages:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
@@ -1407,6 +1469,9 @@ packages:
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
engines: {node: '>= 0.4'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@@ -1486,6 +1551,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -1532,12 +1601,20 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
mnemonist@0.39.8:
resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
multer@1.4.5-lts.1:
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
engines: {node: '>= 6.0.0'}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -1545,6 +1622,10 @@ packages:
resolution: {integrity: sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==}
engines: {node: '>=6.0.0'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -1657,6 +1738,9 @@ packages:
typescript:
optional: true
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
process-warning@4.0.1:
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
@@ -1674,6 +1758,9 @@ packages:
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -1724,6 +1811,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -1837,6 +1927,10 @@ packages:
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'}
strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'}
@@ -1861,6 +1955,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -1944,6 +2041,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -1960,6 +2061,9 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript-eslint@8.24.1:
resolution: {integrity: sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2525,12 +2629,43 @@ snapshots:
'@types/bcryptjs@2.4.6': {}
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.13.4
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.13.4
'@types/estree@1.0.6': {}
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 22.13.4
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
'@types/express@5.0.0':
dependencies:
'@types/body-parser': 1.19.5
'@types/express-serve-static-core': 5.0.6
'@types/qs': 6.9.18
'@types/serve-static': 1.15.7
'@types/http-errors@2.0.4': {}
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
'@types/mime@1.3.5': {}
'@types/multer@1.4.12':
dependencies:
'@types/express': 5.0.0
'@types/node@22.13.4':
dependencies:
undici-types: 6.20.0
@@ -2539,6 +2674,21 @@ snapshots:
dependencies:
'@types/node': 22.13.4
'@types/qs@6.9.18': {}
'@types/range-parser@1.2.7': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.13.4
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 22.13.4
'@types/send': 0.17.4
'@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -2664,6 +2814,8 @@ snapshots:
ansi-styles@6.2.1: {}
append-field@1.0.0: {}
arg@4.1.3: {}
argparse@2.0.1: {}
@@ -2764,6 +2916,12 @@ snapshots:
buffer-crc32@1.0.0: {}
buffer-from@1.1.2: {}
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -2806,12 +2964,21 @@ snapshots:
concat-map@0.0.1: {}
concat-stream@1.6.2:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 2.3.8
typedarray: 0.0.6
content-disposition@0.5.4:
dependencies:
safe-buffer: 5.2.1
cookie@1.0.2: {}
core-util-is@1.0.3: {}
create-require@1.1.1: {}
cross-spawn@7.0.6:
@@ -3496,6 +3663,8 @@ snapshots:
call-bound: 1.0.3
get-intrinsic: 1.2.7
isarray@1.0.0: {}
isarray@2.0.5: {}
isexe@2.0.0: {}
@@ -3567,6 +3736,8 @@ snapshots:
math-intrinsics@1.1.0: {}
media-typer@0.3.0: {}
merge2@1.4.1: {}
micromatch@4.0.8:
@@ -3617,16 +3788,32 @@ snapshots:
minipass@7.1.2: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
mnemonist@0.39.8:
dependencies:
obliterator: 2.0.5
ms@2.1.3: {}
multer@1.4.5-lts.1:
dependencies:
append-field: 1.0.0
busboy: 1.6.0
concat-stream: 1.6.2
mkdirp: 0.5.6
object-assign: 4.1.1
type-is: 1.6.18
xtend: 4.0.2
natural-compare@1.4.0: {}
nodemailer@6.10.0: {}
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
object-keys@1.1.1: {}
@@ -3747,6 +3934,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
process-nextick-args@2.0.1: {}
process-warning@4.0.1: {}
punycode@2.3.1: {}
@@ -3762,6 +3951,16 @@ snapshots:
quick-format-unescaped@4.0.4: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@@ -3820,6 +4019,8 @@ snapshots:
has-symbols: 1.1.0
isarray: 2.0.5
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safe-push-apply@1.0.0:
@@ -3965,6 +4166,8 @@ snapshots:
dependencies:
stream-chain: 2.2.5
streamsearch@1.1.0: {}
strict-uri-encode@2.0.0: {}
string-width@4.2.3:
@@ -4002,6 +4205,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
@@ -4085,6 +4292,11 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.3
@@ -4118,6 +4330,8 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typedarray@0.0.6: {}
typescript-eslint@8.24.1(eslint@9.20.1)(typescript@5.7.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)

View File

@@ -1,4 +1,5 @@
import { prisma } from "../src/shared/prisma";
import {env} from "../src/env"
import bcrypt from "bcryptjs";
import crypto from "node:crypto";
@@ -24,7 +25,7 @@ const defaultConfigs = [
},
{
key: "appLogo",
value: "https://i.ibb.co/xSjDkgVT/image-1.png",
value: `${env.BASE_URL}/uploads/logo/logo.png`,
type: "string",
group: "general",
},

View File

@@ -0,0 +1,17 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const userCount = await prisma.user.count();
console.log(userCount);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -6,10 +6,22 @@ while ! nc -z postgres 5432; do
done
echo "PostgreSQL is up!"
echo "Running migrations and seed..."
echo "Generating Prisma client..."
npx prisma generate --schema=./prisma/schema.prisma
# Check if database needs migrations
echo "Checking migrations..."
npx prisma migrate deploy
pnpm db:seed
# Check if database is empty using Prisma
USER_COUNT=$(node ./scripts/check-db.mjs)
if [ "$USER_COUNT" -eq "0" ]; then
echo "Database is empty, running seeds..."
pnpm db:seed
else
echo "Database already has data, skipping seeds..."
fi
echo "Starting application..."
pnpm start
pnpm start

View File

@@ -9,6 +9,7 @@ const envSchema = z.object({
MINIO_ROOT_USER: z.string().min(1),
MINIO_REGION: z.string().min(1),
MINIO_BUCKET_NAME: z.string().min(1),
BASE_URL: z.string().min(1),
PORT: z.string().min(1),
DATABASE_URL: z.string().min(1),

View File

@@ -1,7 +1,43 @@
import { LogoService } from "./logo.service";
import { AppService } from "./service";
import { MultipartFile } from "@fastify/multipart";
import { FastifyReply, FastifyRequest } from "fastify";
import { prisma } from "../../shared/prisma";
import multer from 'multer';
import path from 'path';
import { Request, Response } from 'express';
import fs from 'fs';
const uploadsDir = path.join(process.cwd(), 'uploads/logo');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadsDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, `${uniqueSuffix}${ext}`);
}
});
const fileFilter = (req: any, file: any, cb: any) => {
if (!file.mimetype.startsWith('image/')) {
cb(new Error('Only images are allowed'));
return;
}
cb(null, true);
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024
}
}).single('file');
export class AppController {
private appService = new AppService();
@@ -51,36 +87,68 @@ export class AppController {
}
async uploadLogo(request: FastifyRequest, reply: FastifyReply) {
try {
const file = (request.body as any).file as MultipartFile;
if (!file) {
return reply.status(400).send({ error: "No file uploaded" });
}
const buffer = await file.toBuffer();
const imageUrl = await this.logoService.uploadLogo(buffer);
// Get current logo URL to delete it
const currentLogo = await this.appService.updateConfig("appLogo", imageUrl);
if (currentLogo && currentLogo.value !== imageUrl) {
await this.logoService.deleteLogo(currentLogo.value);
}
return reply.send({ logo: imageUrl });
} catch (error: any) {
console.error("Upload error:", error);
return reply.status(400).send({ error: error.message });
if (!request.isMultipart()) {
return reply.status(400).send({ error: "Request must be multipart/form-data" });
}
return new Promise((resolve) => {
const rawRequest = request.raw;
rawRequest.headers = request.headers;
upload(rawRequest as Request, reply.raw as Response, async (err) => {
if (err) {
if (err.code === 'LIMIT_FILE_SIZE') {
return resolve(reply.status(400).send({ error: "File size cannot be larger than 5MB" }));
}
return resolve(reply.status(400).send({ error: err.message }));
}
const file = (rawRequest as any).file;
if (!file) {
return resolve(reply.status(400).send({ error: "No file uploaded" }));
}
try {
const imageUrl = await this.logoService.uploadLogo(file.path);
const currentLogo = await this.appService.updateConfig("appLogo", imageUrl);
if (currentLogo && currentLogo.value !== imageUrl) {
await this.logoService.deleteLogo(currentLogo.value);
}
resolve(reply.send({ logo: imageUrl }));
} catch (error: any) {
try {
if (fs.existsSync(file.path)) {
await fs.promises.unlink(file.path);
}
} catch (cleanupError) {
console.error("Error cleaning up temp file:", cleanupError);
}
console.error("Upload error:", error);
resolve(reply.status(400).send({ error: error.message }));
}
});
});
}
async removeLogo(request: FastifyRequest, reply: FastifyReply) {
try {
const currentLogo = await this.appService.updateConfig("appLogo", "");
if (currentLogo && currentLogo.value) {
await this.logoService.deleteLogo(currentLogo.value);
const currentConfig = await prisma.appConfig.findUnique({
where: { key: "appLogo" }
});
if (currentConfig && currentConfig.value) {
await this.logoService.deleteLogo(currentConfig.value);
await this.appService.updateConfig("appLogo", "");
} else {
console.error("No logo found to delete");
}
return reply.send({ message: "Logo removed successfully" });
} catch (error: any) {
console.error("Logo removal error:", error);
return reply.status(400).send({ error: error.message });
}
}

View File

@@ -1,52 +1,61 @@
import { minioClient } from "../../config/minio.config";
import { randomUUID } from "crypto";
import sharp from "sharp";
import fs from "fs";
import path from "path";
import { env } from "../../env";
export class LogoService {
private readonly bucketName = "logos";
private readonly uploadsDir = "/app/uploads/logo";
constructor() {
this.initializeBucket();
this.initializeUploadsDir();
}
private async initializeBucket() {
private initializeUploadsDir() {
try {
const bucketExists = await minioClient.bucketExists(this.bucketName);
if (!bucketExists) {
await minioClient.makeBucket(this.bucketName, "sa-east-1");
const policy = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: { AWS: ["*"] },
Action: ["s3:GetObject"],
Resource: [`arn:aws:s3:::${this.bucketName}/*`],
},
],
};
await minioClient.setBucketPolicy(this.bucketName, JSON.stringify(policy));
if (!fs.existsSync(this.uploadsDir)) {
fs.mkdirSync(this.uploadsDir, { recursive: true });
}
} catch (error) {
console.error("Error initializing logo bucket:", error);
console.error("Error initializing uploads directory:", error);
throw error;
}
}
async uploadLogo(imageBuffer: Buffer): Promise<string> {
async uploadLogo(filePath: string): Promise<string> {
try {
const metadata = await sharp(imageBuffer).metadata();
if (!fs.existsSync(filePath)) {
throw new Error("Upload file not found");
}
const metadata = await sharp(filePath).metadata();
if (!metadata.width || !metadata.height) {
await fs.promises.unlink(filePath);
throw new Error("Invalid image file");
}
const webpBuffer = await sharp(imageBuffer).resize(256, 256, { fit: "contain" }).webp({ quality: 80 }).toBuffer();
const webpBuffer = await sharp(filePath)
.resize(256, 256, { fit: "contain" })
.webp({ quality: 80 })
.toBuffer();
const objectName = `app/${randomUUID()}.webp`;
await minioClient.putObject(this.bucketName, objectName, webpBuffer);
const filename = `${randomUUID()}.webp`;
const outputPath = path.join(this.uploadsDir, filename);
await fs.promises.writeFile(outputPath, webpBuffer);
const publicUrl = `${process.env.MINIO_PUBLIC_URL}/${this.bucketName}/${objectName}`;
return publicUrl;
await fs.promises.unlink(filePath);
return `${env.BASE_URL}/uploads/logo/${filename}`;
} catch (error) {
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
}
} catch (cleanupError) {
console.error("Error cleaning up file:", cleanupError);
}
console.error("Error uploading logo:", error);
throw error;
}
@@ -54,10 +63,19 @@ export class LogoService {
async deleteLogo(imageUrl: string) {
try {
const objectName = imageUrl.split(`/${this.bucketName}/`)[1];
await minioClient.removeObject(this.bucketName, objectName);
const filename = imageUrl.split('/logo/')[1];
if (!filename) {
throw new Error("Invalid logo URL - could not extract filename");
}
const filePath = path.join(this.uploadsDir, filename);
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
}
} catch (error) {
console.error("Error deleting logo:", error);
console.error("Error in logo deletion process:", error);
throw error;
}
}

View File

@@ -75,7 +75,7 @@ export async function appRoutes(app: FastifyInstance) {
app.get(
"/app/configs",
{
preValidation: adminPreValidation,
// preValidation: adminPreValidation,
schema: {
tags: ["App"],
operationId: "getAllConfigs",
@@ -126,10 +126,6 @@ export async function appRoutes(app: FastifyInstance) {
operationId: "uploadLogo",
summary: "Upload app logo",
description: "Upload a new app logo (admin only)",
consumes: ["multipart/form-data"],
body: z.object({
file: z.any().describe("Image file (JPG, PNG, GIF)"),
}),
response: {
200: z.object({
logo: z.string().describe("The logo URL"),

View File

@@ -14,15 +14,13 @@ export class AuthController {
isAdmin: user.isAdmin,
});
// Set token in HTTP-only cookie
reply.setCookie("token", token, {
httpOnly: true,
path: "/",
secure: process.env.NODE_ENV === "production", // Only send over HTTPS in production
sameSite: "strict", // Protect against CSRF
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
});
// Return only user data
return reply.send({ user });
} catch (error: any) {
return reply.status(400).send({ error: error.message });

View File

@@ -88,7 +88,7 @@ export class AuthService {
if (user.loginAttempts) {
const maxAttempts = Number(await this.configService.getValue("maxLoginAttempts"));
const blockDurationSeconds = Number(await this.configService.getValue("loginBlockDuration"));
const blockDuration = blockDurationSeconds * 1000; // convert seconds to milliseconds
const blockDuration = blockDurationSeconds * 1000;
if (
user.loginAttempts.attempts >= maxAttempts &&

View File

@@ -58,7 +58,7 @@ export async function fileRoutes(app: FastifyInstance) {
name: z.string().describe("The file name"),
description: z.string().nullable().describe("The file description"),
extension: z.string().describe("The file extension"),
size: z.string().describe("The file size"), // BigInt retornado como string
size: z.string().describe("The file size"),
objectName: z.string().describe("The object name of the file"),
userId: z.string().describe("The user ID"),
createdAt: z.date().describe("The file creation date"),
@@ -116,7 +116,7 @@ export async function fileRoutes(app: FastifyInstance) {
name: z.string().describe("The file name"),
description: z.string().nullable().describe("The file description"),
extension: z.string().describe("The file extension"),
size: z.string().describe("The file size"), // BigInt retornado como string
size: z.string().describe("The file size"),
objectName: z.string().describe("The object name of the file"),
userId: z.string().describe("The user ID"),
createdAt: z.date().describe("The file creation date"),

View File

@@ -31,7 +31,6 @@ export class PrismaShareRepository implements IShareRepository {
): Promise<Share> {
const { files, recipients, expiration, ...shareData } = data;
// Validate and filter arrays
const validFiles = (files ?? []).filter((id) => id && id.trim().length > 0);
const validRecipients = (recipients ?? []).filter((email) => email && email.trim().length > 0);

View File

@@ -281,7 +281,6 @@ export class ShareService {
throw new Error("Unauthorized to update this share");
}
// Verifica se o alias já está em uso por outro share
const existingAlias = await prisma.shareAlias.findUnique({
where: { alias },
});
@@ -290,7 +289,6 @@ export class ShareService {
throw new Error("Alias already in use");
}
// Cria ou atualiza o alias
const shareAlias = await prisma.shareAlias.upsert({
where: { shareId },
create: { shareId, alias },
@@ -322,7 +320,6 @@ export class ShareService {
throw new Error("Share not found");
}
// Reutiliza a lógica existente do getShare
return this.getShare(shareAlias.shareId, password);
}

View File

@@ -20,8 +20,11 @@ export class StorageService {
}> {
try {
if (isAdmin) {
// Original implementation for admins
const command = process.platform === "win32" ? "wmic logicaldisk get size,freespace,caption" : "df -B1 .";
const command = process.platform === "win32"
? "wmic logicaldisk get size,freespace,caption"
: process.platform === "darwin"
? "df -k ."
: "df -B1 .";
const { stdout } = await execAsync(command);
let total = 0;
@@ -34,6 +37,11 @@ export class StorageService {
total += parseInt(size) || 0;
available += parseInt(freespace) || 0;
}
} else if (process.platform === "darwin") {
const lines = stdout.trim().split("\n");
const [, size, , avail] = lines[1].trim().split(/\s+/);
total = parseInt(size) * 1024;
available = parseInt(avail) * 1024;
} else {
const lines = stdout.trim().split("\n");
const [, size, , avail] = lines[1].trim().split(/\s+/);
@@ -50,17 +58,14 @@ export class StorageService {
uploadAllowed: true,
};
} else if (userId) {
// Implementation for regular users
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
const maxStorageGB = Number(maxTotalStorage) / (1024 * 1024 * 1024);
// Busca apenas os arquivos que pertencem diretamente ao usuário
const userFiles = await prisma.file.findMany({
where: { userId },
select: { size: true },
});
// Calcula o total de espaço usado somando os arquivos
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
const usedStorageGB = Number(totalUsedStorage) / (1024 * 1024 * 1024);

View File

@@ -1,67 +1,67 @@
import { minioClient } from "../../config/minio.config";
import { PrismaClient } from "@prisma/client";
import { randomUUID } from "crypto";
import sharp from "sharp";
const prisma = new PrismaClient();
import fs from "fs";
import path from "path";
import { env } from "../../env";
import multer from 'multer';
import { Request } from 'express';
export class AvatarService {
private readonly bucketName = "avatars";
private readonly uploadsDir = "/app/uploads/avatars";
constructor() {
this.initializeBucket();
this.initializeUploadsDir();
}
private async initializeBucket() {
private initializeUploadsDir() {
try {
const bucketExists = await minioClient.bucketExists(this.bucketName);
if (!bucketExists) {
await minioClient.makeBucket(this.bucketName, "sa-east-1");
const policy = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: { AWS: ["*"] },
Action: ["s3:GetObject"],
Resource: [`arn:aws:s3:::${this.bucketName}/*`],
},
],
};
await minioClient.setBucketPolicy(this.bucketName, JSON.stringify(policy));
if (!fs.existsSync(this.uploadsDir)) {
fs.mkdirSync(this.uploadsDir, { recursive: true });
}
} catch (error) {
console.error("Error initializing avatar bucket:", error);
console.error("Error initializing uploads directory:", error);
}
}
async uploadAvatar(userId: string, imageBuffer: Buffer): Promise<string> {
async uploadAvatar(userId: string, filePath: string): Promise<string> {
try {
// Buscar usuário atual para verificar se tem avatar
const user = await prisma.user.findUnique({
where: { id: userId },
select: { image: true },
});
// Deletar avatar anterior se existir
if (user?.image) {
await this.deleteAvatar(user.image);
if (!fs.existsSync(filePath)) {
throw new Error("Upload file not found");
}
// Validar e fazer upload do novo avatar
const metadata = await sharp(imageBuffer).metadata();
const metadata = await sharp(filePath).metadata();
if (!metadata.width || !metadata.height) {
await fs.promises.unlink(filePath);
throw new Error("Invalid image file");
}
const webpBuffer = await sharp(imageBuffer).resize(256, 256, { fit: "cover" }).webp({ quality: 80 }).toBuffer();
const userDir = path.join(this.uploadsDir, userId);
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir, { recursive: true });
}
const objectName = `${userId}/${randomUUID()}.webp`;
await minioClient.putObject(this.bucketName, objectName, webpBuffer);
const webpBuffer = await sharp(filePath)
.resize(256, 256, { fit: "cover" })
.webp({ quality: 80 })
.toBuffer();
const publicUrl = `${process.env.MINIO_PUBLIC_URL}/${this.bucketName}/${objectName}`;
return publicUrl;
const filename = `${randomUUID()}.webp`;
const outputPath = path.join(userDir, filename);
await fs.promises.writeFile(outputPath, webpBuffer);
await fs.promises.unlink(filePath);
return `${env.BASE_URL}/uploads/avatars/${userId}/${filename}`;
} catch (error) {
try {
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
}
} catch (cleanupError) {
console.error("Error cleaning up file:", cleanupError);
}
console.error("Error uploading avatar:", error);
throw error;
}
@@ -69,11 +69,25 @@ export class AvatarService {
async deleteAvatar(imageUrl: string) {
try {
const objectName = imageUrl.split(`/${this.bucketName}/`)[1];
console.log("Deleting avatar:", objectName);
await minioClient.removeObject(this.bucketName, objectName);
const parts = imageUrl.split('/avatars/')[1]?.split('/');
if (!parts || parts.length !== 2) {
throw new Error("Invalid avatar URL - could not extract user ID and filename");
}
const [userId, filename] = parts;
const filePath = path.join(this.uploadsDir, userId, filename);
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
const userDir = path.join(this.uploadsDir, userId);
const remainingFiles = await fs.promises.readdir(userDir);
if (remainingFiles.length === 0) {
await fs.promises.rmdir(userDir);
}
}
} catch (error) {
console.error("Error deleting avatar:", error);
console.error("Error in avatar deletion process:", error);
throw error;
}
}

View File

@@ -1,8 +1,42 @@
import { AvatarService } from "./avatar.service";
import { UpdateUserSchema, createRegisterUserSchema } from "./dto";
import { UserService } from "./service";
import { MultipartFile } from "@fastify/multipart";
import { FastifyReply, FastifyRequest } from "fastify";
import multer from 'multer';
import path from 'path';
import { Request, Response } from 'express';
import fs from 'fs';
const uploadsDir = "/app/uploads/avatars";
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadsDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, `${uniqueSuffix}${ext}`);
}
});
const fileFilter = (req: any, file: any, cb: any) => {
if (!file.mimetype.startsWith('image/')) {
return cb(new Error('Only images are allowed'));
}
cb(null, true);
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024
}
}).single('file');
export class UserController {
private userService = new UserService();
@@ -91,25 +125,51 @@ export class UserController {
}
async uploadAvatar(request: FastifyRequest, reply: FastifyReply) {
try {
const userId = (request as any).user?.userId;
if (!userId) {
return reply.status(401).send({ error: "Unauthorized" });
}
const file = (request.body as any).file as MultipartFile;
if (!file) {
return reply.status(400).send({ error: "No file uploaded" });
}
const buffer = await file.toBuffer();
const imageUrl = await this.avatarService.uploadAvatar(userId, buffer);
const updatedUser = await this.userService.updateUserImage(userId, imageUrl);
return reply.send(updatedUser);
} catch (error: any) {
console.error("Upload error:", error);
return reply.status(400).send({ error: error.message });
if (!request.isMultipart()) {
return reply.status(400).send({ error: "Request must be multipart/form-data" });
}
const userId = (request as any).user?.userId;
if (!userId) {
return reply.status(401).send({ error: "Unauthorized" });
}
return new Promise((resolve) => {
const rawRequest = request.raw;
rawRequest.headers = request.headers;
upload(rawRequest as Request, reply.raw as Response, async (err) => {
if (err) {
console.error("Upload error:", err);
if (err.code === 'LIMIT_FILE_SIZE') {
return resolve(reply.status(400).send({ error: "File size cannot be larger than 5MB" }));
}
return resolve(reply.status(400).send({ error: err.message }));
}
const file = (rawRequest as any).file;
if (!file) {
return resolve(reply.status(400).send({ error: "No file uploaded" }));
}
try {
const imageUrl = await this.avatarService.uploadAvatar(userId, file.path);
const updatedUser = await this.userService.updateUserImage(userId, imageUrl);
resolve(reply.send(updatedUser));
} catch (error: any) {
try {
if (fs.existsSync(file.path)) {
await fs.promises.unlink(file.path);
}
} catch (cleanupError) {
console.error("Error cleaning up temp file:", cleanupError);
}
console.error("Upload error:", error);
resolve(reply.status(400).send({ error: error.message }));
}
});
});
}
async removeAvatar(request: FastifyRequest, reply: FastifyReply) {

View File

@@ -4,6 +4,7 @@ import { UpdateUserSchema, UserResponseSchema } from "./dto";
import { validatePasswordMiddleware } from "./middleware";
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import { z } from "zod";
import multer from "multer";
export async function userRoutes(app: FastifyInstance) {
const userController = new UserController();
@@ -312,16 +313,20 @@ export async function userRoutes(app: FastifyInstance) {
app.post(
"/users/avatar",
{
preValidation,
preValidation: async (request: FastifyRequest, reply: FastifyReply) => {
try {
await request.jwtVerify();
} catch (err) {
console.error(err);
return reply.status(401).send({ error: "Unauthorized" });
}
},
schema: {
tags: ["User"],
operationId: "uploadAvatar",
summary: "Upload user avatar",
description: "Upload and update user profile image",
consumes: ["multipart/form-data"],
body: z.object({
file: z.any().describe("Image file (JPG, PNG, GIF)"),
}),
response: {
200: UserResponseSchema,
400: z.object({ error: z.string() }),

View File

@@ -8,15 +8,27 @@ import { shareRoutes } from "./modules/share/routes";
import { storageRoutes } from "./modules/storage/routes";
import { userRoutes } from "./modules/user/routes";
import fastifyMultipart from "@fastify/multipart";
import fastifyStatic from "@fastify/static";
import path from "path";
async function startServer() {
const app = await buildApp();
await app.register(fastifyMultipart, {
limits: {
fileSize: 5 * 1024 * 1024,
},
attachFieldsToBody: true,
fieldNameSize: 100,
fieldSize: 100,
fields: 10,
fileSize: 5 * 1024 * 1024,
files: 1,
headerPairs: 2000
}
});
await app.register(fastifyStatic, {
root: "/app/uploads",
prefix: "/uploads/",
decorateReply: false,
});
app.register(authRoutes);

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB