Feature: user upload artwork
This commit is contained in:
@@ -10,11 +10,13 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"express": "^5.1.0",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "2.0.2",
|
||||
"prisma": "^6.17.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/multer": "2.0.0",
|
||||
"eslint": "9.38.0",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.9.3",
|
||||
@@ -84,6 +86,8 @@
|
||||
|
||||
"@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="],
|
||||
|
||||
"@types/multer": ["@types/multer@2.0.0", "", { "dependencies": { "@types/express": "*" } }, "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw=="],
|
||||
|
||||
"@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
@@ -104,6 +108,8 @@
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
@@ -116,6 +122,10 @@
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
|
||||
@@ -138,6 +148,8 @@
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||
|
||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
@@ -304,10 +316,16 @@
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
|
||||
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"multer": ["multer@2.0.2", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "mkdirp": "^0.5.6", "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" } }, "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
@@ -320,6 +338,8 @@
|
||||
|
||||
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
@@ -372,6 +392,8 @@
|
||||
|
||||
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
@@ -402,6 +424,10 @@
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@@ -414,6 +440,8 @@
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
||||
@@ -422,6 +450,8 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
@@ -430,6 +460,8 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
@@ -446,8 +478,16 @@
|
||||
|
||||
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
|
||||
|
||||
"multer/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||
|
||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"multer/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"multer/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"multer/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
}
|
||||
}
|
||||
|
||||
20
server/examples/scripts/create-user.sh
Executable file
20
server/examples/scripts/create-user.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
USERNAME="alexzinn"
|
||||
PASSWORD="al3110e"
|
||||
EMAIL="zinntechniker@gmail.com"
|
||||
|
||||
curl --verbose \
|
||||
--request POST \
|
||||
-H "x-secret: al3110e" \
|
||||
-H "Content-Type: application/json" \
|
||||
http://localhost:3000/api/users \
|
||||
-d "{
|
||||
\"username\": \"${USERNAME}\",
|
||||
\"password\": \"${PASSWORD}\",
|
||||
\"email\": \"${EMAIL}\"
|
||||
}"
|
||||
@@ -12,6 +12,7 @@
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/multer": "2.0.0",
|
||||
"eslint": "9.38.0",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.9.3"
|
||||
@@ -23,6 +24,7 @@
|
||||
"cookie-parser": "^1.4.7",
|
||||
"express": "^5.1.0",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "2.0.2",
|
||||
"prisma": "^6.17.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,3 +27,20 @@ CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON "public"."Users"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- 5. Define the Artworks table
|
||||
CREATE TABLE "public"."Artworks" (
|
||||
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"title" CITEXT UNIQUE NOT NULL,
|
||||
"filename" TEXT NOT NULL,
|
||||
"mimetype" TEXT NOT NULL,
|
||||
"size" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 6. Create the trigger and attach it to the Artworks table
|
||||
CREATE TRIGGER update_artworks_updated_at
|
||||
BEFORE UPDATE ON "public"."Artworks"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
@@ -8,10 +8,20 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Artworks {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
title String @unique @db.Text
|
||||
filename String
|
||||
mimetype String
|
||||
size Int
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
}
|
||||
|
||||
model Users {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
username String @unique @db.Citext
|
||||
email String @unique @db.Citext
|
||||
username String @unique @db.Text
|
||||
email String @unique @db.Text
|
||||
hashed_password String
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Server from './server';
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
Server.listen(port, () => {
|
||||
console.log('Server is running on port [%o]', port);
|
||||
});
|
||||
});
|
||||
|
||||
206
server/src/routes/Artworks.ts
Normal file
206
server/src/routes/Artworks.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import {PrismaClient} from '../../generated/prisma';
|
||||
|
||||
const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Ensure uploads directory exists
|
||||
const uploadsDir = path.resolve('uploads');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, {recursive: true});
|
||||
}
|
||||
|
||||
// Configure multer for file uploads
|
||||
const diskStorage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadsDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Create unique filename with timestamp
|
||||
const uniqueName = `${Date.now()}-${file.originalname}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
// File validation
|
||||
const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
|
||||
// Accept only image files
|
||||
const allowedMimeTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed.'));
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage: diskStorage,
|
||||
fileFilter: fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
||||
}
|
||||
});
|
||||
|
||||
// GET all artworks
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const artworks = await prisma.artworks.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
filename: true,
|
||||
mimetype: true,
|
||||
size: true,
|
||||
created_at: true,
|
||||
updated_at: true
|
||||
},
|
||||
orderBy: {
|
||||
created_at: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).json(artworks);
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
// GET single artwork metadata
|
||||
router.get('/:id', async (req, res) => {
|
||||
const {id} = req.params;
|
||||
try {
|
||||
const artwork = await prisma.artworks.findUnique({
|
||||
where: {id},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
filename: true,
|
||||
mimetype: true,
|
||||
size: true,
|
||||
created_at: true,
|
||||
updated_at: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!artwork) {
|
||||
return res.status(404).json({status: 'error', message: 'Artwork not found'});
|
||||
}
|
||||
|
||||
return res.status(200).json(artwork);
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
// GET artwork image file
|
||||
router.get('/:id/image', async (req, res) => {
|
||||
const {id} = req.params;
|
||||
try {
|
||||
const artwork = await prisma.artworks.findUnique({
|
||||
where: {id},
|
||||
select: {filename: true, mimetype: true}
|
||||
});
|
||||
|
||||
if (!artwork) {
|
||||
return res.status(404).json({status: 'error', message: 'Artwork not found'});
|
||||
}
|
||||
|
||||
const filePath = path.join(uploadsDir, artwork.filename);
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({status: 'error', message: 'Image file not found'});
|
||||
}
|
||||
|
||||
// Set caching headers for better performance
|
||||
res.setHeader('Content-Type', artwork.mimetype);
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year
|
||||
res.setHeader('ETag', artwork.filename); // Use filename as ETag
|
||||
|
||||
// Send file
|
||||
return res.sendFile(filePath);
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
// POST new artwork
|
||||
router.post('/', upload.single('image'), async (req, res) => {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({status: 'error', message: 'No image file uploaded'});
|
||||
}
|
||||
|
||||
if (!req.body.title) {
|
||||
// Cleanup uploaded file if validation fails
|
||||
fs.unlinkSync(req.file.path);
|
||||
return res.status(400).json({status: 'error', message: 'Title is required'});
|
||||
}
|
||||
|
||||
try {
|
||||
const artwork = await prisma.artworks.create({
|
||||
data: {
|
||||
title: req.body.title,
|
||||
filename: req.file.filename,
|
||||
mimetype: req.file.mimetype,
|
||||
size: req.file.size
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
id: artwork.id,
|
||||
title: artwork.title,
|
||||
filename: artwork.filename,
|
||||
mimetype: artwork.mimetype,
|
||||
size: artwork.size,
|
||||
created_at: artwork.created_at,
|
||||
updated_at: artwork.updated_at
|
||||
});
|
||||
} catch (error: any) {
|
||||
// Cleanup uploaded file if database operation fails
|
||||
fs.unlinkSync(req.file.path);
|
||||
console.error(error.message);
|
||||
|
||||
if (error.code === 'P2002') {
|
||||
return res.status(409).json({status: 'error', message: 'An artwork with this title already exists'});
|
||||
}
|
||||
|
||||
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE artwork
|
||||
router.delete('/:id', async (req, res) => {
|
||||
const {id} = req.params;
|
||||
try {
|
||||
const artwork = await prisma.artworks.findUnique({
|
||||
where: {id},
|
||||
select: {filename: true}
|
||||
});
|
||||
|
||||
if (!artwork) {
|
||||
return res.status(404).json({status: 'error', message: 'Artwork not found'});
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await prisma.artworks.delete({where: {id}});
|
||||
|
||||
// Delete file from disk
|
||||
const filePath = path.join(uploadsDir, artwork.filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
return res.status(200).json({status: 'success', message: 'Artwork deleted'});
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -7,6 +7,37 @@ const prisma = new PrismaClient();
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {username, email, password} = req.body;
|
||||
|
||||
console.log(req.body);
|
||||
|
||||
// Check secret header
|
||||
const secret = req.get('x-secret') || req.headers['x-secret'];
|
||||
if (secret !== process.env.SERVER_X_SECRET) {
|
||||
return res.status(403).json({status: 'error', message: 'Forbidden'});
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
message: 'Username, email, and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return res.status(400).json({status: 'error', message: 'Invalid email format'});
|
||||
}
|
||||
|
||||
// Password strength validation
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
message: 'Password must be at least 8 characters long'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const hashedPassword = await Authenticator.hashPassword(password);
|
||||
const user = await prisma.users.create({
|
||||
@@ -16,9 +47,26 @@ router.post('/', async (req, res) => {
|
||||
hashed_password: hashedPassword
|
||||
}
|
||||
});
|
||||
res.status(201).json(user);
|
||||
} catch (error) {
|
||||
|
||||
// Return user without sensitive data
|
||||
return res.status(201).json({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
created_at: user.created_at
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
|
||||
// Handle duplicate username or email
|
||||
if (error.code === 'P2002') {
|
||||
const field = error.meta?.target?.[0] || 'username or email';
|
||||
return res.status(409).json({
|
||||
status: 'error',
|
||||
message: `A user with this ${field} already exists`
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
@@ -27,28 +75,40 @@ router.post('/login', async (req, res) => {
|
||||
const {username, password} = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({message: 'Username and password are required'});
|
||||
return res.status(400).json({status: 'error', message: 'Username and password are required'});
|
||||
}
|
||||
|
||||
const user = await prisma.users.findUnique({where: {username}});
|
||||
try {
|
||||
const user = await prisma.users.findUnique({where: {username}});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({status: 'unauthorized'});
|
||||
if (!user) {
|
||||
return res.status(401).json({status: 'error', message: 'Invalid credentials'});
|
||||
}
|
||||
|
||||
const isPasswordValid = await Authenticator.verifyPassword(password, user.hashed_password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({status: 'error', message: 'Invalid credentials'});
|
||||
}
|
||||
|
||||
// TODO: Generate a JWT token and set it as a cookie
|
||||
return res.status(200).json({
|
||||
status: 'ok',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
return res.status(500).json({status: 'error', message: 'Internal server error'});
|
||||
}
|
||||
|
||||
const isPasswordValid = await Authenticator.verifyPassword(password, user.hashed_password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({status: 'unauthorized'});
|
||||
}
|
||||
|
||||
// TODO: Generate a JWT token and set it as a cookie
|
||||
return res.status(200).json({status: 'ok'});
|
||||
});
|
||||
|
||||
router.post('/logout', async (req, res) => {
|
||||
res.clearCookie('token');
|
||||
res.status(200).json({message: 'Logged out successfully'});
|
||||
res.status(200).json({status: 'ok', message: 'Logged out successfully'});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -3,6 +3,7 @@ import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import usersRouter from './routes/Users';
|
||||
import artworksRouter from './routes/Artworks';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -13,6 +14,7 @@ app.use(express.urlencoded({extended: true}));
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use('/api/users', usersRouter);
|
||||
app.use('/api/artworks', artworksRouter);
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.resolve('..', 'frontend', 'dist', 'index.html'));
|
||||
});
|
||||
|
||||
BIN
server/uploads/1761369629855-IMG_1331.JPG
Normal file
BIN
server/uploads/1761369629855-IMG_1331.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 804 KiB |
BIN
server/uploads/1761369674210-IMG_1330.jpg
Normal file
BIN
server/uploads/1761369674210-IMG_1330.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 830 KiB |
BIN
server/uploads/1761369689998-IMG_1410.JPG
Normal file
BIN
server/uploads/1761369689998-IMG_1410.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
BIN
server/uploads/1761369716209-IMG_1333.JPG
Normal file
BIN
server/uploads/1761369716209-IMG_1333.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 787 KiB |
BIN
server/uploads/1761369735459-IMG_1421.PNG
Normal file
BIN
server/uploads/1761369735459-IMG_1421.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 MiB |
BIN
server/uploads/1761369758547-IMG_2191.JPG
Normal file
BIN
server/uploads/1761369758547-IMG_2191.JPG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
Reference in New Issue
Block a user