From 8ffe1a2a12794ddf6e7ef42f083876217dc13e0e Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 01:44:38 +1100 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=98=B6=20began=20working=20on=20for?= =?UTF-8?q?gotten=20password=20backend=20=F0=9F=98=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 8 +- backend/pnpm-lock.yaml | 153 ++++++++++++++---- .../20241118090923_init/migration.sql | 12 ++ .../20241118091722_init/migration.sql | 16 ++ .../20241118101736_init/migration.sql | 8 + .../20241118101841_init/migration.sql | 28 ++++ .../20241118102133_init/migration.sql | 9 ++ .../20241118102217_init/migration.sql | 2 + backend/prisma/schema.prisma | 14 +- backend/src/index.ts | 29 +++- backend/src/interfaces.ts | 2 + backend/src/routes/Attendee/attendee.ts | 1 - backend/src/routes/Attendee/get-attendee.ts | 1 - backend/src/routes/Attendee/new-attendee.ts | 1 - backend/src/routes/Attendee/test-attendee.ts | 1 - backend/src/routes/OTP/OTPToken.ts | 8 + backend/src/routes/OTP/deleteExpired.ts | 16 ++ backend/src/routes/OTP/generateOTP.ts | 65 ++++++++ backend/src/routes/OTP/sendEmail.ts | 26 +++ backend/tests/otp.test.ts | 64 ++++++++ 20 files changed, 421 insertions(+), 43 deletions(-) create mode 100644 backend/prisma/migrations/20241118090923_init/migration.sql create mode 100644 backend/prisma/migrations/20241118091722_init/migration.sql create mode 100644 backend/prisma/migrations/20241118101736_init/migration.sql create mode 100644 backend/prisma/migrations/20241118101841_init/migration.sql create mode 100644 backend/prisma/migrations/20241118102133_init/migration.sql create mode 100644 backend/prisma/migrations/20241118102217_init/migration.sql create mode 100644 backend/src/routes/OTP/OTPToken.ts create mode 100644 backend/src/routes/OTP/deleteExpired.ts create mode 100644 backend/src/routes/OTP/generateOTP.ts create mode 100644 backend/src/routes/OTP/sendEmail.ts create mode 100644 backend/tests/otp.test.ts diff --git a/backend/package.json b/backend/package.json index d790db1..673b4b1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,24 +12,28 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "^5.21.1", "bcrypt": "^5.1.1", "connect-redis": "^7.1.1", "cors": "^2.8.5", "express": "^4.21.1", "express-session": "^1.18.1", - "prisma": "^5.21.1", + "form-data": "^4.0.1", + "mailgun.js": "^10.2.3", + "node-cron": "^3.0.3", "redis": "^4.7.0", "zod": "^3.23.8", "zod-prisma-types": "^3.1.8" }, "devDependencies": { + "@prisma/client": "^5.22.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "4", "@types/express-session": "^1.18.0", "@types/node": "^22.7.7", + "@types/node-cron": "^3.0.11", "jest-mock-extended": "2.0.4", + "prisma": "^5.22.0", "supertest": "^7.0.0", "sync-request-curl": "^3.0.0", "ts-node": "^10.9.2", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 51a7974..352e7fe 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@prisma/client': - specifier: ^5.21.1 - version: 5.21.1(prisma@5.21.1) bcrypt: specifier: ^5.1.1 version: 5.1.1(encoding@0.1.13) @@ -26,9 +23,15 @@ importers: express-session: specifier: ^1.18.1 version: 1.18.1 - prisma: - specifier: ^5.21.1 - version: 5.21.1 + form-data: + specifier: ^4.0.1 + version: 4.0.1 + mailgun.js: + specifier: ^10.2.3 + version: 10.2.3 + node-cron: + specifier: ^3.0.3 + version: 3.0.3 redis: specifier: ^4.7.0 version: 4.7.0 @@ -39,6 +42,9 @@ importers: specifier: ^3.1.8 version: 3.1.8 devDependencies: + '@prisma/client': + specifier: ^5.22.0 + version: 5.22.0(prisma@5.22.0) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 @@ -54,9 +60,15 @@ importers: '@types/node': specifier: ^22.7.7 version: 22.7.7 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 jest-mock-extended: specifier: 2.0.4 version: 2.0.4(jest@29.7.0(@types/node@22.7.7)(ts-node@10.9.2(@types/node@22.7.7)(typescript@5.6.3)))(typescript@5.6.3) + prisma: + specifier: ^5.22.0 + version: 5.22.0 supertest: specifier: ^7.0.0 version: 7.0.0 @@ -647,8 +659,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/client@5.21.1': - resolution: {integrity: sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==} + '@prisma/client@5.22.0': + resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} peerDependencies: prisma: '*' @@ -659,20 +671,23 @@ packages: '@prisma/debug@5.21.1': resolution: {integrity: sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==} - '@prisma/engines-version@5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36': - resolution: {integrity: sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==} + '@prisma/debug@5.22.0': + resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} + + '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': + resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} - '@prisma/engines@5.21.1': - resolution: {integrity: sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==} + '@prisma/engines@5.22.0': + resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} - '@prisma/fetch-engine@5.21.1': - resolution: {integrity: sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==} + '@prisma/fetch-engine@5.22.0': + resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} '@prisma/generator-helper@5.21.1': resolution: {integrity: sha512-56+FLaNGO7uKIEjN5asV7L0cAWqTc+IoyFbtafYnzfvBS3HURgT+l9UGHrHfPO5EWFiot3my3UOJ/hGZfhNPbA==} - '@prisma/get-platform@5.21.1': - resolution: {integrity: sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==} + '@prisma/get-platform@5.22.0': + resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} '@redis/bloom@1.2.0': resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} @@ -868,6 +883,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/node-cron@3.0.11': + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + '@types/node@22.7.7': resolution: {integrity: sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==} @@ -1008,6 +1026,9 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1036,6 +1057,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + bcrypt@5.1.1: resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==} engines: {node: '>= 10.0.0'} @@ -1423,6 +1447,15 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -1847,6 +1880,10 @@ packages: magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + mailgun.js@10.2.3: + resolution: {integrity: sha512-7Mcw5IFtzN21i+qFQoWI+aQFDpLYSMUIWvDUXKLlpGFVVGfYVL8GIiveS+LIXpEJTQcF1hoNhOhDwenFqNSKmw==} + engines: {node: '>=18.0.0'} + make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -1977,6 +2014,10 @@ packages: node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + node-cron@3.0.3: + resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} + engines: {node: '>=6.0.0'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2125,8 +2166,8 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@5.21.1: - resolution: {integrity: sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==} + prisma@5.22.0: + resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} engines: {node: '>=16.13'} hasBin: true @@ -2150,6 +2191,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -2500,6 +2544,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2507,6 +2554,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -3234,34 +3285,36 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/client@5.21.1(prisma@5.21.1)': + '@prisma/client@5.22.0(prisma@5.22.0)': optionalDependencies: - prisma: 5.21.1 + prisma: 5.22.0 '@prisma/debug@5.21.1': {} - '@prisma/engines-version@5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36': {} + '@prisma/debug@5.22.0': {} - '@prisma/engines@5.21.1': + '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} + + '@prisma/engines@5.22.0': dependencies: - '@prisma/debug': 5.21.1 - '@prisma/engines-version': 5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36 - '@prisma/fetch-engine': 5.21.1 - '@prisma/get-platform': 5.21.1 + '@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/fetch-engine@5.21.1': + '@prisma/fetch-engine@5.22.0': dependencies: - '@prisma/debug': 5.21.1 - '@prisma/engines-version': 5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36 - '@prisma/get-platform': 5.21.1 + '@prisma/debug': 5.22.0 + '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 + '@prisma/get-platform': 5.22.0 '@prisma/generator-helper@5.21.1': dependencies: '@prisma/debug': 5.21.1 - '@prisma/get-platform@5.21.1': + '@prisma/get-platform@5.22.0': dependencies: - '@prisma/debug': 5.21.1 + '@prisma/debug': 5.22.0 '@redis/bloom@1.2.0(@redis/client@1.6.0)': dependencies: @@ -3437,6 +3490,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/node-cron@3.0.11': {} + '@types/node@22.7.7': dependencies: undici-types: 6.19.8 @@ -3578,6 +3633,14 @@ snapshots: asynckit@0.4.0: {} + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-jest@29.7.0(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -3635,6 +3698,8 @@ snapshots: balanced-match@1.0.2: {} + base-64@1.0.0: {} + bcrypt@5.1.1(encoding@0.1.13): dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) @@ -4085,6 +4150,8 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + follow-redirects@1.15.9: {} + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.3 @@ -4687,6 +4754,14 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + mailgun.js@10.2.3: + dependencies: + axios: 1.7.7 + base-64: 1.0.0 + url-join: 4.0.1 + transitivePeerDependencies: + - debug + make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -4804,6 +4879,10 @@ snapshots: node-addon-api@5.1.0: {} + node-cron@3.0.3: + dependencies: + uuid: 8.3.2 + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -4938,9 +5017,9 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@5.21.1: + prisma@5.22.0: dependencies: - '@prisma/engines': 5.21.1 + '@prisma/engines': 5.22.0 optionalDependencies: fsevents: 2.3.3 @@ -4963,6 +5042,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + pure-rand@6.1.0: {} qs@6.13.0: @@ -5334,10 +5415,14 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + url-join@4.0.1: {} + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} + uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: diff --git a/backend/prisma/migrations/20241118090923_init/migration.sql b/backend/prisma/migrations/20241118090923_init/migration.sql new file mode 100644 index 0000000..8d83e30 --- /dev/null +++ b/backend/prisma/migrations/20241118090923_init/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "OTPToken" ( + "userId" INTEGER NOT NULL, + "token" TEXT NOT NULL, + "timeCreated" TIMESTAMP(3) NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "OTPToken_userId_key" ON "OTPToken"("userId"); + +-- AddForeignKey +ALTER TABLE "OTPToken" ADD CONSTRAINT "OTPToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20241118091722_init/migration.sql b/backend/prisma/migrations/20241118091722_init/migration.sql new file mode 100644 index 0000000..3fdef99 --- /dev/null +++ b/backend/prisma/migrations/20241118091722_init/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "OTPToken" ADD COLUMN "id" SERIAL NOT NULL, +ADD CONSTRAINT "OTPToken_pkey" PRIMARY KEY ("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/backend/prisma/migrations/20241118101736_init/migration.sql b/backend/prisma/migrations/20241118101736_init/migration.sql new file mode 100644 index 0000000..8f601e4 --- /dev/null +++ b/backend/prisma/migrations/20241118101736_init/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `expiryTime` to the `OTPToken` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "OTPToken" ADD COLUMN "expiryTime" TIMESTAMP(3) NOT NULL; diff --git a/backend/prisma/migrations/20241118101841_init/migration.sql b/backend/prisma/migrations/20241118101841_init/migration.sql new file mode 100644 index 0000000..8414989 --- /dev/null +++ b/backend/prisma/migrations/20241118101841_init/migration.sql @@ -0,0 +1,28 @@ +/* + Warnings: + + - You are about to drop the `OTPToken` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "OTPToken" DROP CONSTRAINT "OTPToken_userId_fkey"; + +-- DropTable +DROP TABLE "OTPToken"; + +-- CreateTable +CREATE TABLE "OtpToken" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "token" TEXT NOT NULL, + "timeCreated" TIMESTAMP(3) NOT NULL, + "expiryTime" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OtpToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "OtpToken_userId_key" ON "OtpToken"("userId"); + +-- AddForeignKey +ALTER TABLE "OtpToken" ADD CONSTRAINT "OtpToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20241118102133_init/migration.sql b/backend/prisma/migrations/20241118102133_init/migration.sql new file mode 100644 index 0000000..6b34c78 --- /dev/null +++ b/backend/prisma/migrations/20241118102133_init/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Changed the type of `token` on the `OtpToken` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "OtpToken" DROP COLUMN "token", +ADD COLUMN "token" INTEGER NOT NULL; diff --git a/backend/prisma/migrations/20241118102217_init/migration.sql b/backend/prisma/migrations/20241118102217_init/migration.sql new file mode 100644 index 0000000..f431951 --- /dev/null +++ b/backend/prisma/migrations/20241118102217_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "OtpToken" ALTER COLUMN "token" SET DATA TYPE TEXT; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index de1e988..7f6500d 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -21,8 +21,8 @@ enum UserType { model User { id Int @id @default(autoincrement()) - username String - email String + username String @unique + email String @unique password String userType UserType attendee Attendee? @@ -30,6 +30,7 @@ model User { salt String dateJoined DateTime profilePicture String? + oneTimeToken OtpToken? } model Attendee { @@ -68,4 +69,13 @@ model Keyword { text String @unique attendees Attendee[] events Event[] +} + +model OtpToken { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int @unique + token String + timeCreated DateTime + expiryTime DateTime } \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 73dd1af..901e8ab 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -8,6 +8,8 @@ import { PrismaClient, Prisma, UserType, User } from "@prisma/client"; import prisma from "./prisma"; import RedisStore from "connect-redis"; import { createClient } from "redis"; +import { generateOTP } from "./routes/OTP/generateOTP"; +import { removeExpiredOTPs } from "./routes/OTP/deleteExpired"; declare module "express-session" { interface SessionData { userId: number; @@ -32,6 +34,7 @@ let redisStore = new RedisStore({ const app = express(); const SERVER_PORT = 5180; +const SALT_ROUNDS = 10; app.use(cors()); app.use(express.json()); @@ -51,6 +54,9 @@ app.use( }) ); +// OTP removing cron job +removeExpiredOTPs.start(); + app.get("/", (req: Request, res: Response) => { console.log("Hello, TypeScript with Express :)))!"); res.send("Hello, TypeScript with Express :)))!"); @@ -77,7 +83,7 @@ app.post( return res.status(400).json(errorCheck); } - const saltRounds: number = 10; + const saltRounds: number = SALT_ROUNDS; const salt = await bcrypt.genSalt(saltRounds); const hashedPassword = await bcrypt.hash(password, salt); @@ -104,6 +110,27 @@ app.post( } ); +app.post("/auth/otp", async(req: Request, res: Response) => { + try { + const { email } = req.body; + + if(!email) { + throw new Error("Email address expected."); + } + + const result = await generateOTP(email, SALT_ROUNDS); + + if (result) { + return res.status(200).json({ message: "ok" }); + } else { + return res.status(400).json( {message: "Unexpected error while generating OTP."} ); + } + + } catch(error) { + return res.status(400).json({ message: (error as Error).message }); + } +}); + app.post("/auth/login", async (req: TypedRequest, res: Response) => { try { const { username, password } = req.body; diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index b12bdb9..da40bb8 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -1,4 +1,5 @@ import { UserType } from "@prisma/client"; +import { OTPToken } from "./routes/OTP/OTPToken"; export interface User { //id Int @id @default(autoincrement()) @@ -11,6 +12,7 @@ export interface User { profilePicture: string | null; //attendee: Attendee? //society: Society? + otpToken: OTPToken | undefined; } export interface LoginErrors { diff --git a/backend/src/routes/Attendee/attendee.ts b/backend/src/routes/Attendee/attendee.ts index 022855d..22e621a 100644 --- a/backend/src/routes/Attendee/attendee.ts +++ b/backend/src/routes/Attendee/attendee.ts @@ -4,7 +4,6 @@ import { Keyword } from "../Keyword/keyword"; export interface Attendee { id?: number, name: string, - picture: string, keywords?: Keyword[], societies?: Society[] } \ No newline at end of file diff --git a/backend/src/routes/Attendee/get-attendee.ts b/backend/src/routes/Attendee/get-attendee.ts index b3fd825..44228a7 100644 --- a/backend/src/routes/Attendee/get-attendee.ts +++ b/backend/src/routes/Attendee/get-attendee.ts @@ -17,6 +17,5 @@ export const getAttendeeFromID = async (id: number): Promise => return { id: data?.id, name: data?.name, - picture: data?.picture } } \ No newline at end of file diff --git a/backend/src/routes/Attendee/new-attendee.ts b/backend/src/routes/Attendee/new-attendee.ts index d694998..6539b93 100644 --- a/backend/src/routes/Attendee/new-attendee.ts +++ b/backend/src/routes/Attendee/new-attendee.ts @@ -7,7 +7,6 @@ export const newAttendee = async (newAttendee: Attendee) => { const createdAttendee = await db.attendee.create( { data: { name: newAttendee.name, - picture: newAttendee.picture, } } ); diff --git a/backend/src/routes/Attendee/test-attendee.ts b/backend/src/routes/Attendee/test-attendee.ts index 8bccb64..27ed273 100644 --- a/backend/src/routes/Attendee/test-attendee.ts +++ b/backend/src/routes/Attendee/test-attendee.ts @@ -6,7 +6,6 @@ import { getAttendeeFromID } from "./get-attendee"; async function main() { const attendee1: Attendee = { name: "tim drake", - picture: "www.google.com", keywords: [], societies: [] }; diff --git a/backend/src/routes/OTP/OTPToken.ts b/backend/src/routes/OTP/OTPToken.ts new file mode 100644 index 0000000..cf1ba74 --- /dev/null +++ b/backend/src/routes/OTP/OTPToken.ts @@ -0,0 +1,8 @@ +import { User } from "@prisma/client"; + +export interface OTPToken { + user: User, + token: string, + timeCreated: Date, + expiryTime: Date, +} \ No newline at end of file diff --git a/backend/src/routes/OTP/deleteExpired.ts b/backend/src/routes/OTP/deleteExpired.ts new file mode 100644 index 0000000..017619e --- /dev/null +++ b/backend/src/routes/OTP/deleteExpired.ts @@ -0,0 +1,16 @@ +import cron from 'node-cron'; +import prisma from '../../prisma'; + +export const removeExpiredOTPs = cron.schedule('1 * * * *', async () => { + try { + await prisma.otpToken.deleteMany({ + where: { + expiryTime: { + lte: new Date(Date.now()), + }, + }, + }); + } catch (error) { + console.error(error); + } + }); \ No newline at end of file diff --git a/backend/src/routes/OTP/generateOTP.ts b/backend/src/routes/OTP/generateOTP.ts new file mode 100644 index 0000000..7ea14fb --- /dev/null +++ b/backend/src/routes/OTP/generateOTP.ts @@ -0,0 +1,65 @@ +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import prisma from '../../prisma'; +import { OTPToken } from './OTPToken'; +import { sendEmail } from './sendEmail'; + +export const generateOTP = async (emailAddress: string, salt_rounds: number) => { + const user = await getUserFromEmail(emailAddress); + + if(!user) { + throw new Error("User not found."); + } + + await deleteToken(user.id); + + const rand = crypto.randomBytes(4).readUint32BE(0); + const sixDigits = (rand % 900000) + 100000; + const otpCode = sixDigits.toString(); + const hash = await bcrypt.hash(otpCode, salt_rounds); + + const token: OTPToken = { + user, + token: hash, + timeCreated: new Date(Date.now()), + expiryTime: new Date(Date.now()+60000), + }; + + await addTokenToDb(token); + + await sendEmail(emailAddress, user.username, otpCode); + + return true; +} + +const getUserFromEmail = async (emailAddress: string) => { + const result = await prisma.user.findUnique({ + where: { + email: emailAddress, + }, + }); + + return result; +} + +const deleteToken = async(userId: number) => { + const result = await prisma.otpToken.deleteMany({ + where: { + userId + } + }); + + return result; +} + +const addTokenToDb = async(token: OTPToken) => { + const result = await prisma.otpToken.create({ + data: { + userId: token.user.id, + token: token.token, + timeCreated: token.timeCreated, + expiryTime: token.expiryTime + } + }); + return result; +} \ No newline at end of file diff --git a/backend/src/routes/OTP/sendEmail.ts b/backend/src/routes/OTP/sendEmail.ts new file mode 100644 index 0000000..bb4884c --- /dev/null +++ b/backend/src/routes/OTP/sendEmail.ts @@ -0,0 +1,26 @@ +import Mailgun from "mailgun.js"; +import FormData from 'form-data'; + +const mailgun = new Mailgun(FormData); +const mg = mailgun.client({username: 'api', key: process.env["EMAIL_KEY"] || ""}); + +export const sendEmail = async (emailAddress: string, userName: string, code: string) => { + if(!mg){ + throw new Error("invalid email key"); + } + + console.log(`emailing ${userName} at ${emailAddress} with otp ${code}.`); +// const senderDomain = ""; + +// try { +// await mg.messages.create(`${senderDomain}`, { +// from: `No Reply `, +// to: [emailAddress], +// subject: "Pyramids Login One Time Code", +// text: "", +// html: "" +// }); +// } catch (error) { +// throw new Error("sending of email failed"); +// } + } \ No newline at end of file diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts new file mode 100644 index 0000000..fad249d --- /dev/null +++ b/backend/tests/otp.test.ts @@ -0,0 +1,64 @@ +//test/sample.test.ts +import { expect, test, vi, describe } from "vitest"; // 👈🏻 Added the `vi` import +import prisma from "../src/prisma"; +import request from "supertest"; +import app from "../src/index"; + +describe("Tests", () => { + test("register test", async () => { + const { status, body } = await request(app).post("/auth/register").send({ + username: "richard grayson", + password: "iheartkori&barbs", + email: "nightwing1@gmail.com", + userType: "ATTENDEE", + }); + + const newUser = await prisma.user.findFirst({ + where: { + id: body.newUser.id, + }, + }); + + expect(status).toBe(201); + expect(newUser).not.toBeNull(); + expect(body.newUser).toStrictEqual({ + username: "richard grayson", + id: newUser?.id, + }); + + if(newUser) { + const response = await request(app).post("/auth/otp").send({ + email: "nightwing1@gmail.com" + }); + expect(response.status).toBe(200); + + const tokens = await prisma.otpToken.findMany({ + where: { + userId: newUser.id + } + }); + + expect(tokens.length).toBe(1); + + const newToken = await prisma.otpToken.findFirst({ + where: { + userId: newUser.id + } + }); + expect(newToken).not.toBeNull(); + + if(newToken) { + console.log(newToken.token); + + await new Promise(resolve => setTimeout(resolve, 60000)); + + const checkTokens = await prisma.otpToken.findFirst({ + where: { + userId: newUser.id + } + }); + expect(checkTokens).toBeNull(); + } + } + }); +}); From ffa83eca032ac95af6a4e63381c3108de440188d Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 01:54:48 +1100 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=91=BE=20updated=20github=20actions?= =?UTF-8?q?=20to=20run=20tests=20on=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 1 + backend/src/routes/OTP/sendEmail.ts | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 404bd10..69810e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: branches: - main - feature/login + - feature/forgot-password pull_request: branches: - main diff --git a/backend/src/routes/OTP/sendEmail.ts b/backend/src/routes/OTP/sendEmail.ts index bb4884c..bfb51a3 100644 --- a/backend/src/routes/OTP/sendEmail.ts +++ b/backend/src/routes/OTP/sendEmail.ts @@ -1,13 +1,13 @@ import Mailgun from "mailgun.js"; import FormData from 'form-data'; -const mailgun = new Mailgun(FormData); -const mg = mailgun.client({username: 'api', key: process.env["EMAIL_KEY"] || ""}); +//const mailgun = new Mailgun(FormData); +//const mg = mailgun.client({username: 'api', key: process.env["EMAIL_KEY"] || ""}); export const sendEmail = async (emailAddress: string, userName: string, code: string) => { - if(!mg){ - throw new Error("invalid email key"); - } + // if(!mg){ + // throw new Error("invalid email key"); + // } console.log(`emailing ${userName} at ${emailAddress} with otp ${code}.`); // const senderDomain = ""; From 506d35cfca6124b1828f46b9c99a4443e82323ea Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 02:09:42 +1100 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=91=B7=20fixed=20up=20schema=20to?= =?UTF-8?q?=20fix=20ci=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20241118150715_init/migration.sql | 17 +++++++++++++++++ backend/prisma/schema.prisma | 6 +++--- backend/tests/otp.test.ts | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 backend/prisma/migrations/20241118150715_init/migration.sql diff --git a/backend/prisma/migrations/20241118150715_init/migration.sql b/backend/prisma/migrations/20241118150715_init/migration.sql new file mode 100644 index 0000000..a4e3532 --- /dev/null +++ b/backend/prisma/migrations/20241118150715_init/migration.sql @@ -0,0 +1,17 @@ +-- DropForeignKey +ALTER TABLE "Attendee" DROP CONSTRAINT "Attendee_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "OtpToken" DROP CONSTRAINT "OtpToken_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Society" DROP CONSTRAINT "Society_userId_fkey"; + +-- AddForeignKey +ALTER TABLE "Attendee" ADD CONSTRAINT "Attendee_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Society" ADD CONSTRAINT "Society_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OtpToken" ADD CONSTRAINT "OtpToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 7f6500d..7627cd8 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -35,7 +35,7 @@ model User { model Attendee { id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int @unique name String keywords Keyword[] @@ -44,7 +44,7 @@ model Attendee { model Society { id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int @unique name String @unique events Event[] @@ -73,7 +73,7 @@ model Keyword { model OtpToken { id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int @unique token String timeCreated DateTime diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index fad249d..c1686fa 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -60,5 +60,5 @@ describe("Tests", () => { expect(checkTokens).toBeNull(); } } - }); + }, 100000); }); From b7185a15df773c7d7bd2181fd6f36333ea3dd481 Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 02:18:13 +1100 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=91=B7=20making=20adjustments=20to?= =?UTF-8?q?=20try=20to=20fix=20cron=20job=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/OTP/deleteExpired.ts | 2 +- backend/tests/otp.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/OTP/deleteExpired.ts b/backend/src/routes/OTP/deleteExpired.ts index 017619e..91ce4df 100644 --- a/backend/src/routes/OTP/deleteExpired.ts +++ b/backend/src/routes/OTP/deleteExpired.ts @@ -1,7 +1,7 @@ import cron from 'node-cron'; import prisma from '../../prisma'; -export const removeExpiredOTPs = cron.schedule('1 * * * *', async () => { +export const removeExpiredOTPs = cron.schedule('* * * * *', async () => { try { await prisma.otpToken.deleteMany({ where: { diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index c1686fa..ae76382 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -50,7 +50,7 @@ describe("Tests", () => { if(newToken) { console.log(newToken.token); - await new Promise(resolve => setTimeout(resolve, 60000)); + await new Promise(resolve => setTimeout(resolve, 61000)); const checkTokens = await prisma.otpToken.findFirst({ where: { From f5e26ff7053e5f81acaa9e30fe56652e8fb51724 Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 02:24:12 +1100 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=91=B7=20making=20adjustments=20to?= =?UTF-8?q?=20try=20to=20fix=20cron=20job=20issues=20part=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 47 ---------------- .../migration.sql | 11 ---- .../migration.sql | 12 ---- .../migration.sql | 2 - .../migration.sql | 2 - .../20241118090923_init/migration.sql | 12 ---- .../20241118091722_init/migration.sql | 16 ------ .../20241118101736_init/migration.sql | 8 --- .../20241118101841_init/migration.sql | 28 ---------- .../20241118102133_init/migration.sql | 9 --- .../20241118102217_init/migration.sql | 2 - .../20241118150715_init/migration.sql | 17 ------ .../migration.sql | 56 ++++++++++++++++++- backend/prisma/schema.prisma | 2 +- backend/src/routes/OTP/OTPToken.ts | 2 +- backend/src/routes/OTP/deleteExpired.ts | 2 +- backend/src/routes/OTP/generateOTP.ts | 2 +- 17 files changed, 58 insertions(+), 172 deletions(-) delete mode 100644 backend/prisma/migrations/20241107041958_071124_update/migration.sql delete mode 100644 backend/prisma/migrations/20241107042850_071124_update2/migration.sql delete mode 100644 backend/prisma/migrations/20241112111432_121124emailuserunique/migration.sql delete mode 100644 backend/prisma/migrations/20241112111656_121124emailunique/migration.sql delete mode 100644 backend/prisma/migrations/20241112111750_121124rollbackunique/migration.sql delete mode 100644 backend/prisma/migrations/20241118090923_init/migration.sql delete mode 100644 backend/prisma/migrations/20241118091722_init/migration.sql delete mode 100644 backend/prisma/migrations/20241118101736_init/migration.sql delete mode 100644 backend/prisma/migrations/20241118101841_init/migration.sql delete mode 100644 backend/prisma/migrations/20241118102133_init/migration.sql delete mode 100644 backend/prisma/migrations/20241118102217_init/migration.sql delete mode 100644 backend/prisma/migrations/20241118150715_init/migration.sql rename backend/prisma/migrations/{20241102162505_init => 20241118152321_init}/migration.sql (68%) diff --git a/backend/prisma/migrations/20241107041958_071124_update/migration.sql b/backend/prisma/migrations/20241107041958_071124_update/migration.sql deleted file mode 100644 index a96e2a0..0000000 --- a/backend/prisma/migrations/20241107041958_071124_update/migration.sql +++ /dev/null @@ -1,47 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `picture` on the `Attendee` table. All the data in the column will be lost. - - You are about to drop the column `picture` on the `Society` table. All the data in the column will be lost. - - A unique constraint covering the columns `[userId]` on the table `Attendee` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[userId]` on the table `Society` will be added. If there are existing duplicate values, this will fail. - - Added the required column `userId` to the `Attendee` table without a default value. This is not possible if the table is not empty. - - Added the required column `userId` to the `Society` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "UserType" AS ENUM ('ATTENDEE', 'SOCIETY'); - --- AlterTable -ALTER TABLE "Attendee" DROP COLUMN "picture", -ADD COLUMN "userId" INTEGER NOT NULL; - --- AlterTable -ALTER TABLE "Society" DROP COLUMN "picture", -ADD COLUMN "userId" INTEGER NOT NULL; - --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "username" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "userType" "UserType" NOT NULL, - "salt" TEXT NOT NULL, - "dateAdded" TIMESTAMP(3) NOT NULL, - "profilePicture" TEXT NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Attendee_userId_key" ON "Attendee"("userId"); - --- CreateIndex -CREATE UNIQUE INDEX "Society_userId_key" ON "Society"("userId"); - --- AddForeignKey -ALTER TABLE "Attendee" ADD CONSTRAINT "Attendee_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Society" ADD CONSTRAINT "Society_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20241107042850_071124_update2/migration.sql b/backend/prisma/migrations/20241107042850_071124_update2/migration.sql deleted file mode 100644 index 2cd3486..0000000 --- a/backend/prisma/migrations/20241107042850_071124_update2/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `dateAdded` on the `User` table. All the data in the column will be lost. - - Added the required column `dateJoined` to the `User` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "User" DROP COLUMN "dateAdded", -ADD COLUMN "dateJoined" TIMESTAMP(3) NOT NULL, -ALTER COLUMN "profilePicture" DROP NOT NULL; diff --git a/backend/prisma/migrations/20241112111432_121124emailuserunique/migration.sql b/backend/prisma/migrations/20241112111432_121124emailuserunique/migration.sql deleted file mode 100644 index 74bc6d3..0000000 --- a/backend/prisma/migrations/20241112111432_121124emailuserunique/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/backend/prisma/migrations/20241112111656_121124emailunique/migration.sql b/backend/prisma/migrations/20241112111656_121124emailunique/migration.sql deleted file mode 100644 index bac82da..0000000 --- a/backend/prisma/migrations/20241112111656_121124emailunique/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- DropIndex -DROP INDEX "User_username_key"; diff --git a/backend/prisma/migrations/20241112111750_121124rollbackunique/migration.sql b/backend/prisma/migrations/20241112111750_121124rollbackunique/migration.sql deleted file mode 100644 index 0b76ed5..0000000 --- a/backend/prisma/migrations/20241112111750_121124rollbackunique/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- DropIndex -DROP INDEX "User_email_key"; diff --git a/backend/prisma/migrations/20241118090923_init/migration.sql b/backend/prisma/migrations/20241118090923_init/migration.sql deleted file mode 100644 index 8d83e30..0000000 --- a/backend/prisma/migrations/20241118090923_init/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ --- CreateTable -CREATE TABLE "OTPToken" ( - "userId" INTEGER NOT NULL, - "token" TEXT NOT NULL, - "timeCreated" TIMESTAMP(3) NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "OTPToken_userId_key" ON "OTPToken"("userId"); - --- AddForeignKey -ALTER TABLE "OTPToken" ADD CONSTRAINT "OTPToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20241118091722_init/migration.sql b/backend/prisma/migrations/20241118091722_init/migration.sql deleted file mode 100644 index 3fdef99..0000000 --- a/backend/prisma/migrations/20241118091722_init/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail. - - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. - -*/ --- AlterTable -ALTER TABLE "OTPToken" ADD COLUMN "id" SERIAL NOT NULL, -ADD CONSTRAINT "OTPToken_pkey" PRIMARY KEY ("id"); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/backend/prisma/migrations/20241118101736_init/migration.sql b/backend/prisma/migrations/20241118101736_init/migration.sql deleted file mode 100644 index 8f601e4..0000000 --- a/backend/prisma/migrations/20241118101736_init/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `expiryTime` to the `OTPToken` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "OTPToken" ADD COLUMN "expiryTime" TIMESTAMP(3) NOT NULL; diff --git a/backend/prisma/migrations/20241118101841_init/migration.sql b/backend/prisma/migrations/20241118101841_init/migration.sql deleted file mode 100644 index 8414989..0000000 --- a/backend/prisma/migrations/20241118101841_init/migration.sql +++ /dev/null @@ -1,28 +0,0 @@ -/* - Warnings: - - - You are about to drop the `OTPToken` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "OTPToken" DROP CONSTRAINT "OTPToken_userId_fkey"; - --- DropTable -DROP TABLE "OTPToken"; - --- CreateTable -CREATE TABLE "OtpToken" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "token" TEXT NOT NULL, - "timeCreated" TIMESTAMP(3) NOT NULL, - "expiryTime" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "OtpToken_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "OtpToken_userId_key" ON "OtpToken"("userId"); - --- AddForeignKey -ALTER TABLE "OtpToken" ADD CONSTRAINT "OtpToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20241118102133_init/migration.sql b/backend/prisma/migrations/20241118102133_init/migration.sql deleted file mode 100644 index 6b34c78..0000000 --- a/backend/prisma/migrations/20241118102133_init/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - Changed the type of `token` on the `OtpToken` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. - -*/ --- AlterTable -ALTER TABLE "OtpToken" DROP COLUMN "token", -ADD COLUMN "token" INTEGER NOT NULL; diff --git a/backend/prisma/migrations/20241118102217_init/migration.sql b/backend/prisma/migrations/20241118102217_init/migration.sql deleted file mode 100644 index f431951..0000000 --- a/backend/prisma/migrations/20241118102217_init/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "OtpToken" ALTER COLUMN "token" SET DATA TYPE TEXT; diff --git a/backend/prisma/migrations/20241118150715_init/migration.sql b/backend/prisma/migrations/20241118150715_init/migration.sql deleted file mode 100644 index a4e3532..0000000 --- a/backend/prisma/migrations/20241118150715_init/migration.sql +++ /dev/null @@ -1,17 +0,0 @@ --- DropForeignKey -ALTER TABLE "Attendee" DROP CONSTRAINT "Attendee_userId_fkey"; - --- DropForeignKey -ALTER TABLE "OtpToken" DROP CONSTRAINT "OtpToken_userId_fkey"; - --- DropForeignKey -ALTER TABLE "Society" DROP CONSTRAINT "Society_userId_fkey"; - --- AddForeignKey -ALTER TABLE "Attendee" ADD CONSTRAINT "Attendee_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Society" ADD CONSTRAINT "Society_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "OtpToken" ADD CONSTRAINT "OtpToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20241102162505_init/migration.sql b/backend/prisma/migrations/20241118152321_init/migration.sql similarity index 68% rename from backend/prisma/migrations/20241102162505_init/migration.sql rename to backend/prisma/migrations/20241118152321_init/migration.sql index 52b7dfb..4fdcb3f 100644 --- a/backend/prisma/migrations/20241102162505_init/migration.sql +++ b/backend/prisma/migrations/20241118152321_init/migration.sql @@ -1,8 +1,25 @@ +-- CreateEnum +CREATE TYPE "UserType" AS ENUM ('ATTENDEE', 'SOCIETY'); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "userType" "UserType" NOT NULL, + "salt" TEXT NOT NULL, + "dateJoined" TIMESTAMP(3) NOT NULL, + "profilePicture" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "Attendee" ( "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, "name" TEXT NOT NULL, - "picture" TEXT NOT NULL, CONSTRAINT "Attendee_pkey" PRIMARY KEY ("id") ); @@ -10,8 +27,8 @@ CREATE TABLE "Attendee" ( -- CreateTable CREATE TABLE "Society" ( "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, "name" TEXT NOT NULL, - "picture" TEXT NOT NULL, "discordId" TEXT NOT NULL, CONSTRAINT "Society_pkey" PRIMARY KEY ("id") @@ -38,6 +55,17 @@ CREATE TABLE "Keyword" ( CONSTRAINT "Keyword_pkey" PRIMARY KEY ("id") ); +-- CreateTable +CREATE TABLE "OtpToken" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "token" TEXT NOT NULL, + "timeCreated" TIMESTAMP(3) NOT NULL, + "expiryTime" INTEGER NOT NULL, + + CONSTRAINT "OtpToken_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "_AttendeeToKeyword" ( "A" INTEGER NOT NULL, @@ -62,6 +90,18 @@ CREATE TABLE "_EventToKeyword" ( "B" INTEGER NOT NULL ); +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Attendee_userId_key" ON "Attendee"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Society_userId_key" ON "Society"("userId"); + -- CreateIndex CREATE UNIQUE INDEX "Society_name_key" ON "Society"("name"); @@ -71,6 +111,9 @@ CREATE UNIQUE INDEX "Society_discordId_key" ON "Society"("discordId"); -- CreateIndex CREATE UNIQUE INDEX "Keyword_text_key" ON "Keyword"("text"); +-- CreateIndex +CREATE UNIQUE INDEX "OtpToken_userId_key" ON "OtpToken"("userId"); + -- CreateIndex CREATE UNIQUE INDEX "_AttendeeToKeyword_AB_unique" ON "_AttendeeToKeyword"("A", "B"); @@ -95,6 +138,15 @@ CREATE UNIQUE INDEX "_EventToKeyword_AB_unique" ON "_EventToKeyword"("A", "B"); -- CreateIndex CREATE INDEX "_EventToKeyword_B_index" ON "_EventToKeyword"("B"); +-- AddForeignKey +ALTER TABLE "Attendee" ADD CONSTRAINT "Attendee_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Society" ADD CONSTRAINT "Society_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OtpToken" ADD CONSTRAINT "OtpToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "_AttendeeToKeyword" ADD CONSTRAINT "_AttendeeToKeyword_A_fkey" FOREIGN KEY ("A") REFERENCES "Attendee"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 7627cd8..0b921ac 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -77,5 +77,5 @@ model OtpToken { userId Int @unique token String timeCreated DateTime - expiryTime DateTime + expiryTime Int } \ No newline at end of file diff --git a/backend/src/routes/OTP/OTPToken.ts b/backend/src/routes/OTP/OTPToken.ts index cf1ba74..ce05d97 100644 --- a/backend/src/routes/OTP/OTPToken.ts +++ b/backend/src/routes/OTP/OTPToken.ts @@ -4,5 +4,5 @@ export interface OTPToken { user: User, token: string, timeCreated: Date, - expiryTime: Date, + expiryTime: number, } \ No newline at end of file diff --git a/backend/src/routes/OTP/deleteExpired.ts b/backend/src/routes/OTP/deleteExpired.ts index 91ce4df..4cec95d 100644 --- a/backend/src/routes/OTP/deleteExpired.ts +++ b/backend/src/routes/OTP/deleteExpired.ts @@ -6,7 +6,7 @@ export const removeExpiredOTPs = cron.schedule('* * * * *', async () => { await prisma.otpToken.deleteMany({ where: { expiryTime: { - lte: new Date(Date.now()), + lte: Date.now(), }, }, }); diff --git a/backend/src/routes/OTP/generateOTP.ts b/backend/src/routes/OTP/generateOTP.ts index 7ea14fb..16eee52 100644 --- a/backend/src/routes/OTP/generateOTP.ts +++ b/backend/src/routes/OTP/generateOTP.ts @@ -22,7 +22,7 @@ export const generateOTP = async (emailAddress: string, salt_rounds: number) => user, token: hash, timeCreated: new Date(Date.now()), - expiryTime: new Date(Date.now()+60000), + expiryTime: Date.now()+60000, }; await addTokenToDb(token); From a5cc8efa8337670316e7d66b8e572d2c51caf6f2 Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 02:34:45 +1100 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=91=B7=20making=20adjustments=20to?= =?UTF-8?q?=20try=20to=20fix=20cron=20job=20issues=20part=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prisma/migrations/20241118153206_init/migration.sql | 9 +++++++++ backend/prisma/schema.prisma | 2 +- backend/src/routes/OTP/OTPToken.ts | 2 +- backend/src/routes/OTP/deleteExpired.ts | 2 +- backend/src/routes/OTP/generateOTP.ts | 4 ++-- 5 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 backend/prisma/migrations/20241118153206_init/migration.sql diff --git a/backend/prisma/migrations/20241118153206_init/migration.sql b/backend/prisma/migrations/20241118153206_init/migration.sql new file mode 100644 index 0000000..e2efb28 --- /dev/null +++ b/backend/prisma/migrations/20241118153206_init/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Changed the type of `expiryTime` on the `OtpToken` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "OtpToken" DROP COLUMN "expiryTime", +ADD COLUMN "expiryTime" TIMESTAMP(3) NOT NULL; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 0b921ac..7627cd8 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -77,5 +77,5 @@ model OtpToken { userId Int @unique token String timeCreated DateTime - expiryTime Int + expiryTime DateTime } \ No newline at end of file diff --git a/backend/src/routes/OTP/OTPToken.ts b/backend/src/routes/OTP/OTPToken.ts index ce05d97..cf1ba74 100644 --- a/backend/src/routes/OTP/OTPToken.ts +++ b/backend/src/routes/OTP/OTPToken.ts @@ -4,5 +4,5 @@ export interface OTPToken { user: User, token: string, timeCreated: Date, - expiryTime: number, + expiryTime: Date, } \ No newline at end of file diff --git a/backend/src/routes/OTP/deleteExpired.ts b/backend/src/routes/OTP/deleteExpired.ts index 4cec95d..85f7daf 100644 --- a/backend/src/routes/OTP/deleteExpired.ts +++ b/backend/src/routes/OTP/deleteExpired.ts @@ -6,7 +6,7 @@ export const removeExpiredOTPs = cron.schedule('* * * * *', async () => { await prisma.otpToken.deleteMany({ where: { expiryTime: { - lte: Date.now(), + lte: new Date(), }, }, }); diff --git a/backend/src/routes/OTP/generateOTP.ts b/backend/src/routes/OTP/generateOTP.ts index 16eee52..74d0076 100644 --- a/backend/src/routes/OTP/generateOTP.ts +++ b/backend/src/routes/OTP/generateOTP.ts @@ -21,8 +21,8 @@ export const generateOTP = async (emailAddress: string, salt_rounds: number) => const token: OTPToken = { user, token: hash, - timeCreated: new Date(Date.now()), - expiryTime: Date.now()+60000, + timeCreated: new Date(), + expiryTime: new Date(Date.now()+60000), }; await addTokenToDb(token); From 1c6268ce96e78af1e6819307a2e7fb71b41259cd Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 05:38:28 +1100 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=91=B7=20making=20adjustments=20to?= =?UTF-8?q?=20try=20to=20fix=20cron=20job=20issues=20part=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/otp.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index ae76382..28337fc 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -50,7 +50,7 @@ describe("Tests", () => { if(newToken) { console.log(newToken.token); - await new Promise(resolve => setTimeout(resolve, 61000)); + await new Promise(resolve => setTimeout(resolve, 120000)); const checkTokens = await prisma.otpToken.findFirst({ where: { From 5424380cb137afe8892f9279a269fb7925260cf3 Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 05:41:51 +1100 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=91=B7=20making=20adjustments=20to?= =?UTF-8?q?=20try=20to=20fix=20cron=20job=20issues=20part=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/otp.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index 28337fc..636542e 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -60,5 +60,5 @@ describe("Tests", () => { expect(checkTokens).toBeNull(); } } - }, 100000); + }, 200000); }); From 4fea80fe5b1d845f20cd0965ee614f8221d6db1e Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 19 Nov 2024 15:38:08 +1100 Subject: [PATCH 09/13] rewrote otp to use redis > db, and added verification route --- backend/src/index.ts | 39 +++++++++++++++++--- backend/src/routes/OTP/OTPToken.ts | 52 +++++++++++++++++++++++++++ backend/src/routes/OTP/generateOTP.ts | 52 ++++++--------------------- backend/src/routes/OTP/sendEmail.ts | 49 ++++++++++++++++--------- backend/src/routes/OTP/verifyOTP.ts | 12 +++++++ 5 files changed, 141 insertions(+), 63 deletions(-) create mode 100644 backend/src/routes/OTP/verifyOTP.ts diff --git a/backend/src/index.ts b/backend/src/index.ts index 901e8ab..7168e75 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -10,6 +10,11 @@ import RedisStore from "connect-redis"; import { createClient } from "redis"; import { generateOTP } from "./routes/OTP/generateOTP"; import { removeExpiredOTPs } from "./routes/OTP/deleteExpired"; +import dotenv from 'dotenv'; +import { verifyOTP } from "./routes/OTP/verifyOTP"; + +dotenv.config(); + declare module "express-session" { interface SessionData { userId: number; @@ -18,6 +23,7 @@ declare module "express-session" { // Initialize client. if (process.env["REDIS_PORT"] === undefined) { + console.log(process.env); console.error("Redis port not provided in .env file"); process.exit(1); } @@ -67,6 +73,7 @@ app.post( async (req: TypedRequest, res: Response) => { const { username, email, password, userType } = req.body; + console.log(`username: ${username}, email: ${email}, password: ${password}, userType: ${userType}`); // check database for existing user with same username const errorCheck: LoginErrors = { matchingCredentials: true, @@ -110,17 +117,18 @@ app.post( } ); -app.post("/auth/otp", async(req: Request, res: Response) => { +app.post("/auth/otp/generate", async(req: Request, res: Response) => { try { - const { email } = req.body; + const { email } : { email: string} = req.body; if(!email) { throw new Error("Email address expected."); } - const result = await generateOTP(email, SALT_ROUNDS); + const token = await generateOTP(email, SALT_ROUNDS); - if (result) { + if (token) { + await redisClient.set(email, token, { EX: 60 }); return res.status(200).json({ message: "ok" }); } else { return res.status(400).json( {message: "Unexpected error while generating OTP."} ); @@ -131,6 +139,29 @@ app.post("/auth/otp", async(req: Request, res: Response) => { } }); +app.post("/auth/otp/verify", async(req: Request, res: Response) => { + try { + const { email, token } = req.body; + + if(!token) { + throw new Error("One time code expected"); + } + + const hash = await redisClient.get(email); + + if(!hash) { + throw Error("One time code has expired."); + } + + await verifyOTP(token, hash); + + return res.status(200).json({message: "ok" }); + + } catch (error) { + return res.status(400).json({ message: (error as Error).message }); + } +}); + app.post("/auth/login", async (req: TypedRequest, res: Response) => { try { const { username, password } = req.body; diff --git a/backend/src/routes/OTP/OTPToken.ts b/backend/src/routes/OTP/OTPToken.ts index cf1ba74..5b6aa8e 100644 --- a/backend/src/routes/OTP/OTPToken.ts +++ b/backend/src/routes/OTP/OTPToken.ts @@ -1,8 +1,60 @@ import { User } from "@prisma/client"; +import prisma from "../../prisma"; export interface OTPToken { user: User, token: string, timeCreated: Date, expiryTime: Date, +} + + +export const getUserFromEmail = async (emailAddress: string) => { + const result = await prisma.user.findUnique({ + where: { + email: emailAddress, + }, + }); + + return result; +} + +export const deleteToken = async(userId: number) => { + const result = await prisma.otpToken.deleteMany({ + where: { + userId + } + }); + + return result; +} + +export const addTokenToDb = async(token: OTPToken) => { + const result = await prisma.otpToken.create({ + data: { + userId: token.user.id, + token: token.token, + timeCreated: token.timeCreated, + expiryTime: token.expiryTime + } + }); + return result; +} + +export const getTokenFromEmail = async(emailAddress: string) => { + const user = await getUserFromEmail(emailAddress); + if(!user) { + throw new Error(`User with email address ${emailAddress} not found.`) + } + const otpToken = await prisma.otpToken.findFirst({ + where: { + userId: user.id + } + }); + + if(!otpToken) { + throw new Error(`OTP Token for ${emailAddress} not found.`); + } + + return otpToken; } \ No newline at end of file diff --git a/backend/src/routes/OTP/generateOTP.ts b/backend/src/routes/OTP/generateOTP.ts index 74d0076..548249d 100644 --- a/backend/src/routes/OTP/generateOTP.ts +++ b/backend/src/routes/OTP/generateOTP.ts @@ -1,8 +1,9 @@ import crypto from 'crypto'; import bcrypt from 'bcrypt'; import prisma from '../../prisma'; -import { OTPToken } from './OTPToken'; +import { addTokenToDb, deleteToken, getUserFromEmail, OTPToken } from './OTPToken'; import { sendEmail } from './sendEmail'; +import { RedisClientType, RedisModules } from 'redis'; export const generateOTP = async (emailAddress: string, salt_rounds: number) => { const user = await getUserFromEmail(emailAddress); @@ -11,55 +12,22 @@ export const generateOTP = async (emailAddress: string, salt_rounds: number) => throw new Error("User not found."); } - await deleteToken(user.id); + //await deleteToken(user.id); const rand = crypto.randomBytes(4).readUint32BE(0); const sixDigits = (rand % 900000) + 100000; const otpCode = sixDigits.toString(); const hash = await bcrypt.hash(otpCode, salt_rounds); - const token: OTPToken = { - user, - token: hash, - timeCreated: new Date(), - expiryTime: new Date(Date.now()+60000), - }; + // const token: OTPToken = { + // token: hash, + // timeCreated: new Date(), + // expiryTime: new Date(Date.now()+60000), + // }; - await addTokenToDb(token); + //await addTokenToDb(token); await sendEmail(emailAddress, user.username, otpCode); - return true; + return hash; } - -const getUserFromEmail = async (emailAddress: string) => { - const result = await prisma.user.findUnique({ - where: { - email: emailAddress, - }, - }); - - return result; -} - -const deleteToken = async(userId: number) => { - const result = await prisma.otpToken.deleteMany({ - where: { - userId - } - }); - - return result; -} - -const addTokenToDb = async(token: OTPToken) => { - const result = await prisma.otpToken.create({ - data: { - userId: token.user.id, - token: token.token, - timeCreated: token.timeCreated, - expiryTime: token.expiryTime - } - }); - return result; -} \ No newline at end of file diff --git a/backend/src/routes/OTP/sendEmail.ts b/backend/src/routes/OTP/sendEmail.ts index bfb51a3..7932058 100644 --- a/backend/src/routes/OTP/sendEmail.ts +++ b/backend/src/routes/OTP/sendEmail.ts @@ -1,26 +1,41 @@ import Mailgun from "mailgun.js"; import FormData from 'form-data'; -//const mailgun = new Mailgun(FormData); -//const mg = mailgun.client({username: 'api', key: process.env["EMAIL_KEY"] || ""}); +const mailgun = new Mailgun(FormData); export const sendEmail = async (emailAddress: string, userName: string, code: string) => { - // if(!mg){ - // throw new Error("invalid email key"); - // } + const key = process.env["EMAIL_KEY"] + // don't send email when running tests. + if(key === "test") { + return; + } + + const mg = mailgun.client({username: 'api', key: key || ""}); + if(!mg){ + throw new Error("invalid email key"); + } console.log(`emailing ${userName} at ${emailAddress} with otp ${code}.`); -// const senderDomain = ""; + const senderDomain = "mg.pyrmds.app"; -// try { -// await mg.messages.create(`${senderDomain}`, { -// from: `No Reply `, -// to: [emailAddress], -// subject: "Pyramids Login One Time Code", -// text: "", -// html: "" -// }); -// } catch (error) { -// throw new Error("sending of email failed"); -// } + try { + const res = await mg.messages.create(`${senderDomain}`, { + from: `No Reply `, + to: [emailAddress], + subject: "Pyramids Login One Time Code", + text: "Hi", + html: ` +
+

Dear ${userName},

+

We have received a request from you to reset your password for Pyramids.

+

Your one time code is: ${code}.

+

Please note that this code will expire in 5 minutes.

+
+

Thank you.

+
` + }); + console.log(res); + } catch (error) { + throw new Error(`sending of email failed due to ${(error as Error).message}`); + } } \ No newline at end of file diff --git a/backend/src/routes/OTP/verifyOTP.ts b/backend/src/routes/OTP/verifyOTP.ts new file mode 100644 index 0000000..c499717 --- /dev/null +++ b/backend/src/routes/OTP/verifyOTP.ts @@ -0,0 +1,12 @@ +import prisma from "../../prisma"; +import { getTokenFromEmail } from "./OTPToken"; +import bcrypt from 'bcrypt'; + +export const verifyOTP = async (token: string, hash: string) => { + const verify = await bcrypt.compare(token, hash); + + if(!verify) { + throw new Error("Incorrect code."); + } +} + From ad81fd1216ea0795257a6ee4ea468c1bed32eaa4 Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 3 Dec 2024 14:46:49 +1100 Subject: [PATCH 10/13] rewrote tests for redis --- backend/src/index.ts | 6 +++- backend/src/routes/OTP/generateOTP.ts | 1 - backend/src/routes/OTP/verifyOTP.ts | 2 ++ backend/tests/otp.test.ts | 52 ++++++++++++++------------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 7168e75..46a5444 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -143,8 +143,12 @@ app.post("/auth/otp/verify", async(req: Request, res: Response) => { try { const { email, token } = req.body; + if(!email) { + throw new Error("Email address expected."); + } + if(!token) { - throw new Error("One time code expected"); + throw new Error("One time code expected."); } const hash = await redisClient.get(email); diff --git a/backend/src/routes/OTP/generateOTP.ts b/backend/src/routes/OTP/generateOTP.ts index 548249d..719d553 100644 --- a/backend/src/routes/OTP/generateOTP.ts +++ b/backend/src/routes/OTP/generateOTP.ts @@ -1,6 +1,5 @@ import crypto from 'crypto'; import bcrypt from 'bcrypt'; -import prisma from '../../prisma'; import { addTokenToDb, deleteToken, getUserFromEmail, OTPToken } from './OTPToken'; import { sendEmail } from './sendEmail'; import { RedisClientType, RedisModules } from 'redis'; diff --git a/backend/src/routes/OTP/verifyOTP.ts b/backend/src/routes/OTP/verifyOTP.ts index c499717..1265d93 100644 --- a/backend/src/routes/OTP/verifyOTP.ts +++ b/backend/src/routes/OTP/verifyOTP.ts @@ -8,5 +8,7 @@ export const verifyOTP = async (token: string, hash: string) => { if(!verify) { throw new Error("Incorrect code."); } + + return verify; } diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index 636542e..e22bffe 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -1,15 +1,32 @@ //test/sample.test.ts import { expect, test, vi, describe } from "vitest"; // 👈🏻 Added the `vi` import -import prisma from "../src/prisma"; import request from "supertest"; import app from "../src/index"; +import { createClient } from "redis"; +import { afterEach, beforeEach } from "node:test"; +import prisma from "../src/prisma"; + +let redisClient: ReturnType; + +beforeEach(async () => { + redisClient = createClient({ + url: `redis://localhost:${process.env["REDIS_PORT"]}`, + }); + await redisClient.connect().catch(console.error); +}); + +afterEach(async () => { + await redisClient.flushDb(); + await redisClient.quit(); +}); describe("Tests", () => { - test("register test", async () => { + test("otp create test", async () => { + const { status, body } = await request(app).post("/auth/register").send({ username: "richard grayson", password: "iheartkori&barbs", - email: "nightwing1@gmail.com", + email: "pyramidstestdump@gmail.com", userType: "ATTENDEE", }); @@ -28,35 +45,20 @@ describe("Tests", () => { if(newUser) { const response = await request(app).post("/auth/otp").send({ - email: "nightwing1@gmail.com" + email: "pyramidstestdump@gmail.com" }); expect(response.status).toBe(200); - const tokens = await prisma.otpToken.findMany({ - where: { - userId: newUser.id - } - }); + const hash = await redisClient.get(newUser.email); - expect(tokens.length).toBe(1); - - const newToken = await prisma.otpToken.findFirst({ - where: { - userId: newUser.id - } - }); - expect(newToken).not.toBeNull(); + expect(hash).not.toBeNull(); - if(newToken) { - console.log(newToken.token); + if(hash) { + console.log(hash); - await new Promise(resolve => setTimeout(resolve, 120000)); + await new Promise(resolve => setTimeout(resolve, 70000)); - const checkTokens = await prisma.otpToken.findFirst({ - where: { - userId: newUser.id - } - }); + const checkTokens = await redisClient.get(newUser.email); expect(checkTokens).toBeNull(); } } From f737b76f13ad6fd42c07ffe01eb1f73250b12792 Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 3 Dec 2024 15:08:38 +1100 Subject: [PATCH 11/13] fixed otp test for redis --- backend/src/index.ts | 3 --- backend/tests/otp.test.ts | 22 +++++++--------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 46a5444..6a333e1 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -10,11 +10,8 @@ import RedisStore from "connect-redis"; import { createClient } from "redis"; import { generateOTP } from "./routes/OTP/generateOTP"; import { removeExpiredOTPs } from "./routes/OTP/deleteExpired"; -import dotenv from 'dotenv'; import { verifyOTP } from "./routes/OTP/verifyOTP"; -dotenv.config(); - declare module "express-session" { interface SessionData { userId: number; diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index e22bffe..f67e00e 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -6,22 +6,14 @@ import { createClient } from "redis"; import { afterEach, beforeEach } from "node:test"; import prisma from "../src/prisma"; -let redisClient: ReturnType; - -beforeEach(async () => { - redisClient = createClient({ - url: `redis://localhost:${process.env["REDIS_PORT"]}`, - }); - await redisClient.connect().catch(console.error); -}); - -afterEach(async () => { - await redisClient.flushDb(); - await redisClient.quit(); -}); - describe("Tests", () => { test("otp create test", async () => { + const redisClient = createClient({ + url: `redis://localhost:${process.env["REDIS_PORT"]}`, + }); + await redisClient.connect().catch(console.error); + + expect(redisClient).not.toBeUndefined(); const { status, body } = await request(app).post("/auth/register").send({ username: "richard grayson", @@ -44,7 +36,7 @@ describe("Tests", () => { }); if(newUser) { - const response = await request(app).post("/auth/otp").send({ + const response = await request(app).post("/auth/otp/generate").send({ email: "pyramidstestdump@gmail.com" }); expect(response.status).toBe(200); From 8a0eb32f3976723556cfa9e98299341d2e1e6dda Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 3 Dec 2024 15:21:46 +1100 Subject: [PATCH 12/13] added conditional for ci --- backend/src/routes/OTP/sendEmail.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/routes/OTP/sendEmail.ts b/backend/src/routes/OTP/sendEmail.ts index 7932058..5b710c4 100644 --- a/backend/src/routes/OTP/sendEmail.ts +++ b/backend/src/routes/OTP/sendEmail.ts @@ -4,6 +4,7 @@ import FormData from 'form-data'; const mailgun = new Mailgun(FormData); export const sendEmail = async (emailAddress: string, userName: string, code: string) => { + if(process.env["CI"]) return; const key = process.env["EMAIL_KEY"] // don't send email when running tests. if(key === "test") { From 7e5d5efb094fd5f4f5937a7ccf9e85c0b08273ea Mon Sep 17 00:00:00 2001 From: Isaac Date: Sun, 8 Dec 2024 22:18:33 +1100 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=8E=8A=20forgot=20password=20backen?= =?UTF-8?q?d=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20241206134721_init/migration.sql | 11 +++++ backend/prisma/schema.prisma | 10 ----- backend/src/index.ts | 8 ++-- backend/src/routes/OTP/OTPToken.ts | 40 ------------------- backend/src/routes/OTP/deleteExpired.ts | 16 -------- backend/src/routes/OTP/generateOTP.ts | 13 +----- backend/src/routes/OTP/sendEmail.ts | 4 +- backend/src/routes/OTP/verifyOTP.ts | 5 +-- backend/tests/otp.test.ts | 1 - 9 files changed, 18 insertions(+), 90 deletions(-) create mode 100644 backend/prisma/migrations/20241206134721_init/migration.sql delete mode 100644 backend/src/routes/OTP/deleteExpired.ts diff --git a/backend/prisma/migrations/20241206134721_init/migration.sql b/backend/prisma/migrations/20241206134721_init/migration.sql new file mode 100644 index 0000000..fefa79e --- /dev/null +++ b/backend/prisma/migrations/20241206134721_init/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the `OtpToken` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "OtpToken" DROP CONSTRAINT "OtpToken_userId_fkey"; + +-- DropTable +DROP TABLE "OtpToken"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 7627cd8..99ee565 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -30,7 +30,6 @@ model User { salt String dateJoined DateTime profilePicture String? - oneTimeToken OtpToken? } model Attendee { @@ -69,13 +68,4 @@ model Keyword { text String @unique attendees Attendee[] events Event[] -} - -model OtpToken { - id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int @unique - token String - timeCreated DateTime - expiryTime DateTime } \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 6a333e1..41f4aab 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -9,7 +9,6 @@ import prisma from "./prisma"; import RedisStore from "connect-redis"; import { createClient } from "redis"; import { generateOTP } from "./routes/OTP/generateOTP"; -import { removeExpiredOTPs } from "./routes/OTP/deleteExpired"; import { verifyOTP } from "./routes/OTP/verifyOTP"; declare module "express-session" { @@ -57,9 +56,6 @@ app.use( }) ); -// OTP removing cron job -removeExpiredOTPs.start(); - app.get("/", (req: Request, res: Response) => { console.log("Hello, TypeScript with Express :)))!"); res.send("Hello, TypeScript with Express :)))!"); @@ -151,11 +147,13 @@ app.post("/auth/otp/verify", async(req: Request, res: Response) => { const hash = await redisClient.get(email); if(!hash) { - throw Error("One time code has expired."); + throw new Error("One time code is invalid or expired."); } await verifyOTP(token, hash); + await redisClient.del(email); + return res.status(200).json({message: "ok" }); } catch (error) { diff --git a/backend/src/routes/OTP/OTPToken.ts b/backend/src/routes/OTP/OTPToken.ts index 5b6aa8e..e10440c 100644 --- a/backend/src/routes/OTP/OTPToken.ts +++ b/backend/src/routes/OTP/OTPToken.ts @@ -17,44 +17,4 @@ export const getUserFromEmail = async (emailAddress: string) => { }); return result; -} - -export const deleteToken = async(userId: number) => { - const result = await prisma.otpToken.deleteMany({ - where: { - userId - } - }); - - return result; -} - -export const addTokenToDb = async(token: OTPToken) => { - const result = await prisma.otpToken.create({ - data: { - userId: token.user.id, - token: token.token, - timeCreated: token.timeCreated, - expiryTime: token.expiryTime - } - }); - return result; -} - -export const getTokenFromEmail = async(emailAddress: string) => { - const user = await getUserFromEmail(emailAddress); - if(!user) { - throw new Error(`User with email address ${emailAddress} not found.`) - } - const otpToken = await prisma.otpToken.findFirst({ - where: { - userId: user.id - } - }); - - if(!otpToken) { - throw new Error(`OTP Token for ${emailAddress} not found.`); - } - - return otpToken; } \ No newline at end of file diff --git a/backend/src/routes/OTP/deleteExpired.ts b/backend/src/routes/OTP/deleteExpired.ts deleted file mode 100644 index 85f7daf..0000000 --- a/backend/src/routes/OTP/deleteExpired.ts +++ /dev/null @@ -1,16 +0,0 @@ -import cron from 'node-cron'; -import prisma from '../../prisma'; - -export const removeExpiredOTPs = cron.schedule('* * * * *', async () => { - try { - await prisma.otpToken.deleteMany({ - where: { - expiryTime: { - lte: new Date(), - }, - }, - }); - } catch (error) { - console.error(error); - } - }); \ No newline at end of file diff --git a/backend/src/routes/OTP/generateOTP.ts b/backend/src/routes/OTP/generateOTP.ts index 719d553..1c8d474 100644 --- a/backend/src/routes/OTP/generateOTP.ts +++ b/backend/src/routes/OTP/generateOTP.ts @@ -1,8 +1,7 @@ import crypto from 'crypto'; import bcrypt from 'bcrypt'; -import { addTokenToDb, deleteToken, getUserFromEmail, OTPToken } from './OTPToken'; +import { getUserFromEmail } from './OTPToken'; import { sendEmail } from './sendEmail'; -import { RedisClientType, RedisModules } from 'redis'; export const generateOTP = async (emailAddress: string, salt_rounds: number) => { const user = await getUserFromEmail(emailAddress); @@ -11,21 +10,11 @@ export const generateOTP = async (emailAddress: string, salt_rounds: number) => throw new Error("User not found."); } - //await deleteToken(user.id); - const rand = crypto.randomBytes(4).readUint32BE(0); const sixDigits = (rand % 900000) + 100000; const otpCode = sixDigits.toString(); const hash = await bcrypt.hash(otpCode, salt_rounds); - // const token: OTPToken = { - // token: hash, - // timeCreated: new Date(), - // expiryTime: new Date(Date.now()+60000), - // }; - - //await addTokenToDb(token); - await sendEmail(emailAddress, user.username, otpCode); return hash; diff --git a/backend/src/routes/OTP/sendEmail.ts b/backend/src/routes/OTP/sendEmail.ts index 5b710c4..bc26203 100644 --- a/backend/src/routes/OTP/sendEmail.ts +++ b/backend/src/routes/OTP/sendEmail.ts @@ -16,7 +16,7 @@ export const sendEmail = async (emailAddress: string, userName: string, code: st throw new Error("invalid email key"); } - console.log(`emailing ${userName} at ${emailAddress} with otp ${code}.`); + //console.log(`emailing ${userName} at ${emailAddress} with otp ${code}.`); const senderDomain = "mg.pyrmds.app"; try { @@ -30,7 +30,7 @@ export const sendEmail = async (emailAddress: string, userName: string, code: st

Dear ${userName},

We have received a request from you to reset your password for Pyramids.

Your one time code is: ${code}.

-

Please note that this code will expire in 5 minutes.

+

Please note that this code will expire in 60 seconds.


Thank you.

` diff --git a/backend/src/routes/OTP/verifyOTP.ts b/backend/src/routes/OTP/verifyOTP.ts index 1265d93..9bc55f0 100644 --- a/backend/src/routes/OTP/verifyOTP.ts +++ b/backend/src/routes/OTP/verifyOTP.ts @@ -1,5 +1,3 @@ -import prisma from "../../prisma"; -import { getTokenFromEmail } from "./OTPToken"; import bcrypt from 'bcrypt'; export const verifyOTP = async (token: string, hash: string) => { @@ -10,5 +8,4 @@ export const verifyOTP = async (token: string, hash: string) => { } return verify; -} - +} \ No newline at end of file diff --git a/backend/tests/otp.test.ts b/backend/tests/otp.test.ts index f67e00e..ebcd0c7 100644 --- a/backend/tests/otp.test.ts +++ b/backend/tests/otp.test.ts @@ -3,7 +3,6 @@ import { expect, test, vi, describe } from "vitest"; // 👈🏻 Added the `vi` import request from "supertest"; import app from "../src/index"; import { createClient } from "redis"; -import { afterEach, beforeEach } from "node:test"; import prisma from "../src/prisma"; describe("Tests", () => {