diff --git a/.gitignore b/.gitignore index 363a911a..6a2131c9 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ Thumbs.db # Build output dist/ .cache/ + +# Test scripts +rate-limit-test.sh diff --git a/package-lock.json b/package-lock.json index 20692649..c1ae6cef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "icpc-platform", "version": "0.1.0", "dependencies": { + "@better-auth/utils": "^0.2.6", "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", @@ -27,8 +28,11 @@ "@radix-ui/react-tooltip": "^1.1.8", "@svgr/webpack": "^8.1.0", "@tailwindcss/postcss": "^4.0.6", + "@upstash/ratelimit": "^2.0.6", + "@upstash/redis": "^1.35.3", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", + "better-auth": "^1.3.7", "bootstrap": "^5.3.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1777,6 +1781,20 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/utils": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.6.tgz", + "integrity": "sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" + }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", @@ -2899,6 +2917,12 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, "node_modules/@hookform/resolvers": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", @@ -3444,6 +3468,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -3859,6 +3889,27 @@ "node": ">= 10" } }, + "node_modules/@noble/ciphers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.6.0.tgz", + "integrity": "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3919,6 +3970,64 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@peculiar/asn1-android": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.4.0.tgz", + "integrity": "sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.4.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.4.0.tgz", + "integrity": "sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-x509": "^2.4.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.4.0.tgz", + "integrity": "sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-x509": "^2.4.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.4.0.tgz", + "integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.4.0.tgz", + "integrity": "sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.4.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4822,6 +4931,30 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/@simplewebauthn/browser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.2.tgz", + "integrity": "sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw==", + "license": "MIT" + }, + "node_modules/@simplewebauthn/server": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.2.tgz", + "integrity": "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.3.10", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -5764,6 +5897,39 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@upstash/core-analytics": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@upstash/core-analytics/-/core-analytics-0.0.10.tgz", + "integrity": "sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ==", + "license": "MIT", + "dependencies": { + "@upstash/redis": "^1.28.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@upstash/ratelimit": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@upstash/ratelimit/-/ratelimit-2.0.6.tgz", + "integrity": "sha512-Uak5qklMfzFN5RXltxY6IXRENu+Hgmo9iEgMPOlUs2etSQas2N+hJfbHw37OUy4vldLRXeD0OzL+YRvO2l5acg==", + "license": "MIT", + "dependencies": { + "@upstash/core-analytics": "^0.0.10" + }, + "peerDependencies": { + "@upstash/redis": "^1.34.3" + } + }, + "node_modules/@upstash/redis": { + "version": "1.35.3", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.35.3.tgz", + "integrity": "sha512-hSjv66NOuahW3MisRGlSgoszU2uONAY2l5Qo3Sae8OT3/Tng9K+2/cBRuyPBX8egwEGcNNCF9+r0V6grNnhL+w==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -6079,6 +6245,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -6297,6 +6477,58 @@ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, + "node_modules/better-auth": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.3.7.tgz", + "integrity": "sha512-/1fEyx2SGgJQM5ujozDCh9eJksnVkNU/J7Fk/tG5Y390l8nKbrPvqiFlCjlMM+scR+UABJbQzA6An7HT50LHyQ==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "0.2.6", + "@better-fetch/fetch": "^1.1.18", + "@noble/ciphers": "^0.6.0", + "@noble/hashes": "^1.8.0", + "@simplewebauthn/browser": "^13.1.2", + "@simplewebauthn/server": "^13.1.2", + "better-call": "^1.0.13", + "defu": "^6.1.4", + "jose": "^5.10.0", + "kysely": "^0.28.5", + "nanostores": "^0.11.4" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/better-auth/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/better-call": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.15.tgz", + "integrity": "sha512-u4ZNRB1yBx5j3CltTEbY2ZoFPVcgsuvciAqTEmPvnZpZ483vlZf4LGJ5aVau1yMlrvlyHxOCica3OqXBLhmsUw==", + "dependencies": { + "@better-fetch/fetch": "^1.1.4", + "rou3": "^0.5.1", + "set-cookie-parser": "^2.7.1", + "uncrypto": "^0.1.3" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -7070,6 +7302,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -9733,6 +9971,15 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.5.tgz", + "integrity": "sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -10320,6 +10567,21 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanostores": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz", + "integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11528,6 +11790,24 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12006,6 +12286,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rou3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz", + "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12146,6 +12432,12 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -13343,6 +13635,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -13788,9 +14086,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index c979c572..f09345a7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@better-auth/utils": "^0.2.6", "@emotion/cache": "^11.14.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", @@ -28,8 +29,11 @@ "@radix-ui/react-tooltip": "^1.1.8", "@svgr/webpack": "^8.1.0", "@tailwindcss/postcss": "^4.0.6", + "@upstash/ratelimit": "^2.0.6", + "@upstash/redis": "^1.35.3", "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", + "better-auth": "^1.3.7", "bootstrap": "^5.3.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/app/(admin-only)/api/create-training/route.ts b/src/app/(admin-only)/api/create-training/route.ts index d2fe4a17..28f79d03 100644 --- a/src/app/(admin-only)/api/create-training/route.ts +++ b/src/app/(admin-only)/api/create-training/route.ts @@ -1,11 +1,11 @@ import { db } from "@/lib/db"; import { Trainings } from "@/lib/db/schema/training/Trainings"; import { NextRequest, NextResponse } from "next/server"; -import { userData } from "@/lib/session"; -import adminOnly from "@/middelwares/adminOnly"; +import { withAdminAuth, type AuthenticatedUser } from "@/lib/auth-middleware"; +import { rateLimit } from "@/lib/rate-limit"; import expectedBody from "./_expectedBody"; -async function POSTfn(request: NextRequest, user: userData) { +async function POSTfn(request: NextRequest, user: AuthenticatedUser) { try { const { success, data: trainingData } = expectedBody.safeParse( await request.json(), @@ -15,9 +15,9 @@ async function POSTfn(request: NextRequest, user: userData) { } const training = { ...trainingData, - headId: user.userId, + headId: parseInt(user.id), // Convert string ID to number }; - db.insert(Trainings).values(training).execute(); + await db.insert(Trainings).values(training).execute(); return new NextResponse(null, { status: 201 }); } catch (e) { console.log(e); @@ -25,5 +25,9 @@ async function POSTfn(request: NextRequest, user: userData) { } } -const POST = adminOnly(POSTfn); -export { POST } ; +// Apply rate limiting and admin auth +const POST = rateLimit({ maxRequests: 5, windowMs: 60000 })( + withAdminAuth(POSTfn) +); + +export { POST }; diff --git a/src/app/(authorized-only)/api/edit-profile/route.ts b/src/app/(authorized-only)/api/edit-profile/route.ts index 7dd5962d..d8378478 100644 --- a/src/app/(authorized-only)/api/edit-profile/route.ts +++ b/src/app/(authorized-only)/api/edit-profile/route.ts @@ -1,13 +1,13 @@ import { db } from "@/lib/db"; import { UsersFullData } from "@/lib/db/schema/user/UsersFullData"; -import { type userData } from "@/lib/session"; -import authOnly from "@/middelwares/authOnly"; +import { withAuth, type AuthenticatedUser } from "@/lib/auth-middleware"; +import { rateLimit } from "@/lib/rate-limit"; import { eq } from "drizzle-orm"; import { NextResponse, type NextRequest } from "next/server"; import { userFullData as userFulldataValidations } from "@/lib/validation/userFulldataValidations"; import { Users } from "@/lib/db/schema/user/Users"; -async function POSTfn(request: NextRequest, user: userData) { +async function POSTfn(request: NextRequest, user: AuthenticatedUser) { try { const { success, data } = userFulldataValidations.safeParse( await request.json(), @@ -15,23 +15,24 @@ async function POSTfn(request: NextRequest, user: userData) { if (!success) { return new NextResponse(null, { status: 400 }); } + const userId = parseInt(user.id); const userFullData = await db .select() .from(UsersFullData) - .where(eq(UsersFullData.userId, user.userId)) + .where(eq(UsersFullData.userId, userId)) .execute(); if (userFullData.length === 0) { const userData = ( await db .select({ cfHandle: Users.cfHandle }) .from(Users) - .where(eq(Users.userId, user.userId)) + .where(eq(Users.userId, userId)) .execute() )[0]; await db .insert(UsersFullData) .values({ - userId: user.userId, + userId: userId, cfHandle: userData.cfHandle, username: user.username, }) @@ -40,7 +41,7 @@ async function POSTfn(request: NextRequest, user: userData) { await db .update(UsersFullData) .set(data) - .where(eq(UsersFullData.userId, user.userId)) + .where(eq(UsersFullData.userId, userId)) .execute(); return new NextResponse(null, { status: 201 }); } catch (e) { @@ -49,5 +50,9 @@ async function POSTfn(request: NextRequest, user: userData) { } } -const POST = authOnly(POSTfn); +// Apply rate limiting and auth +const POST = rateLimit({ maxRequests: 10, windowMs: 60000 })( + withAuth(POSTfn) +); + export { POST }; diff --git a/src/app/(authorized-only)/api/logout/route.ts b/src/app/(authorized-only)/api/logout/route.ts index df0b5682..6f0b38ee 100644 --- a/src/app/(authorized-only)/api/logout/route.ts +++ b/src/app/(authorized-only)/api/logout/route.ts @@ -1,4 +1,20 @@ +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; -export function GET() { +export async function POST() { + try { + await auth.api.signOut({ + headers: await headers(), + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Logout error:", error); + return NextResponse.json( + { error: "Failed to logout" }, + { status: 500 } + ); + } } diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 00000000..22f57aa9 --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,9 @@ +import { auth } from "@/lib/auth"; + +export async function GET(request: Request) { + return auth.handler(request); +} + +export async function POST(request: Request) { + return auth.handler(request); +} \ No newline at end of file diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index d0911905..7070049e 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -3,6 +3,7 @@ import { serialize } from "cookie"; import { db } from "@/lib/db"; import { Users } from "@/lib/db/schema/user/Users"; import { encryptSession } from "@/lib/session"; +import { rateLimit } from "@/lib/rate-limit"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; import { z } from "zod"; @@ -15,7 +16,8 @@ const errorResponse = NextResponse.json( { error: "Invalid username or password" }, { status: 401 }, ); -export async function POST(request: NextRequest) { + +async function handlePOST(request: NextRequest) { try { // Extracting credentials from the request body @@ -72,3 +74,10 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Failed to log in" }, { status: 500 }); } } + +// Apply rate limiting to login attempts +export const POST = rateLimit({ + maxRequests: 5, + windowMs: 60000, // 1 minute + skipSuccessfulRequests: true +})(handlePOST); diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 050c120e..77ebc626 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { db } from "@/lib/db"; import { Users } from "@/lib/db/schema/user/Users"; +import { rateLimit } from "@/lib/rate-limit"; import { eq, or } from "drizzle-orm"; import bcrypt from "bcryptjs"; import expectedBody from "./expectedBody"; @@ -8,7 +9,7 @@ import { EmailAuth } from "@/lib/db/schema/user/EmailAuth"; import sendEmail from "@/lib/email/sendEmail"; import { UsersFullData } from "@/lib/db/schema/user/UsersFullData"; -export async function POST(request: NextRequest) { +async function handlePOST(request: NextRequest) { try { const { success, data: registerData } = expectedBody.safeParse( await request.json(), @@ -97,6 +98,13 @@ export async function POST(request: NextRequest) { } } +// Apply rate limiting to registration attempts +export const POST = rateLimit({ + maxRequests: 3, + windowMs: 60000, // 1 minute + skipSuccessfulRequests: true +})(handlePOST); + function emailActivation( registerData: typeof Users.$inferInsert, randomToken: string, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6f006af2..db288570 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +// import { Inter } from "next/font/google"; import "./globals.css"; import { Navbar } from "@/components/NavBar"; import { ThemeProvider } from "next-themes"; @@ -7,7 +7,7 @@ import { decryptSession } from "@/lib/session"; import { cookies } from "next/headers"; import UserProvider from "@/providers/user"; -const inter = Inter({ subsets: ["latin"] }); +// const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "ICPC Platform", @@ -24,7 +24,7 @@ export default async function RootLayout({ const user = await decryptSession(session); return ( -
+