Feature: user upload artwork

This commit is contained in:
2025-10-25 02:22:39 -04:00
parent 28d0443f44
commit deb163bd70
23 changed files with 1288 additions and 177 deletions

View File

@@ -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=="],
}
}

View 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}\"
}"

View File

@@ -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"
}
}

View File

@@ -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();

View File

@@ -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)

View File

@@ -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);
});
});

View 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;

View File

@@ -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 !== 'al3110e') {
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;

View File

@@ -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'));
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB