From ca0dc14fd0e7bda3158546d96c6fb3621a930e3b Mon Sep 17 00:00:00 2001 From: Freny07 Date: Sat, 11 Apr 2026 21:28:46 +0530 Subject: [PATCH 01/20] Added Mongodb.ts for auth setup --- lib/mongodb.ts | 20 +++++ package.json | 4 + pnpm-lock.yaml | 210 ++++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 3 + 4 files changed, 237 insertions(+) create mode 100644 lib/mongodb.ts create mode 100644 pnpm-workspace.yaml diff --git a/lib/mongodb.ts b/lib/mongodb.ts new file mode 100644 index 0000000..70af766 --- /dev/null +++ b/lib/mongodb.ts @@ -0,0 +1,20 @@ +import { MongoClient } from "mongodb" + +const uri = process.env.MONGODB_URI! +const options = {} + +declare global { + var _mongoClientPromise: Promise | undefined +} + +let client: MongoClient +let clientPromise: Promise + +if (!global._mongoClientPromise) { + client = new MongoClient(uri, options) + global._mongoClientPromise = client.connect() +} + +clientPromise = global._mongoClientPromise + +export default clientPromise \ No newline at end of file diff --git a/package.json b/package.json index 9f1a2c5..63ae326 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,13 @@ "db:seed": "node scripts/seed.js" }, "dependencies": { + "@auth/mongodb-adapter": "^3.11.1", + "bcrypt": "^6.0.0", "dotenv": "^17.4.1", + "mongodb": "^7.1.1", "mongoose": "^9.4.1", "next": "16.2.3", + "next-auth": "^4.24.13", "react": "19.2.4", "react-dom": "19.2.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7f286f..c058035 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,27 @@ importers: .: dependencies: + '@auth/mongodb-adapter': + specifier: ^3.11.1 + version: 3.11.1(mongodb@7.1.1) + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 dotenv: specifier: ^17.4.1 version: 17.4.1 + mongodb: + specifier: ^7.1.1 + version: 7.1.1 mongoose: specifier: ^9.4.1 version: 9.4.1 next: specifier: 16.2.3 version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-auth: + specifier: ^4.24.13 + version: 4.24.13(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 19.2.4 version: 19.2.4 @@ -55,6 +67,25 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@auth/core@0.41.1': + resolution: {integrity: sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^7.0.7 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + '@auth/mongodb-adapter@3.11.1': + resolution: {integrity: sha512-xY+VUkC3CNXct8UwQgBAQqXASqolSlIARg6oAm1378CtRN2650tQUCOEnGLNLmroVefUeP73M6t+TpGXq72vwQ==} + peerDependencies: + mongodb: ^6 + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -110,6 +141,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -434,6 +469,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -810,6 +848,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} @@ -869,6 +911,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1383,6 +1429,12 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1523,6 +1575,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1610,6 +1666,20 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + next-auth@4.24.13: + resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==} + peerDependencies: + '@auth/core': 0.34.3 + next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 + nodemailer: ^7.0.7 + react: ^17.0.2 || ^18 || ^19 + react-dom: ^17.0.2 || ^18 || ^19 + peerDependenciesMeta: + '@auth/core': + optional: true + nodemailer: + optional: true + next@16.2.3: resolution: {integrity: sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==} engines: {node: '>=20.9.0'} @@ -1631,17 +1701,35 @@ packages: sass: optional: true + node-addon-api@8.7.0: + resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} + engines: {node: ^18 || ^20 || >= 21} + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + oauth4webapi@3.8.5: + resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} + + oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -1670,6 +1758,13 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + oidc-token-hash@5.2.0: + resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} + engines: {node: ^10.13.0 || >=12.0.0} + + openid-client@5.7.1: + resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1724,10 +1819,29 @@ packages: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@5.2.6: + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + peerDependencies: + preact: '>=10' + + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + + preact@10.29.1: + resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -1992,6 +2106,10 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -2028,6 +2146,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2045,6 +2166,23 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@auth/core@0.41.1': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.2.2 + oauth4webapi: 3.8.5 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + + '@auth/mongodb-adapter@3.11.1(mongodb@7.1.1)': + dependencies: + '@auth/core': 0.41.1 + mongodb: 7.1.1 + transitivePeerDependencies: + - '@simplewebauthn/browser' + - '@simplewebauthn/server' + - nodemailer + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -2122,6 +2260,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/runtime@7.29.2': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -2389,6 +2529,8 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@panva/hkdf@1.2.1': {} + '@rtsao/scc@1.1.0': {} '@swc/helpers@0.5.15': @@ -2749,6 +2891,11 @@ snapshots: baseline-browser-mapping@2.10.17: {} + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 @@ -2810,6 +2957,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.7.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3478,6 +3627,10 @@ snapshots: jiti@2.6.1: {} + jose@4.15.9: {} + + jose@6.2.2: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -3585,6 +3738,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3650,6 +3807,21 @@ snapshots: natural-compare@1.4.0: {} + next-auth@4.24.13(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + '@babel/runtime': 7.29.2 + '@panva/hkdf': 1.2.1 + cookie: 0.7.2 + jose: 4.15.9 + next: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + oauth: 0.9.15 + openid-client: 5.7.1 + preact: 10.29.1 + preact-render-to-string: 5.2.6(preact@10.29.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + uuid: 8.3.2 + next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.3 @@ -3674,6 +3846,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@8.7.0: {} + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -3681,10 +3855,18 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + node-gyp-build@4.8.4: {} + node-releases@2.0.37: {} + oauth4webapi@3.8.5: {} + + oauth@0.9.15: {} + object-assign@4.1.1: {} + object-hash@2.2.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -3725,6 +3907,15 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + oidc-token-hash@5.2.0: {} + + openid-client@5.7.1: + dependencies: + jose: 4.15.9 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.2.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3778,8 +3969,23 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@5.2.6(preact@10.29.1): + dependencies: + preact: 10.29.1 + pretty-format: 3.8.0 + + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + + preact@10.29.1: {} + prelude-ls@1.2.1: {} + pretty-format@3.8.0: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -4156,6 +4362,8 @@ snapshots: dependencies: punycode: 2.3.1 + uuid@8.3.2: {} + webidl-conversions@7.0.0: {} whatwg-url@14.2.0: @@ -4212,6 +4420,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.3.6): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..dd1b4fd --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + sharp: true + unrs-resolver: true From 2980851b7f2b80eb0c052cf82b7095f802300301 Mon Sep 17 00:00:00 2001 From: Freny07 Date: Sat, 11 Apr 2026 21:51:48 +0530 Subject: [PATCH 02/20] feat: setup Auth.js with MongoDB --- app/api/auth/[...nextauth]/route.ts | 3 +++ lib/auth.ts | 32 +++++++++++++++++++++++++++++ lib/mongodb.ts | 2 ++ package.json | 1 + pnpm-lock.yaml | 25 ++++++++++++++++------ 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 lib/auth.ts diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..cab6e6d --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/lib/auth" + +export { GET, POST } from "@/lib/auth" \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..43fa2cb --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,32 @@ +import NextAuth from "next-auth" +import Google from "next-auth/providers/google" +import Email from "next-auth/providers/email" +import { MongoDBAdapter } from "@auth/mongodb-adapter" +import clientPromise from "./mongodb" + +const handler = NextAuth({ + adapter: MongoDBAdapter(clientPromise), + + providers: [ + Email({ + server: process.env.EMAIL_SERVER, + from: process.env.EMAIL_FROM, + }), + + Google({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + ], + + session: { + strategy: "database", + maxAge: 30 * 24 * 60 * 60, + }, + + pages: { + signIn: "/login", + }, +}) + +export { handler as GET, handler as POST } \ No newline at end of file diff --git a/lib/mongodb.ts b/lib/mongodb.ts index 70af766..30ca5e3 100644 --- a/lib/mongodb.ts +++ b/lib/mongodb.ts @@ -1,3 +1,5 @@ +console.log("MONGODB_URI:", process.env.MONGODB_URI) + import { MongoClient } from "mongodb" const uri = process.env.MONGODB_URI! diff --git a/package.json b/package.json index 63ae326..405be6d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "mongoose": "^9.4.1", "next": "16.2.3", "next-auth": "^4.24.13", + "nodemailer": "^8.0.5", "react": "19.2.4", "react-dom": "19.2.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c058035..b858a8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@auth/mongodb-adapter': specifier: ^3.11.1 - version: 3.11.1(mongodb@7.1.1) + version: 3.11.1(mongodb@7.1.1)(nodemailer@8.0.5) bcrypt: specifier: ^6.0.0 version: 6.0.0 @@ -28,7 +28,10 @@ importers: version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-auth: specifier: ^4.24.13 - version: 4.24.13(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 4.24.13(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.5)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + nodemailer: + specifier: ^8.0.5 + version: 8.0.5 react: specifier: 19.2.4 version: 19.2.4 @@ -1716,6 +1719,10 @@ packages: node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + nodemailer@8.0.5: + resolution: {integrity: sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==} + engines: {node: '>=6.0.0'} + oauth4webapi@3.8.5: resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} @@ -2166,17 +2173,19 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@auth/core@0.41.1': + '@auth/core@0.41.1(nodemailer@8.0.5)': dependencies: '@panva/hkdf': 1.2.1 jose: 6.2.2 oauth4webapi: 3.8.5 preact: 10.24.3 preact-render-to-string: 6.5.11(preact@10.24.3) + optionalDependencies: + nodemailer: 8.0.5 - '@auth/mongodb-adapter@3.11.1(mongodb@7.1.1)': + '@auth/mongodb-adapter@3.11.1(mongodb@7.1.1)(nodemailer@8.0.5)': dependencies: - '@auth/core': 0.41.1 + '@auth/core': 0.41.1(nodemailer@8.0.5) mongodb: 7.1.1 transitivePeerDependencies: - '@simplewebauthn/browser' @@ -3807,7 +3816,7 @@ snapshots: natural-compare@1.4.0: {} - next-auth@4.24.13(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next-auth@4.24.13(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.5)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 '@panva/hkdf': 1.2.1 @@ -3821,6 +3830,8 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) uuid: 8.3.2 + optionalDependencies: + nodemailer: 8.0.5 next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: @@ -3859,6 +3870,8 @@ snapshots: node-releases@2.0.37: {} + nodemailer@8.0.5: {} + oauth4webapi@3.8.5: {} oauth@0.9.15: {} From bea1245c5b2ed5ac4b7b2a3ecdd0e5947a18f9ed Mon Sep 17 00:00:00 2001 From: Freny07 Date: Sat, 11 Apr 2026 22:13:59 +0530 Subject: [PATCH 03/20] feat: Implemented email link authentication with domain restriction to only iiitl.ac.in emails using resend --- app/login/LoginClient.tsx | 70 +++++++++++++++++++++++++++++++++++++++ app/login/page.tsx | 48 +++------------------------ lib/auth.ts | 29 ++++++++++++---- package.json | 3 +- pnpm-lock.yaml | 54 ++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 50 deletions(-) create mode 100644 app/login/LoginClient.tsx diff --git a/app/login/LoginClient.tsx b/app/login/LoginClient.tsx new file mode 100644 index 0000000..924638a --- /dev/null +++ b/app/login/LoginClient.tsx @@ -0,0 +1,70 @@ +"use client" + +import Link from "next/link" +import { Section } from "@/components/Section" +import { signIn } from "next-auth/react" +import { useState } from "react" + +export default function LoginClient() { + const [email, setEmail] = useState("") + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + + await signIn("email", { + email, + callbackUrl: "/", + }) + } + + return ( +
+
+

+ Sign in to access the alumni directory, events, and the job board. +

+ +
+
+ + setEmail(e.target.value)} + className="mt-1 h-10 w-full rounded-md border border-border bg-background px-3 text-sm" + /> +
+ +
+ + +
+ + +
+ + + +

+ New to IIITL Alumni?{" "} + + Create an account + +

+
+
+ ) +} \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx index 52a6147..a72fccf 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,45 +1,7 @@ -import Link from "next/link"; -import { Section } from "@/components/Section"; +import LoginClient from "./LoginClient" -export const metadata = { title: "Sign in" }; +export const metadata = { title: "Sign in" } -export default function LoginPage() { - return ( -
-
-

Welcome back

-

- Sign in to access the alumni directory, events, and the job board. -

-
-
- - -
-
- - -
- -
-

- New to IIITL Alumni?{" "} - - Create an account - -

-
-
- ); -} +export default function Page() { + return +} \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts index 43fa2cb..c6b00b2 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,24 +1,41 @@ import NextAuth from "next-auth" -import Google from "next-auth/providers/google" import Email from "next-auth/providers/email" import { MongoDBAdapter } from "@auth/mongodb-adapter" import clientPromise from "./mongodb" +import { Resend } from "resend" + +const resend = new Resend(process.env.RESEND_API_KEY) const handler = NextAuth({ adapter: MongoDBAdapter(clientPromise), providers: [ Email({ - server: process.env.EMAIL_SERVER, from: process.env.EMAIL_FROM, - }), - Google({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + async sendVerificationRequest({ identifier, url }) { + console.log("Sending email to:", identifier) + + await resend.emails.send({ + from: process.env.EMAIL_FROM!, + to: identifier, + subject: "Sign in to IIITL Alumni", + html: `

Click to sign in:

${url}`, + }) + }, }), ], + callbacks: { + async signIn({ user }) { + if (user?.email && !user.email.toLowerCase().endsWith("@iiitl.ac.in")) { + console.log("Rejected email:", user.email) + return false + } + return true + }, +}, + session: { strategy: "database", maxAge: 30 * 24 * 60 * 60, diff --git a/package.json b/package.json index 405be6d..4496485 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "next-auth": "^4.24.13", "nodemailer": "^8.0.5", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "resend": "^6.10.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b858a8d..214b8be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + resend: + specifier: ^6.10.0 + version: 6.10.0 devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -478,6 +481,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1167,6 +1173,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1818,6 +1827,9 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postal-mime@2.7.4: + resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -1879,6 +1891,15 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + resend@6.10.0: + resolution: {integrity: sha512-i7CwZpYj4Oho1RxsTpLcCUkO08+HiL4NXrm6jLJ2WzJ89UGI8eROSieLONJA3hnUrf1OYnCyfq5F6POnHUMv1Q==} + engines: {node: '>=20'} + peerDependencies: + '@react-email/render': '*' + peerDependenciesMeta: + '@react-email/render': + optional: true + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1975,6 +1996,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -2031,6 +2055,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svix@1.88.0: + resolution: {integrity: sha512-vm/JrrUd3bVyBE+3L33TIyVSs8gS5fYx7lrISvKlDJXTYX1ACH4REX8P1tHxsSKoZi/rvifM1t0XRc5Vc45THw==} + tailwindcss@4.2.2: resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} @@ -2113,6 +2140,10 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -2542,6 +2573,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@stablelib/base64@1.0.1': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -3366,6 +3399,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3970,6 +4005,8 @@ snapshots: possible-typed-array-names@1.1.0: {} + postal-mime@2.7.4: {} + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -4038,6 +4075,11 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + resend@6.10.0: + dependencies: + postal-mime: 2.7.4 + svix: 1.88.0 + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4180,6 +4222,11 @@ snapshots: stable-hash@0.0.5: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -4252,6 +4299,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svix@1.88.0: + dependencies: + standardwebhooks: 1.0.0 + uuid: 10.0.0 + tailwindcss@4.2.2: {} tapable@2.3.2: {} @@ -4375,6 +4427,8 @@ snapshots: dependencies: punycode: 2.3.1 + uuid@10.0.0: {} + uuid@8.3.2: {} webidl-conversions@7.0.0: {} From 290cdc6b3e0c95d4a47e4cb2b159847af16bf0f5 Mon Sep 17 00:00:00 2001 From: Freny07 Date: Sat, 11 Apr 2026 22:56:51 +0530 Subject: [PATCH 04/20] feat: add Google OAuth provider with domain restriction as well --- app/api/auth/[...nextauth]/route.ts | 2 +- app/login/LoginClient.tsx | 9 +++--- lib/auth.ts | 49 +++++++++++++++++++---------- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index cab6e6d..c8fc4f3 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,3 @@ -import { handlers } from "@/lib/auth" + export { GET, POST } from "@/lib/auth" \ No newline at end of file diff --git a/app/login/LoginClient.tsx b/app/login/LoginClient.tsx index 924638a..8d39457 100644 --- a/app/login/LoginClient.tsx +++ b/app/login/LoginClient.tsx @@ -52,11 +52,12 @@ export default function LoginClient() { + Sign in with Google +

New to IIITL Alumni?{" "} diff --git a/lib/auth.ts b/lib/auth.ts index c6b00b2..2e694cb 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,3 +1,4 @@ +import Google from "next-auth/providers/google" import NextAuth from "next-auth" import Email from "next-auth/providers/email" import { MongoDBAdapter } from "@auth/mongodb-adapter" @@ -10,28 +11,44 @@ const handler = NextAuth({ adapter: MongoDBAdapter(clientPromise), providers: [ - Email({ - from: process.env.EMAIL_FROM, - - async sendVerificationRequest({ identifier, url }) { - console.log("Sending email to:", identifier) - - await resend.emails.send({ - from: process.env.EMAIL_FROM!, - to: identifier, - subject: "Sign in to IIITL Alumni", - html: `

Click to sign in:

${url}`, - }) - }, - }), - ], + Email({ + from: process.env.EMAIL_FROM, + + async sendVerificationRequest({ identifier, url }) { + console.log("Sending email to:", identifier) + + await resend.emails.send({ + from: process.env.EMAIL_FROM!, + to: identifier, + subject: "Sign in to IIITL Alumni", + html: `

Click to sign in:

${url}`, + }) + }, + }), + + Google({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), +], callbacks: { - async signIn({ user }) { + async signIn({ user, account, profile }) { + + // EMAIL restriction if (user?.email && !user.email.toLowerCase().endsWith("@iiitl.ac.in")) { console.log("Rejected email:", user.email) return false } + + // GOOGLE restriction + if (account?.provider === "google") { + if (profile?.hd !== "iiitl.ac.in") { + console.log("Rejected Google:", profile?.email) + return false + } + } + return true }, }, From 86b2db100e565ec905689301b5395a929fb645fd Mon Sep 17 00:00:00 2001 From: Freny07 Date: Sat, 11 Apr 2026 23:14:00 +0530 Subject: [PATCH 05/20] feat: added a rate limit feature --- app/api/rate-limit/route.ts | 10 ++++++++ app/login/LoginClient.tsx | 49 ++++++++++++++++++++++++++++++------- lib/auth.ts | 28 ++++++++++++--------- lib/rateLimit.ts | 24 ++++++++++++++++++ 4 files changed, 91 insertions(+), 20 deletions(-) create mode 100644 app/api/rate-limit/route.ts create mode 100644 lib/rateLimit.ts diff --git a/app/api/rate-limit/route.ts b/app/api/rate-limit/route.ts new file mode 100644 index 0000000..ea44762 --- /dev/null +++ b/app/api/rate-limit/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server" +import { checkRateLimit } from "@/lib/rateLimit" + +export async function POST(req: Request) { + const { email } = await req.json() + + const result = checkRateLimit(email) + + return NextResponse.json(result) +} \ No newline at end of file diff --git a/app/login/LoginClient.tsx b/app/login/LoginClient.tsx index 8d39457..7f15981 100644 --- a/app/login/LoginClient.tsx +++ b/app/login/LoginClient.tsx @@ -7,14 +7,38 @@ import { useState } from "react" export default function LoginClient() { const [email, setEmail] = useState("") + const [error, setError] = useState("") const handleLogin = async (e: React.FormEvent) => { e.preventDefault() - await signIn("email", { - email, - callbackUrl: "/", - }) + setError("") + + try { + // rate limit check BEFORE signIn + const res = await fetch("/api/rate-limit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }) + + const data = await res.json() + + if (!data.allowed) { + setError("Too many requests. Please try again later.") + return + } + + // proceed normally + await signIn("email", { + email, + callbackUrl: "/", + }) + } catch (err) { + setError("Something went wrong. Please try again.") + } } return ( @@ -51,13 +75,20 @@ export default function LoginClient() { + {/* ERROR MESSAGE (no UI change, just added) */} + {error && ( +

+ {error} +

+ )} + + Sign in with Google +

New to IIITL Alumni?{" "} diff --git a/lib/auth.ts b/lib/auth.ts index 2e694cb..a493adf 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -4,6 +4,7 @@ import Email from "next-auth/providers/email" import { MongoDBAdapter } from "@auth/mongodb-adapter" import clientPromise from "./mongodb" import { Resend } from "resend" +import { checkRateLimit } from "./rateLimit" const resend = new Resend(process.env.RESEND_API_KEY) @@ -12,19 +13,24 @@ const handler = NextAuth({ providers: [ Email({ - from: process.env.EMAIL_FROM, + from: process.env.EMAIL_FROM, - async sendVerificationRequest({ identifier, url }) { - console.log("Sending email to:", identifier) + async sendVerificationRequest({ identifier, url }) { - await resend.emails.send({ - from: process.env.EMAIL_FROM!, - to: identifier, - subject: "Sign in to IIITL Alumni", - html: `

Click to sign in:

${url}`, - }) - }, - }), + // RATE LIMIT CHECK + if (!checkRateLimit(identifier)) { + console.log("Rate limit exceeded:", identifier) + throw new Error("Too many requests. Please try again later.") +} + + await resend.emails.send({ + from: process.env.EMAIL_FROM!, + to: identifier, + subject: "Sign in to IIITL Alumni", + html: `

Click to sign in:

${url}`, + }) + }, +}), Google({ clientId: process.env.GOOGLE_CLIENT_ID!, diff --git a/lib/rateLimit.ts b/lib/rateLimit.ts new file mode 100644 index 0000000..bff41ee --- /dev/null +++ b/lib/rateLimit.ts @@ -0,0 +1,24 @@ +const requests = new Map() + +export function checkRateLimit(email: string) { + const now = Date.now() + const windowMs = 60 * 60 * 1000 // 1 hour + + if (!requests.has(email)) { + requests.set(email, []) + } + + const timestamps = requests.get(email)! + + // remove old timestamps + const filtered = timestamps.filter((t) => now - t < windowMs) + + if (filtered.length >= 5) { + return { allowed: false } + } + + filtered.push(now) + requests.set(email, filtered) + + return { allowed: true } +} \ No newline at end of file From 66b188d6c2ca6fea9af99140b7a4f4c08b32696d Mon Sep 17 00:00:00 2001 From: Freny07 Date: Sat, 11 Apr 2026 23:30:23 +0530 Subject: [PATCH 06/20] Feat: added logging got auth failures --- app/set-password/page.tsx | 0 lib/auth.ts | 63 ++++++++++++++++++++++++--------------- lib/logger.ts | 28 +++++++++++++++++ models/Log.ts | 9 ++++++ 4 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 app/set-password/page.tsx create mode 100644 lib/logger.ts create mode 100644 models/Log.ts diff --git a/app/set-password/page.tsx b/app/set-password/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/lib/auth.ts b/lib/auth.ts index a493adf..0aef754 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -5,6 +5,7 @@ import { MongoDBAdapter } from "@auth/mongodb-adapter" import clientPromise from "./mongodb" import { Resend } from "resend" import { checkRateLimit } from "./rateLimit" +import { logEvent } from "./logger" const resend = new Resend(process.env.RESEND_API_KEY) @@ -12,52 +13,66 @@ const handler = NextAuth({ adapter: MongoDBAdapter(clientPromise), providers: [ - Email({ - from: process.env.EMAIL_FROM, + Email({ + from: process.env.EMAIL_FROM, - async sendVerificationRequest({ identifier, url }) { + async sendVerificationRequest({ identifier, url }) { - // RATE LIMIT CHECK - if (!checkRateLimit(identifier)) { - console.log("Rate limit exceeded:", identifier) - throw new Error("Too many requests. Please try again later.") -} + // RATE LIMIT CHECK + if (!checkRateLimit(identifier)) { + console.log("Rate limit exceeded:", identifier) + await logEvent(identifier, "RATE_LIMIT") + throw new Error("Too many requests. Please try again later.") + } - await resend.emails.send({ - from: process.env.EMAIL_FROM!, - to: identifier, - subject: "Sign in to IIITL Alumni", - html: `

Click to sign in:

${url}`, - }) - }, -}), + await resend.emails.send({ + from: process.env.EMAIL_FROM!, + to: identifier, + subject: "Sign in to IIITL Alumni", + html: `

Click to sign in:

${url}`, + }) + }, + }), - Google({ - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }), -], + Google({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), + ], callbacks: { async signIn({ user, account, profile }) { // EMAIL restriction if (user?.email && !user.email.toLowerCase().endsWith("@iiitl.ac.in")) { - console.log("Rejected email:", user.email) + await logEvent(user.email, "INVALID_DOMAIN") return false } // GOOGLE restriction if (account?.provider === "google") { if (profile?.hd !== "iiitl.ac.in") { - console.log("Rejected Google:", profile?.email) + await logEvent(profile?.email || "", "GOOGLE_REJECT") return false } + + // 🔥 CHECK IF NEW USER + const client = await clientPromise + const db = client.db() + + const existingUser = await db + .collection("users") + .findOne({ email: user.email }) + + if (!existingUser) { + // redirect to password setup + return "/set-password?email=" + user.email + } } return true }, -}, +} session: { strategy: "database", diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 0000000..69db678 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,28 @@ +import clientPromise from "./mongodb" +import mongoose from "mongoose" + +// Define schema ONLY once +const LogSchema = new mongoose.Schema({ + email: String, + type: String, + timestamp: { type: Date, default: Date.now }, +}) + +// Prevent model overwrite (important in Next.js) +const Log = + mongoose.models.Log || mongoose.model("Log", LogSchema) + +export async function logEvent(email: string, type: string) { + try { + const client = await clientPromise + + // ensure mongoose is connected + if (mongoose.connection.readyState === 0) { + await mongoose.connect(process.env.MONGODB_URI!) + } + + await Log.create({ email, type }) + } catch (err) { + console.error("Logging failed:", err) + } +} \ No newline at end of file diff --git a/models/Log.ts b/models/Log.ts new file mode 100644 index 0000000..e6a145b --- /dev/null +++ b/models/Log.ts @@ -0,0 +1,9 @@ +import mongoose from "mongoose" + +const LogSchema = new mongoose.Schema({ + email: String, + type: String, + timestamp: { type: Date, default: Date.now }, +}) + +export default mongoose.models.Log || mongoose.model("Log", LogSchema) \ No newline at end of file From 9ac7f801b1f248e31f87c1a174b91687756b8b1d Mon Sep 17 00:00:00 2001 From: Freny07 Date: Sun, 12 Apr 2026 00:01:58 +0530 Subject: [PATCH 07/20] Added set password for Google Sign in Users --- app/HomeWrapper.tsx | 18 ++ app/layout.tsx | 13 +- app/page.tsx | 525 +++++++++++++++++++------------------- app/providers.tsx | 7 + app/set-password/page.tsx | 62 +++++ lib/auth.ts | 51 ++-- package.json | 1 + pnpm-lock.yaml | 9 + 8 files changed, 395 insertions(+), 291 deletions(-) create mode 100644 app/HomeWrapper.tsx create mode 100644 app/providers.tsx diff --git a/app/HomeWrapper.tsx b/app/HomeWrapper.tsx new file mode 100644 index 0000000..ce91799 --- /dev/null +++ b/app/HomeWrapper.tsx @@ -0,0 +1,18 @@ +"use client" + +import { useSession } from "next-auth/react" +import { useRouter } from "next/navigation" +import { useEffect } from "react" + +export default function HomeWrapper({ children }: { children: React.ReactNode }) { + const { data: session } = useSession() + const router = useRouter() + + useEffect(() => { + if (session?.user?.needsPassword) { + router.push("/set-password?email=" + session.user.email) + } + }, [session, router]) + + return <>{children} +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 7176a50..95da3c9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import { Geist, Fraunces } from "next/font/google"; import "./globals.css"; import { Navbar } from "@/components/Navbar"; import { Footer } from "@/components/Footer"; +import Providers from "./providers"; // ✅ ADD THIS const geistSans = Geist({ variable: "--font-geist-sans", @@ -42,10 +43,14 @@ export default function RootLayout({ className={`${geistSans.variable} ${fraunces.variable} h-full antialiased`} > - -
{children}
-