diff --git a/package-lock.json b/package-lock.json index 4d4d085..fd22bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.525.0", "@aws-sdk/s3-request-presigner": "^3.525.0", - "@prisma/client": "^5.10.2", + "@prisma/client": "^6.2.1", "@tiptap/pm": "^2.2.4", "@tiptap/react": "^2.2.4", "@tiptap/starter-kit": "^2.2.4", @@ -34,23 +34,28 @@ }, "devDependencies": { "@eslint/js": "^9.9.1", + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.11", + "@types/node": "^22.10.7", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@types/uuid": "^9.0.8", "@types/winston": "^2.4.4", "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.18", + "autoprefixer": "^10.4.20", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", - "postcss": "^8.4.35", - "prisma": "^5.10.2", - "tailwindcss": "^3.4.1", + "postcss": "^8.4.49", + "prisma": "^6.2.1", + "tailwindcss": "^3.4.17", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "6.0.9" } }, "node_modules/@alloc/quick-lru": { @@ -1347,371 +1352,428 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2367,12 +2429,13 @@ } }, "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz", + "integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==", "hasInstallScript": true, + "license": "Apache-2.0", "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "peerDependencies": { "prisma": "*" @@ -2384,48 +2447,53 @@ } }, "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "devOptional": true + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz", + "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==", + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz", + "integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/fetch-engine": "6.2.1", + "@prisma/get-platform": "6.2.1" } }, "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true + "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz", + "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==", + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz", + "integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/get-platform": "6.2.1" } }, "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz", + "integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0" + "@prisma/debug": "6.2.1" } }, "node_modules/@remirror/core-constants": { @@ -3740,6 +3808,13 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -3761,6 +3836,16 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -3806,6 +3891,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -3843,12 +3938,13 @@ } }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/prop-types": { @@ -4344,6 +4440,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -5007,41 +5104,44 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, "node_modules/escalade": { @@ -6184,12 +6284,16 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -6723,10 +6827,11 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6759,9 +6864,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -6777,9 +6882,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -6857,18 +6963,6 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", @@ -6923,19 +7017,20 @@ } }, "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz", + "integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.22.0" + "@prisma/engines": "6.2.1" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "optionalDependencies": { "fsevents": "2.3.3" @@ -7911,33 +8006,34 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", - "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -8122,10 +8218,11 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", @@ -8217,20 +8314,21 @@ } }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.9.tgz", + "integrity": "sha512-MSgUxHcaXLtnBPktkbUSoQUANApKYuxZ6DrbVENlIorbhL2dZydTLaZ01tjUoE3szeFzlFk9ANOKk0xurh4MKA==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -8239,19 +8337,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -8272,6 +8376,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, diff --git a/package.json b/package.json index 2974455..8376ff4 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,17 @@ "build": "vite build", "lint": "eslint .", "preview": "vite preview", - "server": "node server/index.js" + "server": "node server/index.js", + "prisma-seed": "npx prisma db seed", + "db:seed": "node prisma/seed.js" + }, + "prisma": { + "seed": "npx ts-node --project tsconfig.node.json --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.525.0", "@aws-sdk/s3-request-presigner": "^3.525.0", - "@prisma/client": "^5.10.2", + "@prisma/client": "^6.2.1", "@tiptap/pm": "^2.2.4", "@tiptap/react": "^2.2.4", "@tiptap/starter-kit": "^2.2.4", @@ -29,7 +34,6 @@ "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-router-dom": "^6.22.3", - "sharp": "^0.33.2", "uuid": "^9.0.1", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", @@ -37,22 +41,27 @@ }, "devDependencies": { "@eslint/js": "^9.9.1", + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.11", + "@types/node": "^22.10.7", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@types/uuid": "^9.0.8", "@types/winston": "^2.4.4", "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.18", + "autoprefixer": "^10.4.20", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^15.9.0", - "postcss": "^8.4.35", - "prisma": "^5.10.2", - "tailwindcss": "^3.4.1", + "postcss": "^8.4.49", + "prisma": "^6.2.1", + "tailwindcss": "^3.4.17", "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", - "vite": "^5.4.2" + "vite": "6.0.9" } } diff --git a/prisma/migrations/20250108142452_category_id/migration.sql b/prisma/migrations/20250108142452_category_id/migration.sql new file mode 100644 index 0000000..ad06d79 --- /dev/null +++ b/prisma/migrations/20250108142452_category_id/migration.sql @@ -0,0 +1,45 @@ +/* + Warnings: + + - You are about to drop the column `category` on the `Article` table. All the data in the column will be lost. + - Added the required column `categoryId` to the `Article` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Article" DROP COLUMN "category", +ADD COLUMN "categoryId" INTEGER NOT NULL; + +-- CreateTable +CREATE TABLE "Category" ( + "id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GalleryImage" ( + "id" TEXT NOT NULL, + "url" TEXT NOT NULL, + "caption" TEXT NOT NULL, + "alt" TEXT NOT NULL, + "width" INTEGER NOT NULL, + "height" INTEGER NOT NULL, + "size" INTEGER NOT NULL, + "format" TEXT NOT NULL, + "articleId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "order" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "GalleryImage_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name"); + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GalleryImage" ADD CONSTRAINT "GalleryImage_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b7945bb..0b75737 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,44 +20,51 @@ model User { } model Article { - id String @id @default(uuid()) + id String @id @default(uuid()) title String excerpt String content String - category String + category Category @relation(fields: [categoryId], references: [id]) + categoryId Int city String coverImage String readTime Int - likes Int @default(0) - dislikes Int @default(0) - publishedAt DateTime @default(now()) - author User @relation(fields: [authorId], references: [id]) + likes Int @default(0) + dislikes Int @default(0) + publishedAt DateTime @default(now()) + author User @relation(fields: [authorId], references: [id]) authorId String gallery GalleryImage[] } +model Category { + id Int @id + name String @unique + articles Article[] +} + model GalleryImage { - id String @id @default(uuid()) - url String - caption String - alt String - width Int - height Int - size Int - format String - article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) - articleId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - order Int @default(0) + id String @id @default(uuid()) + url String + caption String + alt String + width Int + height Int + size Int + format String + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + order Int @default(0) } model UserReaction { id String @id @default(uuid()) userId String articleId String - reaction String // 'like' or 'dislike' + reaction String // 'like' or 'dislike' createdAt DateTime @default(now()) @@unique([userId, articleId]) -} \ No newline at end of file +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..46ebfcf --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,16 @@ +import { exec } from 'child_process'; +import util from 'util'; + +const execPromise = util.promisify(exec); + +async function runSeed() { + try { + await execPromise('npx ts-node prisma/seed.ts'); + console.log('Seeding completed successfully.'); + } catch (error) { + console.error('Error executing the seed script:', error); + process.exit(1); + } +} + +runSeed(); \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..a1587ae --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,36 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +async function main() { + // Данные для заполнения + const categories = [ + { id: 1, name: "Film" }, + { id: 2, name: "Theater" }, + { id: 3, name: "Music" }, + { id: 4, name: "Sports" }, + { id: 5, name: "Art" }, + { id: 6, name: "Legends" }, + { id: 7, name: "Anniversaries" }, + { id: 8, name: "Memory" }, + ]; + + // Заполнение данных + for (const category of categories) { + await prisma.category.upsert({ + where: { id: category.id }, + update: {}, + create: category, + }); + } + + console.log('Данные успешно добавлены!'); +} + +main() + .catch((e) => { + throw e; + }) + .finally(async () => { + await prisma.$disconnect(); + }); \ No newline at end of file diff --git a/public/images/odnoklassniki-blk.svg b/public/images/odnoklassniki-blk.svg new file mode 100644 index 0000000..4c1fd49 --- /dev/null +++ b/public/images/odnoklassniki-blk.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/odnoklassniki.svg b/public/images/odnoklassniki.svg new file mode 100644 index 0000000..297f9da --- /dev/null +++ b/public/images/odnoklassniki.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/ok-11.svg b/public/images/ok-11.svg new file mode 100644 index 0000000..9860965 --- /dev/null +++ b/public/images/ok-11.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/public/images/ok.svg b/public/images/ok.svg new file mode 100644 index 0000000..e09d428 --- /dev/null +++ b/public/images/ok.svg @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/server/config/logger.ts b/server/config/logger.ts deleted file mode 100644 index 6d33ea8..0000000 --- a/server/config/logger.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as winston from 'winston'; -import 'winston-daily-rotate-file'; -import * as path from 'path'; - -// Define log levels -const levels = { - error: 0, - warn: 1, - info: 2, - http: 3, - debug: 4, -}; - -// Define log level based on environment -const level = () => { - const env = process.env.NODE_ENV || 'development'; - return env === 'development' ? 'debug' : 'warn'; -}; - -// Define colors for each level -const colors = { - error: 'red', - warn: 'yellow', - info: 'green', - http: 'magenta', - debug: 'blue', -}; - -// Add colors to winston -winston.addColors(colors); - -// Custom format for logging -const format = winston.format.combine( - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), - winston.format.colorize({ all: true }), - winston.format.printf( - (info) => `${info.timestamp} ${info.level}: ${info.message}` - ) -); - -// Define file transport options -const fileRotateTransport = new winston.transports.DailyRotateFile({ - filename: path.join('logs', '%DATE%-server.log'), - datePattern: 'YYYY-MM-DD', - zippedArchive: true, - maxSize: '20m', - maxFiles: '14d', - format: winston.format.combine( - winston.format.uncolorize(), - winston.format.timestamp(), - winston.format.json() - ), -}); - -// Create the logger -const logger = winston.createLogger({ - level: level(), - levels, - format, - transports: [ - new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.simple() - ), - }), - fileRotateTransport, - ], -}); - -// Create a stream object for Morgan middleware -const stream = { - write: (message: string) => { - logger.http(message.trim()); - }, -}; - -export { logger, stream }; \ No newline at end of file diff --git a/server/index.js b/server/index.js deleted file mode 100644 index 6df2748..0000000 --- a/server/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import dotenv from 'dotenv'; -import { PrismaClient } from '@prisma/client'; -import authRoutes from './routes/auth.js'; -import articleRoutes from './routes/articles'; -import userRoutes from './routes/users.js'; - -dotenv.config(); - -const app = express(); -const prisma = new PrismaClient(); - -app.use(cors()); -app.use(express.json()); - -// Routes -app.use('/api/auth', authRoutes); -app.use('/api/articles', articleRoutes); -app.use('/api/users', userRoutes); - -const PORT = process.env.PORT || 5000; - -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); -}); \ No newline at end of file diff --git a/server/index.ts b/server/index.ts deleted file mode 100644 index 959bd62..0000000 --- a/server/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import dotenv from 'dotenv'; -import { logger, stream } from './config/logger.js'; -import { requestLogger } from './middleware/logging/requestLogger.js'; -import { errorLogger } from './middleware/error/errorLogger.js'; -import authRoutes from './routes/auth/index.js'; -import articleRoutes from './routes/articles/index.js'; -import userRoutes from './routes/users/index.js'; - -// Load environment variables -dotenv.config(); - -const app = express(); - -// Middleware -app.use(cors()); -app.use(express.json()); -app.use(requestLogger); - -// Routes -app.use('/api/auth', authRoutes); -app.use('/api/articles', articleRoutes); -app.use('/api/users', userRoutes); - -// Error handling -app.use(errorLogger); - -const PORT = process.env.PORT || 5000; - -app.listen(PORT, () => { - logger.info(`Server running on port ${PORT}`); -}); - -// Handle uncaught exceptions -process.on('uncaughtException', (error) => { - logger.error('Uncaught Exception:', error); - process.exit(1); -}); - -// Handle unhandled promise rejections -process.on('unhandledRejection', (reason, promise) => { - logger.error('Unhandled Rejection at:', promise, 'reason:', reason); - process.exit(1); -}); \ No newline at end of file diff --git a/server/middleware/auth/auth.ts b/server/middleware/auth/auth.ts deleted file mode 100644 index 6de5ba2..0000000 --- a/server/middleware/auth/auth.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Response, NextFunction } from 'express'; -import { AuthRequest } from './types.js'; -import { extractToken } from './extractToken.js'; -import { validateToken } from './validateToken.js'; -import { getUser } from './getUser.js'; - -export async function auth(req: AuthRequest, res: Response, next: NextFunction) { - try { - const token = extractToken(req); - if (!token) { - return res.status(401).json({ error: 'No token provided' }); - } - - const payload = validateToken(token); - if (!payload) { - return res.status(401).json({ error: 'Invalid token' }); - } - - const user = await getUser(payload.id); - if (!user) { - return res.status(401).json({ error: 'User not found' }); - } - - req.user = user; - next(); - } catch { - res.status(401).json({ error: 'Authentication failed' }); - } -} \ No newline at end of file diff --git a/server/middleware/auth/extractToken.ts b/server/middleware/auth/extractToken.ts deleted file mode 100644 index f722490..0000000 --- a/server/middleware/auth/extractToken.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Request } from 'express'; - -export function extractToken(req: Request): string | null { - const authHeader = req.header('Authorization'); - if (!authHeader) return null; - - const [bearer, token] = authHeader.split(' '); - if (bearer !== 'Bearer' || !token) return null; - - return token; -} \ No newline at end of file diff --git a/server/middleware/auth/getUser.ts b/server/middleware/auth/getUser.ts deleted file mode 100644 index d2fa81a..0000000 --- a/server/middleware/auth/getUser.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { User } from '../../../src/types/auth.js'; - -const prisma = new PrismaClient(); - -export async function getUser(userId: string): Promise { - try { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - email: true, - displayName: true, - permissions: true - } - }); - - return user as User | null; - } catch { - return null; - } -} \ No newline at end of file diff --git a/server/middleware/auth/index.ts b/server/middleware/auth/index.ts deleted file mode 100644 index c775c10..0000000 --- a/server/middleware/auth/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { auth } from './auth.js'; -export { extractToken } from './extractToken.js'; -export { validateToken } from './validateToken.js'; -export { getUser } from './getUser.js'; -export * from './types.js'; \ No newline at end of file diff --git a/server/middleware/auth/types.ts b/server/middleware/auth/types.ts deleted file mode 100644 index c154991..0000000 --- a/server/middleware/auth/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Request } from 'express'; -import { User } from '../../../src/types/auth.js'; - -export interface AuthRequest extends Request { - user?: User; -} - -export interface JwtPayload { - id: string; - iat?: number; - exp?: number; -} \ No newline at end of file diff --git a/server/middleware/auth/validateToken.ts b/server/middleware/auth/validateToken.ts deleted file mode 100644 index 0d56d42..0000000 --- a/server/middleware/auth/validateToken.ts +++ /dev/null @@ -1,10 +0,0 @@ -import jwt from 'jsonwebtoken'; -import { JwtPayload } from './types.js'; - -export function validateToken(token: string): JwtPayload | null { - try { - return jwt.verify(token, process.env.JWT_SECRET || '') as JwtPayload; - } catch { - return null; - } -} \ No newline at end of file diff --git a/server/middleware/error/errorHandler.ts b/server/middleware/error/errorHandler.ts deleted file mode 100644 index 26ad5c3..0000000 --- a/server/middleware/error/errorHandler.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -export interface AppError extends Error { - statusCode?: number; -} - -export function errorHandler( - err: AppError, - req: Request, - res: Response, - next: NextFunction -) { - const statusCode = err.statusCode || 500; - const message = err.message || 'Internal Server Error'; - - res.status(statusCode).json({ - error: message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); -} \ No newline at end of file diff --git a/server/middleware/error/errorLogger.ts b/server/middleware/error/errorLogger.ts deleted file mode 100644 index b425a09..0000000 --- a/server/middleware/error/errorLogger.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { logger } from '../../config/logger.js'; - -export interface AppError extends Error { - statusCode?: number; - details?: never; -} - -export const errorLogger = ( - err: AppError, - req: Request, - res: Response, - next: NextFunction -) => { - const errorDetails = { - message: err.message, - stack: err.stack, - timestamp: new Date().toISOString(), - path: req.path, - method: req.method, - statusCode: err.statusCode || 500, - details: err.details, - }; - - logger.error('Application error:', errorDetails); - next(err); -}; \ No newline at end of file diff --git a/server/middleware/logging/requestLogger.ts b/server/middleware/logging/requestLogger.ts deleted file mode 100644 index dc81f37..0000000 --- a/server/middleware/logging/requestLogger.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { logger } from '../../config/logger.js'; - -export const requestLogger = (req: Request, res: Response, next: NextFunction) => { - const start = Date.now(); - - res.on('finish', () => { - const duration = Date.now() - start; - const message = `${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`; - - if (res.statusCode >= 500) { - logger.error(message); - } else if (res.statusCode >= 400) { - logger.warn(message); - } else { - logger.info(message); - } - }); - - next(); -}; \ No newline at end of file diff --git a/server/middleware/validation/validateRequest.ts b/server/middleware/validation/validateRequest.ts deleted file mode 100644 index beffd2a..0000000 --- a/server/middleware/validation/validateRequest.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { Schema } from 'zod'; - -export function validateRequest(schema: Schema) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - await schema.parseAsync({ - body: req.body, - query: req.query, - params: req.params - }); - next(); - } catch (error) { - res.status(400).json({ error: 'Invalid request data' }); - } - }; -} \ No newline at end of file diff --git a/server/routes/articles/controllers/crud.ts b/server/routes/articles/controllers/crud.ts deleted file mode 100644 index d2a570c..0000000 --- a/server/routes/articles/controllers/crud.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Request, Response } from 'express'; -import { prisma } from '../../../../src/lib/prisma'; -import { AuthRequest } from '../../../middleware/auth'; -import { checkPermission } from '../../../utils/permissions.js'; - -export async function getArticle(req: Request, res: Response) { - try { - const article = await prisma.article.findUnique({ - where: { id: req.params.id }, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - } - }); - - if (!article) { - return res.status(404).json({ error: 'Article not found' }); - } - - res.json(article); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function createArticle(req: AuthRequest, res: Response) { - try { - const { title, excerpt, content, category, city, coverImage, readTime } = req.body; - - if (!req.user || !checkPermission(req.user, category, 'create')) { - return res.status(403).json({ error: 'Permission denied' }); - } - - const article = await prisma.article.create({ - data: { - title, - excerpt, - content, - category, - city, - coverImage, - readTime, - authorId: req.user.id - }, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - } - }); - - res.status(201).json(article); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function updateArticle(req: AuthRequest, res: Response) { - try { - const article = await prisma.article.findUnique({ - where: { id: req.params.id } - }); - - if (!article) { - return res.status(404).json({ error: 'Article not found' }); - } - - if (!req.user || !checkPermission(req.user, article.category, 'edit')) { - return res.status(403).json({ error: 'Permission denied' }); - } - - const updatedArticle = await prisma.article.update({ - where: { id: req.params.id }, - data: req.body, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - } - }); - - res.json(updatedArticle); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function deleteArticle(req: AuthRequest, res: Response) { - try { - const article = await prisma.article.findUnique({ - where: { id: req.params.id } - }); - - if (!article) { - return res.status(404).json({ error: 'Article not found' }); - } - - if (!req.user || !checkPermission(req.user, article.category, 'delete')) { - return res.status(403).json({ error: 'Permission denied' }); - } - - await prisma.article.delete({ - where: { id: req.params.id } - }); - - res.json({ message: 'Article deleted successfully' }); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} \ No newline at end of file diff --git a/server/routes/articles/controllers/list.ts b/server/routes/articles/controllers/list.ts deleted file mode 100644 index 4df7e31..0000000 --- a/server/routes/articles/controllers/list.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Request, Response } from 'express'; -import { prisma } from '../../../../src/lib/prisma'; - -export async function listArticles(req: Request, res: Response) { - try { - const { page = 1, category, city } = req.query; - const perPage = 6; - - const where = { - ...(category && { category: category as string }), - ...(city && { city: city as string }) - }; - - const [articles, total] = await Promise.all([ - prisma.article.findMany({ - where, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - }, - skip: ((page as number) - 1) * perPage, - take: perPage, - orderBy: { publishedAt: 'desc' } - }), - prisma.article.count({ where }) - ]); - - res.json({ - articles, - totalPages: Math.ceil(total / perPage), - currentPage: parseInt(page as string) - }); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} \ No newline at end of file diff --git a/server/routes/articles/controllers/search.ts b/server/routes/articles/controllers/search.ts deleted file mode 100644 index ca9ed1b..0000000 --- a/server/routes/articles/controllers/search.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Request, Response } from 'express'; -import { prisma } from '../../../../src/lib/prisma'; - -export async function searchArticles(req: Request, res: Response) { - try { - const { q, page = 1, limit = 9 } = req.query; - const skip = ((page as number) - 1) * (limit as number); - - const where = { - OR: [ - { title: { contains: q as string, mode: 'insensitive' } }, - { excerpt: { contains: q as string, mode: 'insensitive' } }, - { content: { contains: q as string, mode: 'insensitive' } }, - ] - }; - - const [articles, total] = await Promise.all([ - prisma.article.findMany({ - where, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - }, - skip, - take: parseInt(limit as string), - orderBy: { publishedAt: 'desc' } - }), - prisma.article.count({ where }) - ]); - - res.json({ - articles, - totalPages: Math.ceil(total / (limit as number)), - currentPage: parseInt(page as string) - }); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} \ No newline at end of file diff --git a/server/routes/articles/crud.ts b/server/routes/articles/crud.ts deleted file mode 100644 index 9b08d19..0000000 --- a/server/routes/articles/crud.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Request, Response } from 'express'; -import { prisma } from '../../../src/lib/prisma'; - -export async function getArticle(req: Request, res: Response) { - try { - const article = await prisma.article.findUnique({ - where: { id: req.params.id }, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - } - }); - - if (!article) { - return res.status(404).json({ error: 'Article not found' }); - } - - res.json(article); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function createArticle(req: Request, res: Response) { - try { - const { title, excerpt, content, category, city, coverImage, readTime } = req.body; - - const article = await prisma.article.create({ - data: { - title, - excerpt, - content, - category, - city, - coverImage, - readTime, - authorId: req.user!.id - }, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - } - }); - - res.status(201).json(article); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function updateArticle(req: Request, res: Response) { - try { - const article = await prisma.article.update({ - where: { id: req.params.id }, - data: req.body, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - } - }); - - res.json(article); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function deleteArticle(req: Request, res: Response) { - try { - await prisma.article.delete({ - where: { id: req.params.id } - }); - - res.json({ message: 'Article deleted successfully' }); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} \ No newline at end of file diff --git a/server/routes/articles/index.ts b/server/routes/articles/index.ts deleted file mode 100644 index 2b5bd41..0000000 --- a/server/routes/articles/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import express from 'express'; -import { auth } from '../../middleware/auth'; -import { searchArticles } from './controllers/search.js'; -import { listArticles } from './controllers/list.js'; -import { getArticle, createArticle, updateArticle, deleteArticle } from './controllers/crud.js'; - -const router = express.Router(); - -// Search and list routes -router.get('/search', searchArticles); -router.get('/', listArticles); - -// CRUD routes -router.get('/:id', getArticle); -router.post('/', auth, createArticle); -router.put('/:id', auth, updateArticle); -router.delete('/:id', auth, deleteArticle); - -export default router; \ No newline at end of file diff --git a/server/routes/articles/list.ts b/server/routes/articles/list.ts deleted file mode 100644 index 578fa6d..0000000 --- a/server/routes/articles/list.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Request, Response } from 'express'; -import { prisma } from '../../../src/lib/prisma'; - -export async function listArticles(req: Request, res: Response) { - try { - const { page = 1, category, city } = req.query; - const perPage = 6; - - const where = { - ...(category && { category: category as string }), - ...(city && { city: city as string }) - }; - - const [articles, total] = await Promise.all([ - prisma.article.findMany({ - where, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - }, - skip: ((page as number) - 1) * perPage, - take: perPage, - orderBy: { publishedAt: 'desc' } - }), - prisma.article.count({ where }) - ]); - - res.json({ - articles, - totalPages: Math.ceil(total / perPage), - currentPage: parseInt(page as string) - }); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} \ No newline at end of file diff --git a/server/routes/articles/search.ts b/server/routes/articles/search.ts deleted file mode 100644 index 31efe06..0000000 --- a/server/routes/articles/search.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Request, Response } from 'express'; -import { prisma } from '../../../src/lib/prisma'; - -export async function searchArticles(req: Request, res: Response) { - try { - const { q, page = 1, limit = 9 } = req.query; - const skip = ((page as number) - 1) * (limit as number); - - const where = { - OR: [ - { title: { contains: q as string, mode: 'insensitive' } }, - { excerpt: { contains: q as string, mode: 'insensitive' } }, - { content: { contains: q as string, mode: 'insensitive' } }, - ] - }; - - const [articles, total] = await Promise.all([ - prisma.article.findMany({ - where, - include: { - author: { - select: { - id: true, - displayName: true, - email: true - } - } - }, - skip, - take: parseInt(limit as string), - orderBy: { publishedAt: 'desc' } - }), - prisma.article.count({ where }) - ]); - - res.json({ - articles, - totalPages: Math.ceil(total / (limit as number)), - currentPage: parseInt(page as string) - }); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} \ No newline at end of file diff --git a/server/routes/auth.ts b/server/routes/auth.ts deleted file mode 100644 index 8bf9fff..0000000 --- a/server/routes/auth.ts +++ /dev/null @@ -1,27 +0,0 @@ -import express from 'express'; -import { authService } from '../services/authService.js'; -import { auth } from '../middleware/auth'; - -const router = express.Router(); - -// Login route -router.post('/login', async (req, res) => { - try { - const { email, password } = req.body; - const { user, token } = await authService.login(email, password); - res.json({ user, token }); - } catch { - res.status(401).json({ error: 'Invalid credentials' }); - } -}); - -// Get current user route -router.get('/me', auth, async (req, res) => { - try { - res.json(req.user); - } catch { - res.status(500).json({ error: 'Server error' }); - } -}); - -export default router; \ No newline at end of file diff --git a/server/routes/auth/controllers/auth.ts b/server/routes/auth/controllers/auth.ts deleted file mode 100644 index 5addcc0..0000000 --- a/server/routes/auth/controllers/auth.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Request, Response } from 'express'; -import { AuthRequest } from '../../../middleware/auth'; -import { authService } from '../../../services/authService.js'; - -export async function login(req: Request, res: Response) { - try { - const { email, password } = req.body; - const { user, token } = await authService.login(email, password); - res.json({ user, token }); - } catch { - res.status(401).json({ error: 'Invalid credentials' }); - } -} - -export async function getCurrentUser(req: AuthRequest, res: Response) { - try { - if (!req.user) { - return res.status(401).json({ error: 'Not authenticated' }); - } - res.json(req.user); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function refreshToken(req: AuthRequest, res: Response) { - try { - if (!req.user) { - return res.status(401).json({ error: 'Not authenticated' }); - } - const token = await authService.generateToken(req.user.id); - res.json({ token }); - } catch { - res.status(500).json({ error: 'Failed to refresh token' }); - } -} \ No newline at end of file diff --git a/server/routes/auth/index.ts b/server/routes/auth/index.ts deleted file mode 100644 index 3db9e22..0000000 --- a/server/routes/auth/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import express from 'express'; -import { auth } from '../../middleware/auth'; -import { login, getCurrentUser } from './controllers/auth.js'; -import { validateRequest } from '../../middleware/validation/validateRequest.js'; -import { loginSchema } from './validation/authSchemas.js'; - -const router = express.Router(); - -router.post('/login', validateRequest(loginSchema), login); -router.get('/me', auth, getCurrentUser); - -export default router; \ No newline at end of file diff --git a/server/routes/auth/validation/authSchemas.ts b/server/routes/auth/validation/authSchemas.ts deleted file mode 100644 index 40bab48..0000000 --- a/server/routes/auth/validation/authSchemas.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from 'zod'; - -export const loginSchema = z.object({ - body: z.object({ - email: z.string().email(), - password: z.string().min(6) - }) -}); \ No newline at end of file diff --git a/server/routes/gallery/controllers/crud.ts b/server/routes/gallery/controllers/crud.ts deleted file mode 100644 index bb7384a..0000000 --- a/server/routes/gallery/controllers/crud.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Request, Response } from 'express'; -import { AuthRequest } from '../../../middleware/auth/types.js'; -import { galleryService } from '../../../services/galleryService.js'; -import { s3Service } from '../../../services/s3Service.js'; -import { logger } from '../../../config/logger.js'; - -export async function createGalleryImage(req: AuthRequest, res: Response) { - try { - const { articleId } = req.params; - const { url, caption, alt, width, height, size, format } = req.body; - - const image = await galleryService.createImage({ - url, - caption, - alt, - width, - height, - size, - format, - articleId - }); - - res.status(201).json(image); - } catch (error) { - logger.error('Error creating gallery image:', error); - res.status(500).json({ error: 'Failed to create gallery image' }); - } -} - -export async function updateGalleryImage(req: AuthRequest, res: Response) { - try { - const { id } = req.params; - const { caption, alt, order } = req.body; - - const image = await galleryService.updateImage(id, { - caption, - alt, - order - }); - - res.json(image); - } catch (error) { - logger.error('Error updating gallery image:', error); - res.status(500).json({ error: 'Failed to update gallery image' }); - } -} - -export async function deleteGalleryImage(req: AuthRequest, res: Response) { - try { - const { id } = req.params; - await galleryService.deleteImage(id); - res.json({ message: 'Gallery image deleted successfully' }); - } catch (error) { - logger.error('Error deleting gallery image:', error); - res.status(500).json({ error: 'Failed to delete gallery image' }); - } -} - -export async function reorderGalleryImages(req: AuthRequest, res: Response) { - try { - const { articleId } = req.params; - const { imageIds } = req.body; - - await galleryService.reorderImages(articleId, imageIds); - res.json({ message: 'Gallery images reordered successfully' }); - } catch (error) { - logger.error('Error reordering gallery images:', error); - res.status(500).json({ error: 'Failed to reorder gallery images' }); - } -} - -export async function getArticleGallery(req: Request, res: Response) { - try { - const { articleId } = req.params; - const images = await galleryService.getArticleGallery(articleId); - res.json(images); - } catch (error) { - logger.error('Error fetching article gallery:', error); - res.status(500).json({ error: 'Failed to fetch gallery images' }); - } -} \ No newline at end of file diff --git a/server/routes/gallery/index.ts b/server/routes/gallery/index.ts deleted file mode 100644 index afbe3ed..0000000 --- a/server/routes/gallery/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import express from 'express'; -import { auth } from '../../middleware/auth'; -import { - createGalleryImage, - updateGalleryImage, - deleteGalleryImage, - reorderGalleryImages, - getArticleGallery -} from './controllers/crud.js'; - -const router = express.Router(); - -router.get('/article/:articleId', getArticleGallery); -router.post('/article/:articleId', auth, createGalleryImage); -router.put('/:id', auth, updateGalleryImage); -router.delete('/:id', auth, deleteGalleryImage); -router.post('/article/:articleId/reorder', auth, reorderGalleryImages); - -export default router; \ No newline at end of file diff --git a/server/routes/images/index.ts b/server/routes/images/index.ts deleted file mode 100644 index cfc7668..0000000 --- a/server/routes/images/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import express from 'express'; -import multer from 'multer'; -import { auth } from '../../middleware/auth'; -import { s3Service } from '../../services/s3Service.js'; -import { logger } from '../../config/logger.js'; -import { imageResolutions } from '../../../src/config/imageResolutions.js'; - -const router = express.Router(); -const upload = multer(); - -router.post('/upload-url', auth, async (req, res) => { - try { - const { fileName, fileType, resolution } = req.body; - - const selectedResolution = imageResolutions.find(r => r.id === resolution); - if (!selectedResolution) { - throw new Error('Invalid resolution'); - } - - const { uploadUrl, imageId, key } = await s3Service.getUploadUrl(fileName, fileType); - - logger.info(`Generated upload URL for image: ${fileName}`); - res.json({ uploadUrl, imageId, key }); - } catch (error) { - logger.error('Error generating upload URL:', error); - res.status(500).json({ error: 'Failed to generate upload URL' }); - } -}); - -router.post('/process', auth, upload.single('image'), async (req, res) => { - try { - const { file } = req; - const { resolution } = req.body; - - if (!file) { - throw new Error('No file uploaded'); - } - - const selectedResolution = imageResolutions.find(r => r.id === resolution); - if (!selectedResolution) { - throw new Error('Invalid resolution'); - } - - const result = await s3Service.optimizeAndUpload( - file.buffer, - file.originalname, - selectedResolution - ); - - logger.info(`Successfully processed image: ${file.originalname}`); - res.json(result); - } catch (error) { - logger.error('Error processing image:', error); - res.status(500).json({ error: 'Failed to process image' }); - } -}); - -export default router; \ No newline at end of file diff --git a/server/routes/users.ts b/server/routes/users.ts deleted file mode 100644 index 8a40953..0000000 --- a/server/routes/users.ts +++ /dev/null @@ -1,35 +0,0 @@ -import express from 'express'; -import { userService } from '../services/userService.js'; -import { auth } from '../middleware/auth'; -import { Request, Response } from 'express'; -import { User } from '../../src/types/auth.ts'; - -const router = express.Router(); - -router.get('/', auth, async (req: Request, res: Response) => { - try { - if (!req.user?.permissions.isAdmin) { - return res.status(403).json({ error: 'Admin access required' }); - } - const users = await userService.getUsers(); - res.json(users); - } catch (error) { - res.status(500).json({ error: 'Server error' }); - } -}); - -router.put('/:id/permissions', auth, async (req: Request, res: Response) => { - try { - if (!req.user?.permissions.isAdmin) { - return res.status(403).json({ error: 'Admin access required' }); - } - const { id } = req.params; - const { permissions } = req.body; - const user = await userService.updateUserPermissions(id, permissions); - res.json(user); - } catch (error) { - res.status(500).json({ error: 'Server error' }); - } -}); - -export default router; \ No newline at end of file diff --git a/server/routes/users/controllers/users.ts b/server/routes/users/controllers/users.ts deleted file mode 100644 index 1bea794..0000000 --- a/server/routes/users/controllers/users.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Response } from 'express'; -import { AuthRequest } from '../../../middleware/auth'; -import { userService } from '../../../services/userService.js'; - -export async function getUsers(req: AuthRequest, res: Response) { - try { - if (!req.user?.permissions.isAdmin) { - return res.status(403).json({ error: 'Admin access required' }); - } - const users = await userService.getUsers(); - res.json(users); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} - -export async function updateUserPermissions(req: AuthRequest, res: Response) { - try { - if (!req.user?.permissions.isAdmin) { - return res.status(403).json({ error: 'Admin access required' }); - } - const { id } = req.params; - const { permissions } = req.body; - const user = await userService.updateUserPermissions(id, permissions); - res.json(user); - } catch { - res.status(500).json({ error: 'Server error' }); - } -} \ No newline at end of file diff --git a/server/routes/users/index.ts b/server/routes/users/index.ts deleted file mode 100644 index 2d8be10..0000000 --- a/server/routes/users/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import express from 'express'; -import { auth } from '../../middleware/auth'; -import { getUsers, updateUserPermissions } from './controllers/users.js'; - -const router = express.Router(); - -router.get('/', auth, getUsers); -router.put('/:id/permissions', auth, updateUserPermissions); - -export default router; \ No newline at end of file diff --git a/server/services/authService.ts b/server/services/authService.ts deleted file mode 100644 index 6a2f270..0000000 --- a/server/services/authService.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import bcrypt from 'bcryptjs'; -import jwt from 'jsonwebtoken'; -import { User } from '../../src/types/auth.js'; -import { logger } from '../config/logger.js'; - -const prisma = new PrismaClient(); - -export const authService = { - login: async (email: string, password: string) => { - try { - logger.info(`Login attempt for user: ${email}`); - - const user = await prisma.user.findUnique({ - where: { email }, - select: { - id: true, - email: true, - password: true, - displayName: true, - permissions: true - } - }); - - if (!user) { - logger.warn(`Login failed: User not found - ${email}`); - throw new Error('Invalid credentials'); - } - - const isValidPassword = await bcrypt.compare(password, user.password); - if (!isValidPassword) { - logger.warn(`Login failed: Invalid password for user - ${email}`); - throw new Error('Invalid credentials'); - } - - const token = await authService.generateToken(user.id); - const { password: _, ...userWithoutPassword } = user; - - logger.info(`User logged in successfully: ${email}`); - return { - user: userWithoutPassword as User, - token - }; - } catch (error) { - logger.error('Login error:', error); - throw error; - } - }, - - generateToken: async (userId: string) => { - try { - const token = jwt.sign( - { id: userId }, - process.env.JWT_SECRET || '', - { expiresIn: '24h' } - ); - logger.debug(`Generated token for user: ${userId}`); - return token; - } catch (error) { - logger.error('Token generation error:', error); - throw error; - } - }, - - createUser: async (userData: { - email: string; - password: string; - displayName: string; - permissions: any; - }) => { - try { - logger.info(`Creating new user: ${userData.email}`); - - const hashedPassword = await bcrypt.hash(userData.password, 10); - const user = await prisma.user.create({ - data: { - ...userData, - password: hashedPassword - }, - select: { - id: true, - email: true, - displayName: true, - permissions: true - } - }); - - logger.info(`User created successfully: ${userData.email}`); - return user as User; - } catch (error) { - logger.error('User creation error:', error); - throw error; - } - } -}; \ No newline at end of file diff --git a/server/services/galleryService.ts b/server/services/galleryService.ts deleted file mode 100644 index f6779a1..0000000 --- a/server/services/galleryService.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { logger } from '../config/logger.js'; - -const prisma = new PrismaClient(); - -export const galleryService = { - createImage: async (data: { - url: string; - caption: string; - alt: string; - width: number; - height: number; - size: number; - format: string; - articleId: string; - order?: number; - }) => { - try { - const image = await prisma.galleryImage.create({ - data - }); - logger.info(`Created gallery image: ${image.id}`); - return image; - } catch (error) { - logger.error('Error creating gallery image:', error); - throw error; - } - }, - - updateImage: async ( - id: string, - data: { - caption?: string; - alt?: string; - order?: number; - } - ) => { - try { - const image = await prisma.galleryImage.update({ - where: { id }, - data - }); - logger.info(`Updated gallery image: ${id}`); - return image; - } catch (error) { - logger.error(`Error updating gallery image ${id}:`, error); - throw error; - } - }, - - deleteImage: async (id: string) => { - try { - await prisma.galleryImage.delete({ - where: { id } - }); - logger.info(`Deleted gallery image: ${id}`); - } catch (error) { - logger.error(`Error deleting gallery image ${id}:`, error); - throw error; - } - }, - - reorderImages: async (articleId: string, imageIds: string[]) => { - try { - await prisma.$transaction( - imageIds.map((id, index) => - prisma.galleryImage.update({ - where: { id }, - data: { order: index } - }) - ) - ); - logger.info(`Reordered gallery images for article: ${articleId}`); - } catch (error) { - logger.error(`Error reordering gallery images for article ${articleId}:`, error); - throw error; - } - }, - - getArticleGallery: async (articleId: string) => { - try { - const images = await prisma.galleryImage.findMany({ - where: { articleId }, - orderBy: { order: 'asc' } - }); - return images; - } catch (error) { - logger.error(`Error fetching gallery for article ${articleId}:`, error); - throw error; - } - } -}; \ No newline at end of file diff --git a/server/services/s3Service.ts b/server/services/s3Service.ts deleted file mode 100644 index 076a7b9..0000000 --- a/server/services/s3Service.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { v4 as uuidv4 } from 'uuid'; -import sharp from 'sharp'; -import { logger } from '../config/logger.js'; - -const s3Client = new S3Client({ - region: process.env.AWS_REGION || 'us-east-1', - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' - } -}); - -const BUCKET_NAME = process.env.AWS_S3_BUCKET || ''; - -export const s3Service = { - getUploadUrl: async (fileName: string, fileType: string) => { - const imageId = uuidv4(); - const key = `uploads/${imageId}-${fileName}`; - - const command = new PutObjectCommand({ - Bucket: BUCKET_NAME, - Key: key, - ContentType: fileType - }); - - try { - const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); - logger.info(`Generated pre-signed URL for upload: ${key}`); - return { uploadUrl, imageId, key }; - } catch (error) { - logger.error('Error generating pre-signed URL:', error); - throw error; - } - }, - - optimizeAndUpload: async (buffer: Buffer, key: string, resolution: { width: number; height: number }) => { - try { - let sharpInstance = sharp(buffer); - - // Get image metadata - const metadata = await sharpInstance.metadata(); - - // Resize if resolution is specified - if (resolution.width > 0 && resolution.height > 0) { - sharpInstance = sharpInstance.resize(resolution.width, resolution.height, { - fit: 'inside', - withoutEnlargement: true - }); - } - - // Convert to WebP for better compression - const optimizedBuffer = await sharpInstance - .webp({ quality: 80 }) - .toBuffer(); - - // Upload optimized image - const optimizedKey = key.replace(/\.[^/.]+$/, '.webp'); - await s3Client.send(new PutObjectCommand({ - Bucket: BUCKET_NAME, - Key: optimizedKey, - Body: optimizedBuffer, - ContentType: 'image/webp' - })); - - logger.info(`Successfully optimized and uploaded image: ${optimizedKey}`); - - return { - key: optimizedKey, - width: metadata.width, - height: metadata.height, - format: 'webp', - size: optimizedBuffer.length - }; - } catch (error) { - logger.error('Error optimizing and uploading image:', error); - throw error; - } - } -}; \ No newline at end of file diff --git a/server/services/userService.ts b/server/services/userService.ts deleted file mode 100644 index e928897..0000000 --- a/server/services/userService.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { User } from '../../src/types/auth'; - -const prisma = new PrismaClient(); - -export const userService = { - getUsers: async (): Promise => { - try { - const users = await prisma.user.findMany({ - select: { - id: true, - email: true, - displayName: true, - permissions: true - } - }); - return users as User[]; - } catch (error) { - console.error('Error fetching users:', error); - throw new Error('Failed to fetch users'); - } - }, - - updateUserPermissions: async (userId: string, permissions: User['permissions']): Promise => { - try { - const updatedUser = await prisma.user.update({ - where: { id: userId }, - data: { - permissions: permissions as any - }, - select: { - id: true, - email: true, - displayName: true, - permissions: true - } - }); - return updatedUser as User; - } catch (error) { - console.error('Error updating user permissions:', error); - throw new Error('Failed to update user permissions'); - } - } -}; \ No newline at end of file diff --git a/server/utils/permissions.ts b/server/utils/permissions.ts deleted file mode 100644 index 1241b14..0000000 --- a/server/utils/permissions.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Category, City } from '../../src/types'; -import { User } from '../../src/types/auth'; - -export const checkPermission = ( - user: User, - category: Category, - action: 'create' | 'edit' | 'delete' -): boolean => { - if (user.permissions.isAdmin) return true; - return !!user.permissions.categories[category]?.[action]; -}; - -export const checkCityAccess = (user: User, city: City): boolean => { - if (user.permissions.isAdmin) return true; - return user.permissions.cities.includes(city); -}; \ No newline at end of file diff --git a/src/components/ArticleCard.tsx b/src/components/ArticleCard.tsx index bc2e168..9a9daed 100644 --- a/src/components/ArticleCard.tsx +++ b/src/components/ArticleCard.tsx @@ -1,6 +1,7 @@ import { Clock, ThumbsUp, MapPin } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Article } from '../types'; +import { getCategoryName } from '../utils/categories'; import MinutesWord from './MinutesWord.tsx'; interface ArticleCardProps { @@ -9,6 +10,8 @@ interface ArticleCardProps { } export function ArticleCard({ article, featured = false }: ArticleCardProps) { + const categoryName = getCategoryName(article.categoryId); + return (
- {article.category} + {categoryName} diff --git a/src/components/AuthGuard.tsx b/src/components/AuthGuard.tsx index 9dcf7d7..efc666d 100644 --- a/src/components/AuthGuard.tsx +++ b/src/components/AuthGuard.tsx @@ -31,13 +31,13 @@ export function AuthGuard({ children, requiredPermissions }: AuthGuardProps) { const { category, action } = requiredPermissions; if (!user.permissions.isAdmin) { - if (category && !user.permissions.categories[category]?.[action]) { + if (category && !user.permissions.categories[category.name]?.[action]) { return (

Access Denied

- You don't have permission to {action} articles in the {category} category. + You don't have permission to {action} articles in the {category.name} category.

diff --git a/src/components/FeaturedSection.tsx b/src/components/FeaturedSection.tsx index b728b4f..890b0c2 100644 --- a/src/components/FeaturedSection.tsx +++ b/src/components/FeaturedSection.tsx @@ -3,30 +3,32 @@ import { useLocation, useSearchParams } from 'react-router-dom'; import { ArticleCard } from './ArticleCard'; import { Pagination } from './Pagination'; import { articles } from '../data/mock'; +import { getCategoryId } from '../utils/categories'; +import { CategoryName } from '../types'; const ARTICLES_PER_PAGE = 6; export function FeaturedSection() { const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); - const category = searchParams.get('category'); + const categoryParam = searchParams.get('category') as CategoryName | null; const city = searchParams.get('city'); const currentPage = parseInt(searchParams.get('page') || '1', 10); const filteredArticles = useMemo(() => { return articles.filter(article => { - if (category && city) { - return article.category === category && article.city === city; + if (categoryParam && city) { + return article.categoryId === getCategoryId(categoryParam) && article.city === city; } - if (category) { - return article.category === category; + if (categoryParam) { + return article.categoryId === getCategoryId(categoryParam); } if (city) { return article.city === city; } return true; }); - }, [category, city]); + }, [categoryParam, city]); const totalPages = Math.ceil(filteredArticles.length / ARTICLES_PER_PAGE); @@ -61,7 +63,7 @@ export function FeaturedSection() {

{city ? `${city} ` : ''} - {category ? `${category} Статьи` : 'Тематические статьи'} + {categoryParam ? `${categoryParam} Статьи` : 'Тематические статьи'}

Показано {Math.min(currentPage * ARTICLES_PER_PAGE, filteredArticles.length)} из {filteredArticles.length} статей @@ -73,7 +75,7 @@ export function FeaturedSection() { ))}

diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx index 31bf776..fcacc06 100644 --- a/src/components/Footer/index.tsx +++ b/src/components/Footer/index.tsx @@ -9,67 +9,67 @@ export function Footer() {
{/* About Section */}
-

About CultureScope

+

О нас

- CultureScope is your premier destination for arts and culture coverage. - Founded in 2024, we bring you the latest in film, theater, music, sports, - and cultural stories from around the globe. + Культура двух столиц - это главное место, где вы можете найти информацию об искусстве и культуре. + Основав портал в 2015 году, мы предлагаем вам самые свежие новости о кино, + театре, музыке, спорте и культуре Москвы и Санкт-Петербурга.

{/* Quick Links */}
-

Quick Links

+

Быстрые ссылки

  • - Film + Кино
  • - Theater + Театр
  • - Music + Музыка
  • - Sports + Спорт
  • - Art + Исскуство
  • - Legends + Легенды
  • - Anniversaries + Юбилеи
  • - Memory + Память
@@ -77,12 +77,12 @@ export function Footer() { {/* Contact Info */}
-

Contact Us

+

Контакты

-

Our Partners

+

Наши партнеры

  • - - Metropolitan Museum + + Туроператор «Прогулки»
  • @@ -121,7 +121,7 @@ export function Footer() {

    - © {new Date().getFullYear()} CultureScope. All rights reserved. + © {new Date().getFullYear()} Культура двух столиц. Все права защищены.

    diff --git a/src/components/GalleryManager.tsx b/src/components/GalleryManager.tsx index 1eeed5b..b1b9419 100644 --- a/src/components/GalleryManager.tsx +++ b/src/components/GalleryManager.tsx @@ -49,11 +49,11 @@ export function GalleryManager({ images, onChange }: GalleryManagerProps) {
    {/* Add New Image */}
    -

    Add New Image

    +

    Добавить новое фото

    - Add Image + Добавить фото
    {/* Gallery Preview */}
    -

    Gallery Images

    +

    Состав галереи

    {images.map((image, index) => (
    void; + onError: (error: string) => void; +} + +export function CoverImageUpload({ coverImage, onImageUpload, onError }: CoverImageUploadProps) { + const fileInputRef = useRef(null); + + const handleFileSelect = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + // Проверка разрешения изображения + const resolution = imageResolutions.find(r => r.id === 'large'); + if (!resolution) + throw new Error('Invalid resolution'); + + const formData = new FormData(); + formData.append('file', file); + formData.append('resolutionId', resolution.id); + formData.append('folder', 'articles/1'); + + // Получение токена из локального хранилища + const token = localStorage.getItem('token'); + + // Отправка запроса на сервер + const response = await axios.post('/api/images/upload-url', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + 'Authorization': `Bearer ${token}`, // Передача токена аутентификации + }, + }); + + if (response.data?.fileUrl) { + onImageUpload(response.data.fileUrl); // Передача URL загруженного изображения + } + } catch (error) { + onError('Failed to upload image. Please try again.'); + console.error('Upload error:', error); + } + + // Сброс значения input для повторного выбора того же файла + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleClearImage = () => { + onImageUpload(''); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
    + +
    + {coverImage && ( +
    + Cover preview + +
    +
    + )} +
    + + + +
    +

    + Рекомендуемый размер: 1920x1080px. Поддерживаются PNG, JPG, JPEG или WebP до 10MB. +

    +
    +
    + ); +} diff --git a/src/components/ImageUpload/ImageUploader.tsx b/src/components/ImageUpload/ImageUploader.tsx index 2b30e40..7e0c1d2 100644 --- a/src/components/ImageUpload/ImageUploader.tsx +++ b/src/components/ImageUpload/ImageUploader.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { ImageDropzone } from './ImageDropzone'; import { ResolutionSelect } from './ResolutionSelect'; import { UploadProgress } from './UploadProgress'; diff --git a/src/data/mock.ts b/src/data/mock.ts index a84ca61..db12c27 100644 --- a/src/data/mock.ts +++ b/src/data/mock.ts @@ -1,15 +1,15 @@ -import { Article, Author } from '../types'; +import {Article, Author, CategoryName} from '../types'; export const authors: Author[] = [ { id: '1', - name: 'Elena Martinez', + name: 'Елена Маркова', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=150&h=150', bio: 'Cultural critic and art historian based in Barcelona', }, { id: '2', - name: 'James Chen', + name: 'Илья Золкин', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?auto=format&fit=crop&q=80&w=150&h=150', bio: 'Music journalist and classical pianist', }, @@ -21,7 +21,7 @@ export const articles: Article[] = [ title: 'The Renaissance of Independent Theater', excerpt: 'How small theater companies are revolutionizing modern storytelling through innovative approaches and community engagement.', content: 'In the heart of urban artistic communities, independent theater companies are crafting innovative narratives that challenge traditional storytelling methods.', - category: 'Theater', + categoryId: 2, // Theater city: 'New York', author: authors[0], coverImage: 'https://images.unsplash.com/photo-1503095396549-807759245b35?auto=format&fit=crop&q=80&w=2070', @@ -56,7 +56,7 @@ export const articles: Article[] = [ title: 'Evolution of Digital Art Museums', excerpt: 'Exploring the intersection of technology and traditional art spaces in the modern digital age.', content: 'As we venture further into the digital age, museums are adapting to new ways of presenting art that blend traditional curation with cutting-edge technology.', - category: 'Art', + categoryId: 5, // Art city: 'London', author: authors[1], coverImage: 'https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b?auto=format&fit=crop&q=80&w=2070', @@ -72,7 +72,7 @@ export const articles: Article[] = [ title: 'The Future of Classical Music', excerpt: 'Contemporary composers bridging the gap between traditional and modern musical expressions.', content: 'Classical music is experiencing a remarkable transformation as new composers blend traditional orchestral elements with contemporary influences.', - category: 'Music', + categoryId: 3, // Music city: 'London', author: authors[1], coverImage: 'https://images.unsplash.com/photo-1520523839897-bd0b52f945a0?auto=format&fit=crop&q=80&w=2070', @@ -88,7 +88,7 @@ export const articles: Article[] = [ title: 'Modern Literature in the Digital Age', excerpt: 'How e-books and digital platforms are changing the way we consume and create literature.', content: 'The digital revolution has transformed the literary landscape, offering new ways for authors to reach readers and for stories to be told.', - category: 'Art', + categoryId: 5, // Art city: 'New York', author: authors[0], coverImage: 'https://images.unsplash.com/photo-1524995997946-a1c2e315a42f?auto=format&fit=crop&q=80&w=2070', @@ -104,7 +104,7 @@ export const articles: Article[] = [ title: 'The Rise of Immersive Art Installations', excerpt: 'How interactive and immersive art is transforming gallery spaces worldwide.', content: 'Interactive art installations are revolutionizing the way we experience and engage with contemporary art.', - category: 'Art', + categoryId: 5, // Art city: 'New York', author: authors[0], coverImage: 'https://images.unsplash.com/photo-1547826039-bfc35e0f1ea8?auto=format&fit=crop&q=80&w=2070', @@ -120,7 +120,7 @@ export const articles: Article[] = [ title: 'Film Festivals in the Post-Pandemic Era', excerpt: 'How film festivals are adapting to hybrid formats and reaching wider audiences.', content: 'Film festivals are embracing digital platforms while maintaining the magic of in-person screenings.', - category: 'Film', + categoryId: 1, // Film city: 'London', author: authors[1], coverImage: 'https://images.unsplash.com/photo-1478720568477-152d9b164e26?auto=format&fit=crop&q=80&w=2070', @@ -136,10 +136,10 @@ export const articles: Article[] = [ title: 'Street Art: From Vandalism to Validation', excerpt: 'The evolution of street art and its acceptance in the contemporary art world.', content: 'Street art has transformed from a controversial form of expression to a celebrated art movement.', - category: 'Art', + categoryId: 5, // Art city: 'New York', author: authors[0], - coverImage: 'https://images.unsplash.com/photo-1499781350541-7783f6c6a0c8?auto=format&fit=crop&q=80&w=2070', + coverImage: 'https://images.unsplash.com/photo-1524995997946-a1c2e315a42f?auto=format&fit=crop&q=80&w=2070', gallery: [] , publishedAt: '2024-03-09T10:00:00Z', readTime: 6, @@ -152,7 +152,7 @@ export const articles: Article[] = [ title: 'The New Wave of Theater Technology', excerpt: 'How digital innovations are enhancing live theater performances.', content: 'Modern theater productions are incorporating cutting-edge technology to create immersive experiences.', - category: 'Theater', + categoryId: 2, // Theater city: 'London', author: authors[1], coverImage: 'https://images.unsplash.com/photo-1507676184212-d03ab07a01bf?auto=format&fit=crop&q=80&w=2070', diff --git a/src/data/mockUsers.ts b/src/data/mockUsers.ts index 775fd89..8b20e77 100644 --- a/src/data/mockUsers.ts +++ b/src/data/mockUsers.ts @@ -1,7 +1,17 @@ import { User } from '../types/auth'; import { Category, City } from '../types'; -const categories: Category[] = ['Film', 'Theater', 'Music', 'Sports', 'Art', 'Legends', 'Anniversaries', 'Memory']; +const categories: Category[] = [ + {id : 1, name : 'Film'}, + {id : 2, name : 'Theater'}, + {id : 3, name : 'Music'}, + {id : 4, name : 'Sports'}, + {id : 5, name : 'Art'}, + {id : 6, name : 'Legends'}, + {id : 7, name : 'Anniversaries'}, + {id : 8, name : 'Memory'} +]; + const cities: City[] = ['New York', 'London']; export const mockUsers: User[] = [ @@ -12,7 +22,7 @@ export const mockUsers: User[] = [ permissions: { categories: categories.reduce((acc, category) => ({ ...acc, - [category]: { create: true, edit: true, delete: true } + [category.id]: { create: true, edit: true, delete: true } }), {}), cities: cities, isAdmin: true diff --git a/src/hooks/useBackgroundImage.ts b/src/hooks/useBackgroundImage.ts index 682e13b..f2a16d2 100644 --- a/src/hooks/useBackgroundImage.ts +++ b/src/hooks/useBackgroundImage.ts @@ -1,4 +1,4 @@ -import { Category } from '../types'; +import { CategoryName } from '../types'; const backgroundImages = { Film: '/images/film-bg.avif?auto=format&fit=crop&q=80&w=2070', @@ -12,6 +12,6 @@ const backgroundImages = { default: 'https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b?auto=format&fit=crop&q=80&w=2070' }; -export function useBackgroundImage(category?: Category | null) { - return category ? backgroundImages[category] : backgroundImages.default; +export function useBackgroundImage(name?: CategoryName | null) { + return name ? backgroundImages[name] : backgroundImages.default; } \ No newline at end of file diff --git a/src/hooks/useUserManagement.ts b/src/hooks/useUserManagement.ts index d59527e..6e129ea 100644 --- a/src/hooks/useUserManagement.ts +++ b/src/hooks/useUserManagement.ts @@ -39,8 +39,8 @@ export function useUserManagement() { ...selectedUser.permissions, categories: { ...selectedUser.permissions.categories, - [category]: { - ...selectedUser.permissions.categories[category], + [category.name]: { + ...selectedUser.permissions.categories[category.name], [action]: value, }, }, diff --git a/src/pages/AdminPage.tsx b/src/pages/AdminPage.tsx index b74311f..2aa0512 100644 --- a/src/pages/AdminPage.tsx +++ b/src/pages/AdminPage.tsx @@ -1,20 +1,32 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; +import axios from "axios"; import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { Header } from '../components/Header'; import { GalleryManager } from '../components/GalleryManager'; -import { Category, City, GalleryImage } from '../types'; -import { ImagePlus, Bold, Italic, List, ListOrdered, Quote, Pencil, Trash2, ChevronLeft, ChevronRight } from 'lucide-react'; +import { CoverImageUpload } from '../components/ImageUpload/CoverImageUpload'; +import {Category, City, Article, GalleryImage, CategoryMap} from '../types'; +import { Bold, Italic, List, ListOrdered, Quote, Pencil, Trash2, ChevronLeft, ChevronRight } from 'lucide-react'; import { articles } from '../data/mock'; -const categories: Category[] = ['Film', 'Theater', 'Music', 'Sports', 'Art', 'Legends', 'Anniversaries', 'Memory']; +//const categories: Category[] = ['Film', 'Theater', 'Music', 'Sports', 'Art', 'Legends', 'Anniversaries', 'Memory']; +const categories: Category[] = ['Кино', 'Театр', 'Музыка', 'Спорт', 'Искусство', 'Легенды', 'Юбилеи', 'Память']; + +/* +const categorieIds: Category[] = CategoryNames.map((name, index) => ({ + id: index + 1, + name, +})); +*/ + + const cities: City[] = ['New York', 'London']; const ARTICLES_PER_PAGE = 5; export function AdminPage() { const [title, setTitle] = useState(''); const [excerpt, setExcerpt] = useState(''); - const [category, setCategory] = useState('Art'); + const [category, setCategory] = useState('Искусство'); const [city, setCity] = useState('New York'); const [coverImage, setCoverImage] = useState(''); const [readTime, setReadTime] = useState(5); @@ -25,6 +37,7 @@ export function AdminPage() { const [currentPage, setCurrentPage] = useState(1); const [filterCategory, setFilterCategory] = useState(''); const [filterCity, setFilterCity] = useState(''); + const [error, setError] = useState(null); const editor = useEditor({ extensions: [StarterKit], @@ -36,6 +49,25 @@ export function AdminPage() { }, }); +/* + useEffect(() => { + async function fetchArticles() { + try { + const response = await axios.get('/api/articles', { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }); + setArticlesList(response.data); + } catch (err) { + setError('Не удалось загрузить статьи'); + console.error(err); + } + } + fetchArticles(); + }, []); +*/ + const filteredArticles = useMemo(() => { return articlesList.filter(article => { if (filterCategory && filterCity) { @@ -82,12 +114,19 @@ export function AdminPage() { setShowDeleteModal(null); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + + const categoryId : number = CategoryMap[category]; + if (!categoryId) { + console.error('Invalid category name.'); + return; + } + const articleData = { title, excerpt, - category, + categoryId, city, coverImage, readTime, @@ -97,14 +136,22 @@ export function AdminPage() { if (editingId) { setArticlesList(prev => - prev.map(article => - article.id === editingId - ? { ...article, ...articleData } - : article - ) + prev.map(article => + article.id === editingId + ? {...article, ...articleData} + : article + ) ); setEditingId(null); } else { + // Создание новой статьи + const response = await axios.post('/api/articles', articleData, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}`, + }, + }); + setArticlesList(prev => [...prev, response.data]); + // In a real app, this would send data to an API console.log('Creating new article:', articleData); } @@ -126,13 +173,19 @@ export function AdminPage() {

    - {editingId ? 'Edit Article' : 'Create New Article'} + {editingId ? 'Редактировать статью' : 'Создать новую статью'}

    + {error && ( +
    + {error} +
    + )} +