feat: working login flow and registration system
This commit is contained in:
parent
b0a3fa82aa
commit
041babae70
27 changed files with 800 additions and 74 deletions
|
@ -80,6 +80,7 @@ Redis Configuration
|
||||||
Mailing Configuration
|
Mailing Configuration
|
||||||
- `POSTAL_BASE_URL` - Base URL for the Postal API
|
- `POSTAL_BASE_URL` - Base URL for the Postal API
|
||||||
- `POSTAL_API_KEY` - API Key for the Postal API
|
- `POSTAL_API_KEY` - API Key for the Postal API
|
||||||
|
- `FROM_EMAIL` - Email address to send emails from
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|
|
@ -37,12 +37,15 @@
|
||||||
"@nestjs/terminus": "^10.2.3",
|
"@nestjs/terminus": "^10.2.3",
|
||||||
"@nestjs/typeorm": "^10.0.2",
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"argon2": "^0.40.3",
|
||||||
"bullmq": "^5.9.0",
|
"bullmq": "^5.9.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"hbs": "^4.2.0",
|
"hbs": "^4.2.0",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"keygrip": "^1.1.0",
|
||||||
"mysql2": "^3.10.2",
|
"mysql2": "^3.10.2",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"nestjs-otel": "^6.1.1",
|
"nestjs-otel": "^6.1.1",
|
||||||
|
@ -61,9 +64,12 @@
|
||||||
"@nestjs/cli": "^10.4.2",
|
"@nestjs/cli": "^10.4.2",
|
||||||
"@nestjs/schematics": "^10.1.2",
|
"@nestjs/schematics": "^10.1.2",
|
||||||
"@nestjs/testing": "^10.3.10",
|
"@nestjs/testing": "^10.3.10",
|
||||||
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/keygrip": "^1.0.6",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
|
"@types/oidc-provider": "^8.5.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||||
"@typescript-eslint/parser": "^7.16.1",
|
"@typescript-eslint/parser": "^7.16.1",
|
||||||
|
|
129
pnpm-lock.yaml
129
pnpm-lock.yaml
|
@ -41,6 +41,9 @@ importers:
|
||||||
'@opentelemetry/api':
|
'@opentelemetry/api':
|
||||||
specifier: ^1.9.0
|
specifier: ^1.9.0
|
||||||
version: 1.9.0
|
version: 1.9.0
|
||||||
|
argon2:
|
||||||
|
specifier: ^0.40.3
|
||||||
|
version: 0.40.3
|
||||||
bullmq:
|
bullmq:
|
||||||
specifier: ^5.9.0
|
specifier: ^5.9.0
|
||||||
version: 5.9.0
|
version: 5.9.0
|
||||||
|
@ -50,6 +53,9 @@ importers:
|
||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.14.1
|
specifier: ^0.14.1
|
||||||
version: 0.14.1
|
version: 0.14.1
|
||||||
|
cookie-parser:
|
||||||
|
specifier: ^1.4.6
|
||||||
|
version: 1.4.6
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.5
|
specifier: ^16.4.5
|
||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
|
@ -59,6 +65,9 @@ importers:
|
||||||
ioredis:
|
ioredis:
|
||||||
specifier: ^5.4.1
|
specifier: ^5.4.1
|
||||||
version: 5.4.1
|
version: 5.4.1
|
||||||
|
keygrip:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
mysql2:
|
mysql2:
|
||||||
specifier: ^3.10.2
|
specifier: ^3.10.2
|
||||||
version: 3.10.2
|
version: 3.10.2
|
||||||
|
@ -108,15 +117,24 @@ importers:
|
||||||
'@nestjs/testing':
|
'@nestjs/testing':
|
||||||
specifier: ^10.3.10
|
specifier: ^10.3.10
|
||||||
version: 10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10))
|
version: 10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10))
|
||||||
|
'@types/cookie-parser':
|
||||||
|
specifier: ^1.4.7
|
||||||
|
version: 1.4.7
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
'@types/jest':
|
'@types/jest':
|
||||||
specifier: ^29.5.12
|
specifier: ^29.5.12
|
||||||
version: 29.5.12
|
version: 29.5.12
|
||||||
|
'@types/keygrip':
|
||||||
|
specifier: ^1.0.6
|
||||||
|
version: 1.0.6
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.14.10
|
specifier: ^20.14.10
|
||||||
version: 20.14.10
|
version: 20.14.10
|
||||||
|
'@types/oidc-provider':
|
||||||
|
specifier: ^8.5.1
|
||||||
|
version: 8.5.1
|
||||||
'@types/supertest':
|
'@types/supertest':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
|
@ -824,6 +842,10 @@ packages:
|
||||||
resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==}
|
resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@phc/format@1.0.0':
|
||||||
|
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
@ -867,6 +889,9 @@ packages:
|
||||||
'@tsconfig/node16@1.0.4':
|
'@tsconfig/node16@1.0.4':
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
|
'@types/accepts@1.3.7':
|
||||||
|
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
|
@ -885,9 +910,18 @@ packages:
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
|
'@types/content-disposition@0.5.8':
|
||||||
|
resolution: {integrity: sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==}
|
||||||
|
|
||||||
|
'@types/cookie-parser@1.4.7':
|
||||||
|
resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==}
|
||||||
|
|
||||||
'@types/cookiejar@2.1.5':
|
'@types/cookiejar@2.1.5':
|
||||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||||
|
|
||||||
|
'@types/cookies@0.9.0':
|
||||||
|
resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==}
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||||
|
|
||||||
|
@ -906,6 +940,9 @@ packages:
|
||||||
'@types/graceful-fs@4.1.9':
|
'@types/graceful-fs@4.1.9':
|
||||||
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
|
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
|
||||||
|
|
||||||
|
'@types/http-assert@1.5.5':
|
||||||
|
resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==}
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4':
|
'@types/http-cache-semantics@4.0.4':
|
||||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||||
|
|
||||||
|
@ -927,6 +964,15 @@ packages:
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/keygrip@1.0.6':
|
||||||
|
resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==}
|
||||||
|
|
||||||
|
'@types/koa-compose@3.2.8':
|
||||||
|
resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==}
|
||||||
|
|
||||||
|
'@types/koa@2.15.0':
|
||||||
|
resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==}
|
||||||
|
|
||||||
'@types/methods@1.1.4':
|
'@types/methods@1.1.4':
|
||||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||||
|
|
||||||
|
@ -936,6 +982,9 @@ packages:
|
||||||
'@types/node@20.14.10':
|
'@types/node@20.14.10':
|
||||||
resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
|
resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
|
||||||
|
|
||||||
|
'@types/oidc-provider@8.5.1':
|
||||||
|
resolution: {integrity: sha512-NS8tBPOj9GG6SxyrUHWBzglOtAYNDX41J4cRE45oeK0iSqI6V6tDW70aPWg25pJFNSC1evccXFm9evfwjxm7HQ==}
|
||||||
|
|
||||||
'@types/qs@6.9.15':
|
'@types/qs@6.9.15':
|
||||||
resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==}
|
resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==}
|
||||||
|
|
||||||
|
@ -1202,6 +1251,10 @@ packages:
|
||||||
arg@5.0.2:
|
arg@5.0.2:
|
||||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||||
|
|
||||||
|
argon2@0.40.3:
|
||||||
|
resolution: {integrity: sha512-FrSmz4VeM91jwFvvjsQv9GYp6o/kARWoYKjbjDB2U5io1H3e5X67PYGclFDeQff6UXIhUd4aHR3mxCdBbMMuQw==}
|
||||||
|
engines: {node: '>=16.17.0'}
|
||||||
|
|
||||||
argparse@1.0.10:
|
argparse@1.0.10:
|
||||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||||
|
|
||||||
|
@ -1551,9 +1604,17 @@ packages:
|
||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
cookie-parser@1.4.6:
|
||||||
|
resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
|
cookie@0.4.1:
|
||||||
|
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
cookie@0.6.0:
|
cookie@0.6.0:
|
||||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -2918,6 +2979,10 @@ packages:
|
||||||
node-abort-controller@3.1.1:
|
node-abort-controller@3.1.1:
|
||||||
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
||||||
|
|
||||||
|
node-addon-api@8.1.0:
|
||||||
|
resolution: {integrity: sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==}
|
||||||
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
|
|
||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==}
|
||||||
|
|
||||||
|
@ -2934,6 +2999,10 @@ packages:
|
||||||
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
node-gyp-build@4.8.1:
|
||||||
|
resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-int64@0.4.0:
|
node-int64@0.4.0:
|
||||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||||
|
|
||||||
|
@ -4894,6 +4963,8 @@ snapshots:
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.25.1': {}
|
'@opentelemetry/semantic-conventions@1.25.1': {}
|
||||||
|
|
||||||
|
'@phc/format@1.0.0': {}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
@ -4927,6 +4998,10 @@ snapshots:
|
||||||
|
|
||||||
'@tsconfig/node16@1.0.4': {}
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
|
'@types/accepts@1.3.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.14.10
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.24.8
|
'@babel/parser': 7.24.8
|
||||||
|
@ -4957,8 +5032,21 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.14.10
|
'@types/node': 20.14.10
|
||||||
|
|
||||||
|
'@types/content-disposition@0.5.8': {}
|
||||||
|
|
||||||
|
'@types/cookie-parser@1.4.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
|
||||||
'@types/cookiejar@2.1.5': {}
|
'@types/cookiejar@2.1.5': {}
|
||||||
|
|
||||||
|
'@types/cookies@0.9.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/connect': 3.4.38
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
'@types/keygrip': 1.0.6
|
||||||
|
'@types/node': 20.14.10
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 8.56.10
|
'@types/eslint': 8.56.10
|
||||||
|
@ -4989,6 +5077,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.14.10
|
'@types/node': 20.14.10
|
||||||
|
|
||||||
|
'@types/http-assert@1.5.5': {}
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4': {}
|
'@types/http-cache-semantics@4.0.4': {}
|
||||||
|
|
||||||
'@types/http-errors@2.0.4': {}
|
'@types/http-errors@2.0.4': {}
|
||||||
|
@ -5010,6 +5100,23 @@ snapshots:
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/keygrip@1.0.6': {}
|
||||||
|
|
||||||
|
'@types/koa-compose@3.2.8':
|
||||||
|
dependencies:
|
||||||
|
'@types/koa': 2.15.0
|
||||||
|
|
||||||
|
'@types/koa@2.15.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/accepts': 1.3.7
|
||||||
|
'@types/content-disposition': 0.5.8
|
||||||
|
'@types/cookies': 0.9.0
|
||||||
|
'@types/http-assert': 1.5.5
|
||||||
|
'@types/http-errors': 2.0.4
|
||||||
|
'@types/keygrip': 1.0.6
|
||||||
|
'@types/koa-compose': 3.2.8
|
||||||
|
'@types/node': 20.14.10
|
||||||
|
|
||||||
'@types/methods@1.1.4': {}
|
'@types/methods@1.1.4': {}
|
||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
'@types/mime@1.3.5': {}
|
||||||
|
@ -5018,6 +5125,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
|
|
||||||
|
'@types/oidc-provider@8.5.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/koa': 2.15.0
|
||||||
|
'@types/node': 20.14.10
|
||||||
|
|
||||||
'@types/qs@6.9.15': {}
|
'@types/qs@6.9.15': {}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
|
@ -5324,6 +5436,12 @@ snapshots:
|
||||||
|
|
||||||
arg@5.0.2: {}
|
arg@5.0.2: {}
|
||||||
|
|
||||||
|
argon2@0.40.3:
|
||||||
|
dependencies:
|
||||||
|
'@phc/format': 1.0.0
|
||||||
|
node-addon-api: 8.1.0
|
||||||
|
node-gyp-build: 4.8.1
|
||||||
|
|
||||||
argparse@1.0.10:
|
argparse@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js: 1.0.3
|
sprintf-js: 1.0.3
|
||||||
|
@ -5737,8 +5855,15 @@ snapshots:
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
cookie-parser@1.4.6:
|
||||||
|
dependencies:
|
||||||
|
cookie: 0.4.1
|
||||||
|
cookie-signature: 1.0.6
|
||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
|
cookie@0.4.1: {}
|
||||||
|
|
||||||
cookie@0.6.0: {}
|
cookie@0.6.0: {}
|
||||||
|
|
||||||
cookiejar@2.1.4: {}
|
cookiejar@2.1.4: {}
|
||||||
|
@ -7368,6 +7493,8 @@ snapshots:
|
||||||
|
|
||||||
node-abort-controller@3.1.1: {}
|
node-abort-controller@3.1.1: {}
|
||||||
|
|
||||||
|
node-addon-api@8.1.0: {}
|
||||||
|
|
||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
|
@ -7381,6 +7508,8 @@ snapshots:
|
||||||
detect-libc: 2.0.3
|
detect-libc: 2.0.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-gyp-build@4.8.1: {}
|
||||||
|
|
||||||
node-int64@0.4.0: {}
|
node-int64@0.4.0: {}
|
||||||
|
|
||||||
node-releases@2.0.14: {}
|
node-releases@2.0.14: {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Controller, Get, Redirect, Render } from '@nestjs/common';
|
import { Controller, Get, Redirect } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { PostalClientService } from 'nestjs-postal-client';
|
import { PostalClientService } from 'nestjs-postal-client';
|
||||||
|
|
||||||
|
@ -14,18 +14,4 @@ export class AppController {
|
||||||
getHello(): any {
|
getHello(): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('email_test')
|
|
||||||
async getEmailTest(): Promise<any> {
|
|
||||||
await this.postal.sendMessage({
|
|
||||||
to: ['kakious@kakio.us'],
|
|
||||||
subject: 'Test email',
|
|
||||||
from: 'kakious@kakio.us',
|
|
||||||
plain_body: 'This is a test email',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: 'Email sent',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
20
src/auth/auth.const.ts
Normal file
20
src/auth/auth.const.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Redis Password Reset Const
|
||||||
|
export const PASSWORD_RESET_CACHE_KEY = 'password_reset:';
|
||||||
|
export const PASSWORD_RESET_EXPIRATION = 60 * 5; // 5 minutes
|
||||||
|
|
||||||
|
export const getResetKey = (code: string): string => `${PASSWORD_RESET_CACHE_KEY}${code}`;
|
||||||
|
|
||||||
|
// Failed Login Attempts Const
|
||||||
|
export const FAILED_LOGIN_ATTEMPTS_CACHE_KEY = 'failed_login_attempts:';
|
||||||
|
export const FAILED_LOGIN_ATTEMPTS_EXPIRATION = 60 * 60 * 1; // 1 hour
|
||||||
|
export const FAILED_LOGIN_ATTEMPTS_LIMIT = 15;
|
||||||
|
|
||||||
|
export const getFailedLoginAttemptsKey = (ip: string): string =>
|
||||||
|
`${FAILED_LOGIN_ATTEMPTS_CACHE_KEY}${ip}`;
|
||||||
|
|
||||||
|
// Registration Const
|
||||||
|
export const REGISTRATION_CACHE_KEY = 'registration_count:';
|
||||||
|
export const REGISTRATION_EXPIRATION = 60 * 60 * 24; // 24 hours
|
||||||
|
export const REGISTRATION_LIMIT = 10;
|
||||||
|
|
||||||
|
export const getRegistrationKey = (ip: string): string => `${REGISTRATION_CACHE_KEY}${ip}`;
|
|
@ -5,11 +5,22 @@ import { OidcService } from './oidc/core.service';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { RedisModule } from '../redis/redis.module';
|
import { RedisModule } from '../redis/redis.module';
|
||||||
import { AuthController } from './controllers/auth.controller';
|
import { AuthController } from './controllers/auth.controller';
|
||||||
|
import { AuthService } from './services/auth.service';
|
||||||
|
import { MailModule } from '../mail/mail.module';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { OidcSession } from '../database/models/oidc_session.model';
|
||||||
|
import { OidcClient } from '../database/models/oidc_client.model';
|
||||||
|
import { OidcClientPermission } from '../database/models/oidc_client_permissions.model';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule, RedisModule],
|
imports: [
|
||||||
|
UserModule,
|
||||||
|
RedisModule,
|
||||||
|
MailModule,
|
||||||
|
TypeOrmModule.forFeature([OidcSession, OidcClient, OidcClientPermission]),
|
||||||
|
],
|
||||||
controllers: [OidcController, AuthController],
|
controllers: [OidcController, AuthController],
|
||||||
providers: [ConfigService, OidcService],
|
providers: [ConfigService, OidcService, AuthService],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|
|
@ -1,34 +1,42 @@
|
||||||
import { BadRequestException, Body, Controller, Get, Post, Render, Res } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Render, Res } from '@nestjs/common';
|
||||||
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
import { ForgotPasswordDto } from '../dto/forgotPassword.dto';
|
||||||
|
import { CreateUserDto } from '../dto/register.dto';
|
||||||
|
import { LoginUserDto } from '../dto/loginUser.dto';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor() {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
// TODO: Implement RateLimit
|
// TODO: Implement RateLimit
|
||||||
public async postHello(@Body() body: any): Promise<any> {
|
public async postLogin(
|
||||||
return body;
|
@Body() body: LoginUserDto,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
): Promise<any> {
|
||||||
|
const sessionData = await this.authService.login(body.username, body.password);
|
||||||
|
|
||||||
|
// process the sessionData.cookies and set it in the response
|
||||||
|
sessionData.cookiesForms.forEach((cookie) => {
|
||||||
|
res.cookie(cookie.name, cookie.value, cookie.options);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sessionData.sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement RateLimit
|
// TODO: Implement RateLimit
|
||||||
@Post('register')
|
@Post('register')
|
||||||
public async postRegister(@Body() body: any): Promise<any> {
|
public async postRegister(@Body() body: CreateUserDto): Promise<any> {
|
||||||
return body;
|
return await this.authService.register(body.username, body.email, body.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement RateLimit
|
// TODO: Implement RateLimit
|
||||||
@Post('reset-password')
|
@Post('reset-password')
|
||||||
public async postForgotPassword(@Body('email') email: string): Promise<any> {
|
public async postForgotPassword(@Body() body: ForgotPasswordDto): Promise<any> {
|
||||||
const user = await this.findUserByEmail(email);
|
return await this.authService.forgotPassword(body.email);
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new BadRequestException({ error: true, message: 'User not found' });
|
|
||||||
} else {
|
|
||||||
await this.sendPasswordResetEmail(email);
|
|
||||||
return { error: false, message: 'Password reset email sent' };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render pages
|
// Render pages
|
||||||
|
@ -60,15 +68,4 @@ export class AuthController {
|
||||||
login: 'login',
|
login: 'login',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper Functions
|
|
||||||
|
|
||||||
private async findUserByEmail(email: string) {
|
|
||||||
// Implement this method to find the user by email
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendPasswordResetEmail(email: string) {
|
|
||||||
// Implement this method to send the password reset email
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
6
src/auth/dto/forgotPassword.dto.ts
Normal file
6
src/auth/dto/forgotPassword.dto.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { IsEmail } from 'class-validator';
|
||||||
|
|
||||||
|
export class ForgotPasswordDto {
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
}
|
11
src/auth/dto/loginUser.dto.ts
Normal file
11
src/auth/dto/loginUser.dto.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { IsString, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginUserDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
12
src/auth/dto/register.dto.ts
Normal file
12
src/auth/dto/register.dto.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
|
@ -25,6 +25,8 @@ import { UserService } from 'src/user/user.service';
|
||||||
import { Span } from 'nestjs-otel';
|
import { Span } from 'nestjs-otel';
|
||||||
import generateId from './helper/nanoid.helper';
|
import generateId from './helper/nanoid.helper';
|
||||||
import { context, trace } from '@opentelemetry/api';
|
import { context, trace } from '@opentelemetry/api';
|
||||||
|
import * as KeyGrip from 'keygrip';
|
||||||
|
import { getEpochTime } from '../../util/time.util';
|
||||||
|
|
||||||
// This is an async import for the oidc-provider package as it's now only esm and we need to use it in a commonjs environment.
|
// This is an async import for the oidc-provider package as it's now only esm and we need to use it in a commonjs environment.
|
||||||
async function getProvider(): Promise<{
|
async function getProvider(): Promise<{
|
||||||
|
@ -445,4 +447,61 @@ export class OidcService implements OnModuleInit {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new OIDC session
|
||||||
|
* @param accountId The account ID
|
||||||
|
* @returns {Promise<{session: any, cookies: string[]}>} - Returns a promise that resolves with the session and cookies
|
||||||
|
*/
|
||||||
|
@Span()
|
||||||
|
async createSession(accountId: string | number) {
|
||||||
|
const loginTs = getEpochTime();
|
||||||
|
const uid = await generateId();
|
||||||
|
const sessionId = await generateId();
|
||||||
|
const expire = new Date();
|
||||||
|
expire.setSeconds(expire.getSeconds() + SESSION_LIFE);
|
||||||
|
accountId = accountId.toString();
|
||||||
|
|
||||||
|
await this.provider.Session.adapter.upsert(
|
||||||
|
sessionId,
|
||||||
|
{
|
||||||
|
jti: sessionId,
|
||||||
|
uid,
|
||||||
|
accountId,
|
||||||
|
loginTs,
|
||||||
|
iat: loginTs,
|
||||||
|
exp: Math.floor(expire.getTime() / 1000),
|
||||||
|
},
|
||||||
|
SESSION_LIFE,
|
||||||
|
);
|
||||||
|
const keyGrip = KeyGrip(this.cookies);
|
||||||
|
const sessionCookie = `_session=${sessionId}; path=/; expires=${expire.toUTCString()}; httponly`;
|
||||||
|
const cookies = [sessionCookie];
|
||||||
|
const [pre, ...post] = sessionCookie.split(';');
|
||||||
|
cookies.push([`_session.sig=${keyGrip.sign(pre)}`, ...post].join(';'));
|
||||||
|
// Map the cookies to bbe in this format { name: string, value: string, options: { expires: Date, sameSite: 'strict', httpOnly: true, path: '/' } }
|
||||||
|
const cookiesForms = [
|
||||||
|
{
|
||||||
|
name: '_session',
|
||||||
|
value: sessionId,
|
||||||
|
options: {
|
||||||
|
expires: expire,
|
||||||
|
sameSite: 'strict',
|
||||||
|
httpOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '_session.sig',
|
||||||
|
value: keyGrip.sign(sessionId),
|
||||||
|
options: {
|
||||||
|
expires: expire,
|
||||||
|
sameSite: 'strict',
|
||||||
|
httpOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
this.logger.debug(`Created new session`, { sessionId, accountId });
|
||||||
|
return { sessionId, cookies, cookiesForms };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { UserService } from '../../user/user.service';
|
||||||
|
import { RedisService } from '../../redis/redis.service';
|
||||||
|
import { MailService } from '../../mail/mail.service';
|
||||||
|
import { getResetKey, PASSWORD_RESET_EXPIRATION } from '../auth.const';
|
||||||
|
import { OidcService } from '../oidc/core.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly redisService: RedisService,
|
||||||
|
private readonly oidcService: OidcService,
|
||||||
|
private readonly mailService: MailService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a password as forgotten. This will send an email to the user with a link to reset their password.
|
||||||
|
* @param email The email address of the user
|
||||||
|
* @returns A message indicating that the password reset email has been sent
|
||||||
|
*/
|
||||||
|
public async forgotPassword(email: string): Promise<{ error: boolean; message: string }> {
|
||||||
|
const user = await this.userService.getUserbyEmail(email);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('No user found with that email');
|
||||||
|
} else {
|
||||||
|
const resetCode = await this.generateResetCode();
|
||||||
|
await this.mailService.sendPasswordResetEmail(email, resetCode);
|
||||||
|
await this.storePasswordResetCode(resetCode, user.id);
|
||||||
|
return { error: false, message: 'Password reset email sent' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset a user's password
|
||||||
|
* @param code The password reset code
|
||||||
|
* @param password The new password
|
||||||
|
*/
|
||||||
|
public async resetPassword(
|
||||||
|
code: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<{ error: boolean; message: string }> {
|
||||||
|
const userId = await this.getPasswordResetCode(code);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new BadRequestException('Invalid reset code');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userService.updatePassword(userId, password);
|
||||||
|
await this.redisService.del(getResetKey(code));
|
||||||
|
return { error: false, message: 'Password reset successfully sent' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new user
|
||||||
|
* @param username The username of the user
|
||||||
|
* @param email The email address of the user
|
||||||
|
* @param password The password of the user
|
||||||
|
* @returns A message indicating that the user has been registered
|
||||||
|
*/
|
||||||
|
public async register(
|
||||||
|
username: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<{ error: boolean; message: string }> {
|
||||||
|
const validateUnique = await this.userService.validateUniqueUsernameAndEmail(username, email);
|
||||||
|
|
||||||
|
if (validateUnique.length !== 0) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
message: 'Username or email already exists',
|
||||||
|
fields: validateUnique,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userService.createUser(username, email, password);
|
||||||
|
await this.mailService.sendWelcomeEmail(email);
|
||||||
|
return { error: false, message: 'User registered' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a user's login credentials and return the session cookies
|
||||||
|
* @param username The username or email address of the user
|
||||||
|
* @param password The password of the user
|
||||||
|
* @returns The session cookies
|
||||||
|
*/
|
||||||
|
public async login(username: string, password: string): Promise<any> {
|
||||||
|
const user = await this.userService.authenticate(username, password);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.oidcService.createSession(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset code generation logic
|
||||||
|
* @returns Promise<string> A reset code
|
||||||
|
*/
|
||||||
|
private async generateResetCode(): Promise<string> {
|
||||||
|
let code = Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
|
// Check if the code already exists
|
||||||
|
if (await this.redisService.get(code)) {
|
||||||
|
code = await this.generateResetCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a password reset code in the Redis store
|
||||||
|
* @param code The password reset code
|
||||||
|
* @param userId The user ID associated with the code
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
private async storePasswordResetCode(code: string, userId: string | number): Promise<void> {
|
||||||
|
await this.redisService.set(getResetKey(code), userId, PASSWORD_RESET_EXPIRATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a password reset code from the Redis store
|
||||||
|
* @param code The password reset code
|
||||||
|
* @returns The user ID associated with the code
|
||||||
|
*/
|
||||||
|
private async getPasswordResetCode(code: string): Promise<number | null> {
|
||||||
|
return this.redisService.get<number>(getResetKey(code));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
export default async () => ({
|
export default async () => ({
|
||||||
base: {
|
base: {
|
||||||
localUrl: `${process.env.BASE_URL}`,
|
localUrl: `${process.env.BASE_URL}`,
|
||||||
|
fromEmail: `${process.env.FROM_EMAIL}`,
|
||||||
},
|
},
|
||||||
redis: {
|
redis: {
|
||||||
host: process.env['REDIS_HOST'] ?? 'localhost',
|
host: process.env['REDIS_HOST'] ?? 'localhost',
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export const MAX_STRING_LENGTH = 255;
|
export const MAX_STRING_LENGTH = 255;
|
||||||
export const FETCH_AFTER_CREATION_FAILED =
|
export const FETCH_AFTER_CREATION_FAILED = 'Failed to fetch entity after creation';
|
||||||
'Failed to fetch entity after creation';
|
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
ADMIN = 'admin',
|
ADMIN = 'admin',
|
||||||
|
|
|
@ -21,19 +21,19 @@ export class OidcClient {
|
||||||
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: true })
|
@Column({ type: 'varchar', length: MAX_STRING_LENGTH, nullable: true })
|
||||||
client_name: string;
|
client_name: string;
|
||||||
|
|
||||||
@Column({ type: 'simple-array', nullable: false, default: [] })
|
@Column({ type: 'simple-array', nullable: false })
|
||||||
redirect_uris: string[];
|
redirect_uris: string[];
|
||||||
|
|
||||||
@Column({ type: 'simple-array', nullable: true, default: [] })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
client_cors: string[];
|
client_cors: string[];
|
||||||
|
|
||||||
@Column({ type: 'simple-array', nullable: true, default: [] })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
allowed_introspection_targets: string[];
|
allowed_introspection_targets: string[];
|
||||||
|
|
||||||
@Column({ type: 'simple-array', nullable: false, default: [] })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
include_permissions_from_client: string[];
|
include_permissions_from_client: string[];
|
||||||
|
|
||||||
@Column({ type: 'simple-array', nullable: false, default: [] })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
post_logout_redirect_uris: string[];
|
post_logout_redirect_uris: string[];
|
||||||
|
|
||||||
@Column({ type: 'simple-array', nullable: true })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
import type {
|
import type { AdapterPayload, ClientAuthorizationState, UnknownObject } from 'oidc-provider';
|
||||||
AdapterPayload,
|
|
||||||
ClientAuthorizationState,
|
|
||||||
UnknownObject,
|
|
||||||
} from 'oidc-provider';
|
|
||||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import { convertFromNumberToTime, convertFromTimeToNumber } from '../../util/time.util';
|
||||||
convertFromNumberToTime,
|
|
||||||
convertFromTimeToNumber,
|
|
||||||
} from '../../util/time.util';
|
|
||||||
|
|
||||||
@Entity('oidc_session')
|
@Entity('oidc_session')
|
||||||
export class OidcSession implements AdapterPayload {
|
export class OidcSession implements AdapterPayload {
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
import {
|
||||||
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
import { MAX_STRING_LENGTH, UserRole } from '../database.const';
|
import { MAX_STRING_LENGTH, UserRole } from '../database.const';
|
||||||
import { ApiKey } from './api_keys.model';
|
import { ApiKey } from './api_keys.model';
|
||||||
|
|
||||||
|
@ -7,13 +16,13 @@ export class User {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ length: MAX_STRING_LENGTH })
|
@Column({ length: MAX_STRING_LENGTH, unique: true })
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
@Column({ length: MAX_STRING_LENGTH, name: 'display_name', nullable: true })
|
@Column({ length: MAX_STRING_LENGTH, name: 'display_name', nullable: true })
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
||||||
@Column({ length: MAX_STRING_LENGTH })
|
@Column({ length: MAX_STRING_LENGTH, unique: true })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@Column({ name: 'email_verified', default: false })
|
@Column({ name: 'email_verified', default: false })
|
||||||
|
@ -31,7 +40,7 @@ export class User {
|
||||||
@Column({ length: MAX_STRING_LENGTH, nullable: true })
|
@Column({ length: MAX_STRING_LENGTH, nullable: true })
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
|
||||||
@Column({ length: 15, type: String })
|
@Column({ length: 15, type: String, default: UserRole.USER })
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
|
|
||||||
@Column({ name: 'disabled', default: false })
|
@Column({ name: 'disabled', default: false })
|
||||||
|
@ -41,6 +50,13 @@ export class User {
|
||||||
@Column({ name: 'email_hash', length: MAX_STRING_LENGTH, nullable: true })
|
@Column({ name: 'email_hash', length: MAX_STRING_LENGTH, nullable: true })
|
||||||
emailHash: string;
|
emailHash: string;
|
||||||
|
|
||||||
|
// on update or creation of the email field, update the email hash sha256
|
||||||
|
@BeforeInsert()
|
||||||
|
@BeforeUpdate()
|
||||||
|
updateEmailHashOnUpdate() {
|
||||||
|
this.emailHash = createHash('sha256').update(this.email).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
// Relationship Mapping
|
// Relationship Mapping
|
||||||
|
|
||||||
@OneToMany(() => ApiKey, (apiKey) => apiKey.user)
|
@OneToMany(() => ApiKey, (apiKey) => apiKey.user)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { PostalClientModule } from 'nestjs-postal-client';
|
import { PostalClientModule } from 'nestjs-postal-client';
|
||||||
import postalConfig from './postal.config';
|
import postalConfig from './postal.config';
|
||||||
import { PostalConfigService } from './postal_config.service';
|
import { PostalConfigService } from './postal_config.service';
|
||||||
|
import { MailService } from './mail.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -13,7 +14,7 @@ import { PostalConfigService } from './postal_config.service';
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [ConfigService],
|
providers: [ConfigService, MailService],
|
||||||
exports: [PostalClientModule],
|
exports: [PostalClientModule, MailService],
|
||||||
})
|
})
|
||||||
export class MailModule {}
|
export class MailModule {}
|
||||||
|
|
46
src/mail/mail.service.ts
Normal file
46
src/mail/mail.service.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PostalClientService } from 'nestjs-postal-client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
logger = new Logger(MailService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly postalService: PostalClientService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a password reset email to the user
|
||||||
|
* @param email The email address of the user
|
||||||
|
*/
|
||||||
|
public async sendPasswordResetEmail(email: string, resetCode: string): Promise<void> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a welcome email to the user
|
||||||
|
* @param email The email address of the user
|
||||||
|
*/
|
||||||
|
public async sendWelcomeEmail(email: string): Promise<void> {
|
||||||
|
this.logger.debug(`Sending welcome email to ${email}`);
|
||||||
|
|
||||||
|
this.postalService.sendMessage({
|
||||||
|
to: [email],
|
||||||
|
subject: 'Welcome to the app!',
|
||||||
|
from: this.configService.getOrThrow('MAIL_FROM'),
|
||||||
|
plain_body: 'Welcome to the app!',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a verification email to the user
|
||||||
|
* @param email The email address of the user
|
||||||
|
*/
|
||||||
|
public async sendVerificationEmail(email: string): Promise<void> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a password changed email to the user
|
||||||
|
* @param email The email address of the user
|
||||||
|
*/
|
||||||
|
public async sendPasswordChangedEmail(email: string): Promise<void> {}
|
||||||
|
}
|
|
@ -2,11 +2,14 @@ import { NestFactory } from '@nestjs/core';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||||
AppModule,
|
|
||||||
);
|
app.useGlobalPipes(new ValidationPipe());
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
app.useStaticAssets(join(__dirname, '..', 'public'));
|
app.useStaticAssets(join(__dirname, '..', 'public'));
|
||||||
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||||
|
|
|
@ -68,7 +68,7 @@ export class RedisService implements OnApplicationShutdown {
|
||||||
* @param value Value to store
|
* @param value Value to store
|
||||||
* @param ttl Time to live in seconds
|
* @param ttl Time to live in seconds
|
||||||
*/
|
*/
|
||||||
public async set(key: string, value: string | object, ttl?: number): Promise<void> {
|
public async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||||
this.logger.debug(`Setting key ${key}`);
|
this.logger.debug(`Setting key ${key}`);
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
value = JSON.stringify(value);
|
value = JSON.stringify(value);
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
export const USER_NOT_FOUND_ERROR = 'User not found';
|
export const USER_NOT_FOUND_ERROR = 'User not found';
|
||||||
|
export const INVALID_CREDENTIALS_ERROR =
|
||||||
|
'The email or password you entered is incorrect or the user was not found';
|
||||||
|
export const DISABLED_USER_ERROR = 'User is disabled';
|
||||||
|
|
||||||
// Caching Constants for Redis
|
// Caching Constants for Redis
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import { hash, verify } from 'argon2';
|
||||||
|
|
||||||
import { User } from '../database/models/user.model';
|
import { User } from '../database/models/user.model';
|
||||||
import { RedisService } from '../redis/redis.service';
|
import { RedisService } from '../redis/redis.service';
|
||||||
import { Span } from 'nestjs-otel';
|
import { Span } from 'nestjs-otel';
|
||||||
import { USER_NOT_FOUND_ERROR, userCacheKey } from './user.constant';
|
import {
|
||||||
|
DISABLED_USER_ERROR,
|
||||||
|
INVALID_CREDENTIALS_ERROR,
|
||||||
|
USER_NOT_FOUND_ERROR,
|
||||||
|
userCacheKey,
|
||||||
|
} from './user.constant';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
|
@ -59,4 +65,105 @@ export class UserService {
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a user by their email address
|
||||||
|
* @param email The user's email address
|
||||||
|
* @returns Promise<User> The user
|
||||||
|
*/
|
||||||
|
@Span()
|
||||||
|
async getUserbyEmail(email: string): Promise<User> {
|
||||||
|
const user = await this.userRepository.findOne({ where: { email } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(USER_NOT_FOUND_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a username and email is unique
|
||||||
|
* @param username The username to validate
|
||||||
|
* @param email The email to validate
|
||||||
|
* @returns Fields[] The fields that are not unique
|
||||||
|
*/
|
||||||
|
@Span()
|
||||||
|
async validateUniqueUsernameAndEmail(username: string, email: string): Promise<string[]> {
|
||||||
|
const user = await this.userRepository.findOne({
|
||||||
|
select: { username: true, email: true },
|
||||||
|
where: [{ username }, { email }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fields: string[] = [];
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
if (user.username === username) {
|
||||||
|
fields.push('username');
|
||||||
|
}
|
||||||
|
if (user.email === email) {
|
||||||
|
fields.push('email');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user's password
|
||||||
|
* @param userId The user's ID
|
||||||
|
* @param password The new password
|
||||||
|
*/
|
||||||
|
@Span()
|
||||||
|
async updatePassword(userId: string | number, password: string): Promise<void> {
|
||||||
|
const hashedPassword = await hash(password);
|
||||||
|
await this.userRepository.update(userId, { password: hashedPassword });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new user
|
||||||
|
* @param email The user's email address
|
||||||
|
* @param username The user's username
|
||||||
|
* @param password The user's password
|
||||||
|
* @returns Promise<User> The newly created user
|
||||||
|
*/
|
||||||
|
@Span()
|
||||||
|
async createUser(username: string, email: string, password: string): Promise<User> {
|
||||||
|
const hashedPassword = await hash(password);
|
||||||
|
|
||||||
|
const user = this.userRepository.create({ email, username, password: hashedPassword });
|
||||||
|
|
||||||
|
return await this.userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate a user
|
||||||
|
* @param username The user's email address or username
|
||||||
|
* @param password The user's password
|
||||||
|
* @returns Promise<User> The authenticated user
|
||||||
|
*/
|
||||||
|
@Span()
|
||||||
|
async authenticate(username: string, password: string): Promise<User> {
|
||||||
|
// lowercase email
|
||||||
|
username = username.toLowerCase();
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOne({ where: [{ username }, { email: username }] });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException(INVALID_CREDENTIALS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await verify(user.password, password);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
// TODO: Implement ticking to prevent brute force attacks
|
||||||
|
throw new UnauthorizedException(INVALID_CREDENTIALS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.disabled) {
|
||||||
|
throw new UnauthorizedException(DISABLED_USER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,5 @@ export const convertFromNumberToTime = (time: number | null): Date | null => {
|
||||||
export const convertFromTimeToNumber = (time: Date | null): number | null => {
|
export const convertFromTimeToNumber = (time: Date | null): number | null => {
|
||||||
return time ? time.getTime() / 1000 : null;
|
return time ? time.getTime() / 1000 : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEpochTime = (date = Date.now()) => Math.floor(date / 1000);
|
||||||
|
|
|
@ -55,8 +55,8 @@
|
||||||
<p class="text-gray-400 mb-6 text-center">We're so excited to see you again!</p>
|
<p class="text-gray-400 mb-6 text-center">We're so excited to see you again!</p>
|
||||||
<form action="{{ login_url }}" method="POST" class="space-y-6">
|
<form action="{{ login_url }}" method="POST" class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label for="email" class="block text-sm font-medium text-gray-400">Email or Username</label>
|
<label for="username" class="block text-sm font-medium text-gray-400">Email or Username</label>
|
||||||
<input type="text" placeholder="yip@yap.yop" name="email" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
<input type="text" placeholder="yip@yap.yop" name="username" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
|
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
|
||||||
|
|
186
views/auth/register.hbs
Normal file
186
views/auth/register.hbs
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Registration Page</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.video-bg {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.video-bg, .overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.registration-prompt {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
position: static;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.shake {
|
||||||
|
animation: shake 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
color: #f87171; /* Tailwind class 'text-red-500' */
|
||||||
|
font-size: 0.875rem; /* Tailwind class 'text-sm' */
|
||||||
|
margin-top: 0.25rem; /* Tailwind class 'mt-1' */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="relative flex items-center justify-start min-h-screen">
|
||||||
|
<video autoplay muted loop class="video-bg">
|
||||||
|
<source src="/assets/login.webm" type="video/webm">
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
<div class="overlay"></div>
|
||||||
|
<div class="relative bg-gray-800 p-8 rounded-lg shadow-lg w-full max-w-md ml-16 registration-prompt">
|
||||||
|
<h2 class="text-2xl font-bold mb-2 text-white text-center">Create an Account</h2>
|
||||||
|
<p class="text-gray-400 mb-6 text-center">Join us by filling out the form below.</p>
|
||||||
|
<form id="registrationForm" action="/auth/register" method="POST" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-400">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span id="usernameError" class="error-message hidden"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-400">Email</label>
|
||||||
|
<input type="email" id="email" name="email" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span id="emailError" class="error-message hidden"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="verifyPassword" class="block text-sm font-medium text-gray-400">Verify Password</label>
|
||||||
|
<input type="password" id="verifyPassword" name="verifyPassword" required class="mt-1 block w-full px-3 py-2 bg-gray-700 text-white border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span id="passwordError" class="error-message hidden">Passwords do not match</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Register</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-4 text-sm text-center">
|
||||||
|
<span class="text-gray-400">Already have an account? </span><a href="/auth/login" class="font-medium text-indigo-500 hover:text-indigo-400">Log In</a>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex items-center justify-center text-gray-400 text-xs">
|
||||||
|
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 204.73 204.47">
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #fff;
|
||||||
|
stroke-width: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g id="svg">
|
||||||
|
<g id="svgg">
|
||||||
|
<path id="path0" class="cls-1" d="M87.69,1.22C25.89,11.33-13.11,71.52,4.06,130.27c2.91,9.95,2.81,9.92,5.07,1.07,1.11-4.32,2.34-8.7,2.74-9.73.5-1.27.5-3.66,0-7.51C1.85,36.65,88.31-16.64,153.44,26.86c67.68,45.2,44.99,151.23-35.36,165.24-10.87,1.9-10.31,2.09-18.71-6.25-8.16-8.1-8-8.17-4.75,2.22l1.64,5.26-4.77-.51c-7.24-.77-18.02-3.71-19.82-5.4-4.91-4.61-8.76-19.09-9.26-34.81-.18-5.6-.57-10.2-.87-10.23-3.89-.28-13.29,4.17-19.89,9.41-5.97,4.74-6.02,4.7-3.91-3.57,6.93-27.13,27.84-54.82,50.35-66.66l5.01-2.64-2.73-.51c-1.5-.28-5.67-.35-9.26-.14l-6.53.37,6.83-3.43c7.96-4,14.12-6.14,20.87-7.26,2.77-.46,4.82-1.16,4.82-1.64,0-3,19.12-26.83,21.52-26.83.35,0,.43,3.27.19,7.33-.55,9.09-.71,8.86,10.82,15.24,10.14,5.61,11.94,7.09,11.65,9.57-.35,3.02-2.34,2.57-4.44-1-2.17-3.7-4.25-4.99-9.58-5.96-4.45-.81-4.68-.56-3.68,3.9,1.24,5.55,4.55,7.99,13.36,9.85,10.08,2.14,26.65,9.16,26.14,11.07-.19.73.21,2.18.9,3.22,1.2,1.82,1.19,1.98-.18,3.83-4.8,6.48-11.34,7.82-18.59,3.82-15.61-8.62-25.89-12.19-36.77-12.79-10.3-.56-18.56,1.78-10.29,2.91,3.9.54,7.97,2.52,10.33,5.05l1.73,1.86h-3.52c-23.99.1-42.29,20.92-37.02,42.13,1.83,7.37,6.88,16.46,13.12,23.61,2.46,2.82,2.6,2.69,1.05-1.03-4.13-9.89-2.1-27.31,4-34.25,4.43-5.04,4.84-4.83,4.44,2.3-1.22,21.5,13.44,35.47,40.58,38.67,4.98.59,5.26.26,1.59-1.84-14.85-8.47-26.07-27.76-23.68-40.69,2.92-15.8,12.63-21.34,34.29-19.56l10.56.87,3.15-1.84c7.55-4.42,17.35-15.45,15.44-17.36-.58-.58-6.99-3.62-14.23-6.75l-13.18-5.69-.36-4.47c-.44-5.58-.41-5.53-6.84-9.87-4.85-3.27-5.31-3.79-5.31-6,0-6.16-3.48-18.85-4.93-17.96-.37.23-.9,3.95-1.18,8.27-.52,8.03-1.12,9.27-3.25,6.71-.68-.82-.96-3.68-.98-9.93-.03-9.38-.86-11.79-4.09-11.79-5.02,0-19.77,11.77-29.26,23.35-1.53,1.87-3.02,2.57-8.57,4.05-21.98,5.85-51.41,23.66-59.59,36.06-2.69,4.07-2.28,4.17,3.95.94,12.69-6.58,26.23-10.86,19.26-6.1-20.43,13.98-33.21,37.15-37.46,67.92-1.03,7.47,22.04,28.83,40.44,37.46,76.3,35.75,160.95-30.76,143.93-113.09C191.73,29.3,139.44-7.25,87.69,1.22"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Powered by Waterwolf
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('registrationForm').addEventListener('submit', async function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const usernameInput = document.getElementById('username');
|
||||||
|
const emailInput = document.getElementById('email');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const verifyPasswordInput = document.getElementById('verifyPassword');
|
||||||
|
const usernameError = document.getElementById('usernameError');
|
||||||
|
const emailError = document.getElementById('emailError');
|
||||||
|
const passwordError = document.getElementById('passwordError');
|
||||||
|
|
||||||
|
// Clear previous error messages
|
||||||
|
usernameError.classList.add('hidden');
|
||||||
|
emailError.classList.add('hidden');
|
||||||
|
passwordError.classList.add('hidden');
|
||||||
|
usernameInput.classList.remove('border-red-500');
|
||||||
|
emailInput.classList.remove('border-red-500');
|
||||||
|
|
||||||
|
const username = usernameInput.value;
|
||||||
|
const email = emailInput.value;
|
||||||
|
const password = passwordInput.value;
|
||||||
|
const verifyPassword = verifyPasswordInput.value;
|
||||||
|
|
||||||
|
if (password !== verifyPassword) {
|
||||||
|
passwordError.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const responseData = await response.json();
|
||||||
|
if (response.status === 400 && responseData.message === 'Username or email already exists') {
|
||||||
|
if (responseData.fields.includes('username')) {
|
||||||
|
usernameInput.classList.add('border-red-500', 'shake');
|
||||||
|
usernameError.textContent = 'Username already exists';
|
||||||
|
usernameError.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
usernameInput.classList.remove('border-red-500');
|
||||||
|
usernameError.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (responseData.fields.includes('email')) {
|
||||||
|
emailInput.classList.add('border-red-500', 'shake');
|
||||||
|
emailError.textContent = 'Email already exists';
|
||||||
|
emailError.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
emailInput.classList.remove('border-red-500');
|
||||||
|
emailError.classList.add('hidden');
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
usernameInput.classList.remove('shake');
|
||||||
|
emailInput.classList.remove('shake');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.location.href = '/auth/login?message=Registration+Successful';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue