mirror of
https://github.com/kyantech/Palmr.git
synced 2025-11-02 04:53:26 +00:00
feat: move logo and avatars to local upload
This commit is contained in:
@@ -10,3 +10,4 @@ MINIO_REGION="sa-east-1"
|
||||
MINIO_BUCKET_NAME="files"
|
||||
|
||||
PORT=3333
|
||||
BASE_URL=http://localhost:3333
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
214
apps/server/pnpm-lock.yaml
generated
214
apps/server/pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
17
apps/server/scripts/check-db.mjs
Normal file
17
apps/server/scripts/check-db.mjs
Normal 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();
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() }),
|
||||
|
||||
@@ -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);
|
||||
|
||||
BIN
apps/server/uploads/logo/logo.png
Normal file
BIN
apps/server/uploads/logo/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Reference in New Issue
Block a user