From 8ffe1a2a12794ddf6e7ef42f083876217dc13e0e Mon Sep 17 00:00:00 2001 From: gyumi Date: Tue, 19 Nov 2024 01:44:38 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=98=B6=20began=20working=20on=20forgotten?= =?UTF-8?q?=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(); + } + } + }); +});