Compare commits
10 Commits
7ef46af4f3
...
5fa704cd96
Author | SHA1 | Date |
---|---|---|
Igor Hrenowitsch Propisnov | 5fa704cd96 | |
Igor Hrenowitsch Propisnov | d96572f975 | |
Igor Hrenowitsch Propisnov | 876f0b42b8 | |
Igor Hrenowitsch Propisnov | d8f65f1241 | |
Igor Hrenowitsch Propisnov | fcd147bca4 | |
Igor Hrenowitsch Propisnov | 2a53e946ea | |
Igor Hrenowitsch Propisnov | bbe444ea5f | |
Igor Hrenowitsch Propisnov | b31e5029cc | |
Igor Hrenowitsch Propisnov | 551b2e9659 | |
Igor Hrenowitsch Propisnov | 9993c63a56 |
|
@ -36,9 +36,13 @@
|
||||||
"argon2": "^0.40.1",
|
"argon2": "^0.40.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"connect-typeorm": "^2.0.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
|
"install": "^0.13.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
@ -53,8 +57,10 @@
|
||||||
"@types/argon2": "^0.15.0",
|
"@types/argon2": "^0.15.0",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/express-session": "^1.18.0",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
|
|
|
@ -44,15 +44,27 @@ dependencies:
|
||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.14.1
|
specifier: ^0.14.1
|
||||||
version: 0.14.1
|
version: 0.14.1
|
||||||
|
connect-typeorm:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0(typeorm@0.3.20)
|
||||||
cookie-parser:
|
cookie-parser:
|
||||||
specifier: ^1.4.6
|
specifier: ^1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
|
express-session:
|
||||||
|
specifier: ^1.18.0
|
||||||
|
version: 1.18.0
|
||||||
|
install:
|
||||||
|
specifier: ^0.13.0
|
||||||
|
version: 0.13.0
|
||||||
passport:
|
passport:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
passport-jwt:
|
passport-jwt:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
|
passport-local:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.11.5
|
specifier: ^8.11.5
|
||||||
version: 8.11.5
|
version: 8.11.5
|
||||||
|
@ -91,12 +103,18 @@ devDependencies:
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^4.17.17
|
specifier: ^4.17.17
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
'@types/express-session':
|
||||||
|
specifier: ^1.18.0
|
||||||
|
version: 1.18.0
|
||||||
'@types/jest':
|
'@types/jest':
|
||||||
specifier: ^29.5.2
|
specifier: ^29.5.2
|
||||||
version: 29.5.12
|
version: 29.5.12
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.3.1
|
specifier: ^20.3.1
|
||||||
version: 20.12.4
|
version: 20.12.4
|
||||||
|
'@types/passport-local':
|
||||||
|
specifier: ^1.0.38
|
||||||
|
version: 1.0.38
|
||||||
'@types/supertest':
|
'@types/supertest':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
|
@ -1304,13 +1322,11 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 20.12.4
|
'@types/node': 20.12.4
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/connect@3.4.38:
|
/@types/connect@3.4.38:
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.12.4
|
'@types/node': 20.12.4
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/cookie-parser@1.4.7:
|
/@types/cookie-parser@1.4.7:
|
||||||
resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==}
|
resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==}
|
||||||
|
@ -1322,6 +1338,10 @@ packages:
|
||||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/debug@0.0.31:
|
||||||
|
resolution: {integrity: sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@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==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1347,7 +1367,11 @@ packages:
|
||||||
'@types/qs': 6.9.14
|
'@types/qs': 6.9.14
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
dev: true
|
|
||||||
|
/@types/express-session@1.18.0:
|
||||||
|
resolution: {integrity: sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
|
||||||
/@types/express@4.17.21:
|
/@types/express@4.17.21:
|
||||||
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
|
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
|
||||||
|
@ -1356,7 +1380,6 @@ packages:
|
||||||
'@types/express-serve-static-core': 4.17.43
|
'@types/express-serve-static-core': 4.17.43
|
||||||
'@types/qs': 6.9.14
|
'@types/qs': 6.9.14
|
||||||
'@types/serve-static': 1.15.7
|
'@types/serve-static': 1.15.7
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/graceful-fs@4.1.9:
|
/@types/graceful-fs@4.1.9:
|
||||||
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
|
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
|
||||||
|
@ -1366,7 +1389,6 @@ packages:
|
||||||
|
|
||||||
/@types/http-errors@2.0.4:
|
/@types/http-errors@2.0.4:
|
||||||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/istanbul-lib-coverage@2.0.6:
|
/@types/istanbul-lib-coverage@2.0.6:
|
||||||
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
|
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
|
||||||
|
@ -1411,27 +1433,44 @@ packages:
|
||||||
|
|
||||||
/@types/mime@1.3.5:
|
/@types/mime@1.3.5:
|
||||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/node@20.12.4:
|
/@types/node@20.12.4:
|
||||||
resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==}
|
resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
|
|
||||||
|
/@types/passport-local@1.0.38:
|
||||||
|
resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
'@types/passport': 1.0.16
|
||||||
|
'@types/passport-strategy': 0.2.38
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/passport-strategy@0.2.38:
|
||||||
|
resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
'@types/passport': 1.0.16
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/passport@1.0.16:
|
||||||
|
resolution: {integrity: sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==}
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.21
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/qs@6.9.14:
|
/@types/qs@6.9.14:
|
||||||
resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==}
|
resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/range-parser@1.2.7:
|
/@types/range-parser@1.2.7:
|
||||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/send@0.17.4:
|
/@types/send@0.17.4:
|
||||||
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime': 1.3.5
|
'@types/mime': 1.3.5
|
||||||
'@types/node': 20.12.4
|
'@types/node': 20.12.4
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/serve-static@1.15.7:
|
/@types/serve-static@1.15.7:
|
||||||
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
||||||
|
@ -1439,7 +1478,6 @@ packages:
|
||||||
'@types/http-errors': 2.0.4
|
'@types/http-errors': 2.0.4
|
||||||
'@types/node': 20.12.4
|
'@types/node': 20.12.4
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/stack-utils@2.0.3:
|
/@types/stack-utils@2.0.3:
|
||||||
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
||||||
|
@ -2254,6 +2292,20 @@ packages:
|
||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
typedarray: 0.0.6
|
typedarray: 0.0.6
|
||||||
|
|
||||||
|
/connect-typeorm@2.0.0(typeorm@0.3.20):
|
||||||
|
resolution: {integrity: sha512-0OcbHJkNMTJjSrbcKGljr4PKgRq13Dds7zQq3+8oaf4syQTgGvGv9OgnXo2qg+Bljkh4aJNzIvW74QOVLn8zrw==}
|
||||||
|
peerDependencies:
|
||||||
|
typeorm: ^0.3.0
|
||||||
|
dependencies:
|
||||||
|
'@types/debug': 0.0.31
|
||||||
|
'@types/express-session': 1.18.0
|
||||||
|
debug: 4.3.4
|
||||||
|
express-session: 1.18.0
|
||||||
|
typeorm: 0.3.20(pg@8.11.5)(ts-node@10.9.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/consola@2.15.3:
|
/consola@2.15.3:
|
||||||
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
|
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
|
||||||
|
|
||||||
|
@ -2282,6 +2334,10 @@ packages:
|
||||||
/cookie-signature@1.0.6:
|
/cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
|
/cookie-signature@1.0.7:
|
||||||
|
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cookie@0.4.1:
|
/cookie@0.4.1:
|
||||||
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -2709,6 +2765,22 @@ packages:
|
||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/express-session@1.18.0:
|
||||||
|
resolution: {integrity: sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
dependencies:
|
||||||
|
cookie: 0.6.0
|
||||||
|
cookie-signature: 1.0.7
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
on-headers: 1.0.2
|
||||||
|
parseurl: 1.3.3
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
uid-safe: 2.1.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/express@4.19.2:
|
/express@4.19.2:
|
||||||
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
|
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
@ -3224,6 +3296,11 @@ packages:
|
||||||
wrap-ansi: 6.2.0
|
wrap-ansi: 6.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/install@0.13.0:
|
||||||
|
resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/interpret@1.4.0:
|
/interpret@1.4.0:
|
||||||
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
|
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
@ -4259,6 +4336,11 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
|
|
||||||
|
/on-headers@1.0.2:
|
||||||
|
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/once@1.4.0:
|
/once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4379,6 +4461,13 @@ packages:
|
||||||
passport-strategy: 1.0.0
|
passport-strategy: 1.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/passport-local@1.0.0:
|
||||||
|
resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
dependencies:
|
||||||
|
passport-strategy: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/passport-strategy@1.0.0:
|
/passport-strategy@1.0.0:
|
||||||
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
|
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
@ -4616,6 +4705,11 @@ packages:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/random-bytes@1.0.0:
|
||||||
|
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/randombytes@2.1.0:
|
/randombytes@2.1.0:
|
||||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5457,6 +5551,13 @@ packages:
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/uid-safe@2.1.5:
|
||||||
|
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
random-bytes: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/uid@2.0.2:
|
/uid@2.0.2:
|
||||||
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
|
@ -10,9 +10,9 @@ import { CspMiddleware } from './middleware/csp-middleware/csp.middleware';
|
||||||
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
|
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
|
||||||
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
|
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
|
||||||
import { AuthModule } from './modules/auth-module/auth.module';
|
import { AuthModule } from './modules/auth-module/auth.module';
|
||||||
import { AccessTokenGuard } from './modules/auth-module/common/guards';
|
|
||||||
import { DatabaseModule } from './modules/database-module/database.module';
|
import { DatabaseModule } from './modules/database-module/database.module';
|
||||||
import { SendgridModule } from './modules/sendgrid-module/sendgrid.module';
|
import { SendgridModule } from './modules/sendgrid-module/sendgrid.module';
|
||||||
|
import { SessionModule } from './modules/session/session.module';
|
||||||
import { UserModule } from './modules/user-module/user.module';
|
import { UserModule } from './modules/user-module/user.module';
|
||||||
import { VerifyModule } from './modules/verify-module/verify.module';
|
import { VerifyModule } from './modules/verify-module/verify.module';
|
||||||
|
|
||||||
|
@ -27,13 +27,10 @@ import { VerifyModule } from './modules/verify-module/verify.module';
|
||||||
UserModule,
|
UserModule,
|
||||||
SendgridModule,
|
SendgridModule,
|
||||||
VerifyModule,
|
VerifyModule,
|
||||||
|
SessionModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [AppService, ClearExpiredSessionsCron],
|
||||||
AppService,
|
|
||||||
{ provide: 'APP_GUARD', useClass: AccessTokenGuard },
|
|
||||||
ClearExpiredSessionsCron,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
public configure(consumer: MiddlewareConsumer): void {
|
public configure(consumer: MiddlewareConsumer): void {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { SessionService } from 'src/modules/auth-module/services/session.service';
|
import { SessionService } from 'src/modules/session/services/session.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClearExpiredSessionsCron {
|
export class ClearExpiredSessionsCron {
|
||||||
|
@ -8,12 +8,13 @@ export class ClearExpiredSessionsCron {
|
||||||
|
|
||||||
public constructor(private readonly sessionService: SessionService) {}
|
public constructor(private readonly sessionService: SessionService) {}
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, {
|
@Cron(CronExpression.EVERY_12_HOURS, {
|
||||||
name: 'Clear-Expired-Sessions',
|
name: 'Clear-Expired-Sessions',
|
||||||
timeZone: 'Europe/Berlin',
|
timeZone: 'Europe/Berlin',
|
||||||
})
|
})
|
||||||
public handleCron(): void {
|
public handleCron(): void {
|
||||||
this.logger.log('Cronjob Executed: Clear-Expired-Sessions');
|
this.logger.log('-Cronjob Executed: Delete-Expired-Sessions-');
|
||||||
this.sessionService.clearExpiredSessions();
|
this.sessionService.deleteAllExpiredSessions();
|
||||||
|
this.logger.log('-------------------------------------------');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,29 @@
|
||||||
|
import { ISession } from 'connect-typeorm';
|
||||||
import {
|
import {
|
||||||
Entity,
|
Entity,
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
Column,
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
BaseEntity,
|
||||||
|
Index,
|
||||||
|
DeleteDateColumn,
|
||||||
|
PrimaryColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { UserCredentials } from './user-credentials.entity';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Session {
|
export class Session extends BaseEntity implements ISession {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryColumn('varchar', { length: 255 })
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Column()
|
@Index()
|
||||||
public sessionId: string;
|
@Column('bigint')
|
||||||
|
public expiredAt: number;
|
||||||
|
|
||||||
@Column({ type: 'timestamp' })
|
@Column({ type: 'text' })
|
||||||
public expiresAt: Date;
|
public json: string;
|
||||||
|
|
||||||
@Column({})
|
@DeleteDateColumn()
|
||||||
public userAgent: string;
|
public destroyedAt?: Date;
|
||||||
|
|
||||||
@ManyToOne(() => UserCredentials, (userCredentials) => userCredentials.id, {
|
|
||||||
nullable: false,
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'userCredentialsId' })
|
|
||||||
public userCredentials: UserCredentials['id'];
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
|
@ -15,10 +15,7 @@ export class UserCredentials {
|
||||||
public email: string;
|
public email: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
public hash: string;
|
public hashedPassword: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
public refreshToken?: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
|
@ -18,9 +18,9 @@ export class UserData {
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
public isEmailConfirmed: boolean;
|
public isEmailConfirmed: boolean;
|
||||||
|
|
||||||
@OneToOne(() => UserCredentials)
|
@OneToOne(() => UserCredentials, { eager: true }) // eager: true lädt UserCredentials automatisch, wenn Sie UserData laden
|
||||||
@JoinColumn({ name: 'userCredentialsId' })
|
@JoinColumn() // Diese Dekoration sagt TypeORM, welche Spalte der Fremdschlüssel ist
|
||||||
public user: UserCredentials;
|
public userCredentials: UserCredentials;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { SessionInitService } from './modules/session/services';
|
||||||
|
|
||||||
async function setupSwagger(app: INestApplication): Promise<void> {
|
async function setupSwagger(app: INestApplication): Promise<void> {
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
|
@ -31,6 +32,12 @@ async function setupSwagger(app: INestApplication): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupSessions(app: INestApplication): Promise<void> {
|
||||||
|
const sessionService = app.get(SessionInitService);
|
||||||
|
|
||||||
|
app.use(sessionService.initSession());
|
||||||
|
}
|
||||||
|
|
||||||
async function setupCookieParser(app: INestApplication): Promise<void> {
|
async function setupCookieParser(app: INestApplication): Promise<void> {
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
}
|
}
|
||||||
|
@ -50,6 +57,7 @@ async function bootstrap(): Promise<void> {
|
||||||
await setupSwagger(app);
|
await setupSwagger(app);
|
||||||
await setupPrefix(app);
|
await setupPrefix(app);
|
||||||
await setupClassValidator(app);
|
await setupClassValidator(app);
|
||||||
|
await setupSessions(app);
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
|
@ -14,6 +14,7 @@ export class CorsMiddleware implements NestMiddleware {
|
||||||
const requestOrigin = req.headers.origin;
|
const requestOrigin = req.headers.origin;
|
||||||
|
|
||||||
if (!requestOrigin || allowedOrigins.includes(requestOrigin)) {
|
if (!requestOrigin || allowedOrigins.includes(requestOrigin)) {
|
||||||
|
res.header('Access-Control-Allow-Credentials', 'true');
|
||||||
res.header('Access-Control-Allow-Origin', requestOrigin || '*');
|
res.header('Access-Control-Allow-Origin', requestOrigin || '*');
|
||||||
res.header(
|
res.header(
|
||||||
'Access-Control-Allow-Methods',
|
'Access-Control-Allow-Methods',
|
||||||
|
|
|
@ -12,6 +12,7 @@ export class CspMiddleware implements NestMiddleware {
|
||||||
if (cspDirectives) {
|
if (cspDirectives) {
|
||||||
res.setHeader('Content-Security-Policy', cspDirectives);
|
res.setHeader('Content-Security-Policy', cspDirectives);
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,31 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Session, UserCredentials } from 'src/entities';
|
import { UserCredentials } from 'src/entities';
|
||||||
|
|
||||||
import { SendgridModule } from '../sendgrid-module/sendgrid.module';
|
import { SendgridModule } from '../sendgrid-module/sendgrid.module';
|
||||||
|
import { SessionModule } from '../session/session.module';
|
||||||
import { UserModule } from '../user-module/user.module';
|
import { UserModule } from '../user-module/user.module';
|
||||||
import { VerifyModule } from '../verify-module/verify.module';
|
import { VerifyModule } from '../verify-module/verify.module';
|
||||||
|
|
||||||
import { AuthController } from './controller/auth.controller';
|
import { AuthController } from './controller/auth.controller';
|
||||||
import { SessionRepository } from './repositories/session.repository';
|
|
||||||
import { UserCredentialsRepository } from './repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from './repositories/user-credentials.repository';
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { SessionService } from './services/session.service';
|
import { LocalStrategy } from './strategies/local.strategy';
|
||||||
import { TokenManagementService } from './services/token-management.service';
|
|
||||||
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
JwtModule.register({}),
|
||||||
|
TypeOrmModule.forFeature([UserCredentials]),
|
||||||
|
PassportModule,
|
||||||
|
SessionModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
SendgridModule,
|
SendgridModule,
|
||||||
VerifyModule,
|
VerifyModule,
|
||||||
JwtModule.register({}),
|
|
||||||
TypeOrmModule.forFeature([UserCredentials, Session]),
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
AuthService,
|
|
||||||
SessionService,
|
|
||||||
TokenManagementService,
|
|
||||||
UserCredentialsRepository,
|
|
||||||
SessionRepository,
|
|
||||||
AccessTokenStrategy,
|
|
||||||
RefreshTokenStrategy,
|
|
||||||
],
|
],
|
||||||
|
providers: [AuthService, UserCredentialsRepository, LocalStrategy],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [SessionService],
|
exports: [],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
||||||
import { JwtPayload } from 'src/modules/auth-module/models/types';
|
|
||||||
|
|
||||||
export const GetCurrentUserId = createParamDecorator(
|
|
||||||
(_: undefined, context: ExecutionContext): number => {
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
const user = request.user as JwtPayload;
|
|
||||||
|
|
||||||
return user.sub;
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -1,13 +0,0 @@
|
||||||
//import { JwtPayloadWithRefreshToken } from 'src/modules/auth-module/models/types';
|
|
||||||
|
|
||||||
// export const GetCurrentUser = createParamDecorator(
|
|
||||||
// (
|
|
||||||
// data: keyof JwtPayloadWithRefreshToken | undefined,
|
|
||||||
// context: ExecutionContext
|
|
||||||
// ) => {
|
|
||||||
// const request = context.switchToHttp().getRequest();
|
|
||||||
|
|
||||||
// if (!data) return request.user;
|
|
||||||
// return request.user[data];
|
|
||||||
// }
|
|
||||||
// );
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './get-user-id.decorator';
|
|
||||||
// export * from './get-user.decorator';
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AccessTokenGuard extends AuthGuard('jwt-access-token') {
|
|
||||||
public constructor(private readonly reflector: Reflector) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public canActivate(
|
|
||||||
context: ExecutionContext
|
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
|
||||||
// Check if the current route is marked as public
|
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
|
||||||
context.getHandler(),
|
|
||||||
context.getClass(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Allow access if the route is public, otherwise defer to the standard JWT authentication mechanism
|
|
||||||
if (isPublic) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './access-token.guard';
|
|
||||||
export * from './refresh-token.guard';
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
|
|
||||||
export class RefreshTokenGuard extends AuthGuard('jwt-refresh-token') {
|
|
||||||
public constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +1,21 @@
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
|
Get,
|
||||||
Body,
|
Body,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Res,
|
|
||||||
Req,
|
Req,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response, Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import { SessionGuard } from 'src/modules/session/guard';
|
||||||
|
import { SuccessDto } from 'src/shared';
|
||||||
import { Public } from 'src/shared/decorator';
|
import { Public } from 'src/shared/decorator';
|
||||||
|
|
||||||
import { GetCurrentUserId } from '../common/decorators';
|
import { LocalAuthGuard } from '../guard';
|
||||||
import {
|
import { SigninResponseDto, UserCredentialsDto } from '../models/dto';
|
||||||
AccessTokenDto,
|
|
||||||
LoginResponseDto,
|
|
||||||
UserCredentialsDto,
|
|
||||||
} from '../models/dto';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
@ApiTags('Authentication')
|
@ApiTags('Authentication')
|
||||||
|
@ -26,50 +25,53 @@ export class AuthController {
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: 'User signed up successfully',
|
description: 'User signed up successfully',
|
||||||
type: LoginResponseDto,
|
type: SuccessDto,
|
||||||
})
|
})
|
||||||
@Public()
|
|
||||||
@Post('signup')
|
@Post('signup')
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@Public()
|
||||||
public async signup(
|
public async signup(
|
||||||
@Body() userCredentials: UserCredentialsDto
|
@Body() userCredentials: UserCredentialsDto
|
||||||
): Promise<LoginResponseDto> {
|
): Promise<SuccessDto> {
|
||||||
return this.authService.signup(userCredentials);
|
return this.authService.signup(userCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: 'User signin successfully',
|
description: 'User signin successfully',
|
||||||
type: LoginResponseDto,
|
type: SigninResponseDto,
|
||||||
})
|
})
|
||||||
|
@ApiBody({ type: UserCredentialsDto })
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@UseGuards(LocalAuthGuard)
|
||||||
@Public()
|
@Public()
|
||||||
@Post('signin')
|
@Post('signin')
|
||||||
public async signin(
|
public async signin(@Req() request: Request): Promise<SigninResponseDto> {
|
||||||
@Res({ passthrough: true }) response: Response,
|
return this.authService.getLoginResponse(
|
||||||
@Req() request: Request,
|
request.user as SigninResponseDto & { userAgent: string }
|
||||||
@Body() userCredentials: UserCredentialsDto
|
);
|
||||||
): Promise<LoginResponseDto> {
|
|
||||||
return await this.authService.signin(userCredentials, response, request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: 'User tokens refreshed successfully',
|
description: 'User signed out',
|
||||||
type: AccessTokenDto,
|
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Public()
|
@UseGuards(SessionGuard)
|
||||||
@Post('refresh')
|
@Post('signout')
|
||||||
public async refreshToken(@Req() request: Request): Promise<AccessTokenDto> {
|
public async signout(@Req() request: Request): Promise<SuccessDto> {
|
||||||
return await this.authService.refresh(request);
|
return this.authService.signout(request.sessionID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: 'User signed out successfully',
|
description: 'Check if user is authenticated',
|
||||||
type: Boolean,
|
type: SuccessDto,
|
||||||
})
|
})
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('logout')
|
@UseGuards(SessionGuard)
|
||||||
public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
|
@Get('status')
|
||||||
return this.authService.logout(userId);
|
public status(@Req() request: Request): Promise<SuccessDto> {
|
||||||
|
return this.authService.checkAuthStatus(
|
||||||
|
request.sessionID,
|
||||||
|
request.headers['user-agent']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './local.auth.guard';
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { SessionService } from 'src/modules/session/services/session.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalAuthGuard extends AuthGuard('local') {
|
||||||
|
public constructor(private readonly sessionService: SessionService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const result = (await super.canActivate(context)) as boolean;
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
await super.logIn(request);
|
||||||
|
await this.sessionService.enforceSessionLimit(request.user.id);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class AccessTokenDto {
|
|
||||||
@ApiProperty({
|
|
||||||
title: 'Access token',
|
|
||||||
description: 'Access token',
|
|
||||||
example: 'eyJhbGci',
|
|
||||||
})
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
public access_token: string;
|
|
||||||
}
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * from './user-credentials.dto';
|
export * from './user-credentials.dto';
|
||||||
export * from './login-response.dto';
|
export * from './signin-response.dto';
|
||||||
export * from './access-token.dto';
|
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class LoginResponseDto {
|
export class SigninResponseDto {
|
||||||
@ApiProperty({
|
|
||||||
title: 'Access token',
|
|
||||||
description: 'Access token',
|
|
||||||
example: 'eyJhbGci',
|
|
||||||
})
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
public access_token: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
title: 'Email',
|
title: 'Email',
|
||||||
description: 'User Email',
|
description: 'User Email',
|
||||||
|
@ -28,5 +19,5 @@ export class LoginResponseDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
public userId: string;
|
public id: string;
|
||||||
}
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './jwt-payload.type';
|
|
||||||
// export * from './jwt-payload-with-refresh-token.type';
|
|
||||||
export * from './token-payload.type';
|
|
||||||
export * from './tokens.type';
|
|
|
@ -1,3 +0,0 @@
|
||||||
// import { JwtPayload } from './jwt-payload.type';
|
|
||||||
|
|
||||||
// export type JwtPayloadWithRefreshToken = JwtPayload & { refresh_token: string };
|
|
|
@ -1,4 +0,0 @@
|
||||||
export type JwtPayload = {
|
|
||||||
email: string;
|
|
||||||
sub: number;
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
export type TokenPayload = {
|
|
||||||
sub: string;
|
|
||||||
email: string;
|
|
||||||
iat: number;
|
|
||||||
exp: number;
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export type Tokens = {
|
|
||||||
access_token: string;
|
|
||||||
refresh_token: string;
|
|
||||||
};
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Response } from 'express';
|
|
||||||
import { Session } from 'src/entities';
|
|
||||||
import { DeleteResult, Repository } from 'typeorm';
|
|
||||||
import { LessThan } from 'typeorm';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SessionRepository {
|
|
||||||
public constructor(
|
|
||||||
@InjectRepository(Session) private sessionRepository: Repository<Session>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public createSession(userId: string, userAgent: string): Promise<Session> {
|
|
||||||
const sessionId = uuidv4();
|
|
||||||
const expirationDate = new Date();
|
|
||||||
|
|
||||||
expirationDate.setHours(expirationDate.getHours() + 1);
|
|
||||||
|
|
||||||
const session = this.sessionRepository.create({
|
|
||||||
userCredentials: userId,
|
|
||||||
sessionId,
|
|
||||||
expiresAt: expirationDate,
|
|
||||||
userAgent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.sessionRepository.save(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public findSessionBySessionId(sessionId: string): Promise<Session> {
|
|
||||||
return this.sessionRepository.findOne({
|
|
||||||
where: { sessionId },
|
|
||||||
relations: ['userCredentials'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public attachSessionToResponse(response: Response, sessionId: string): void {
|
|
||||||
response.cookie('session_id', sessionId, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public validateSessionUserAgent(
|
|
||||||
sessionId: string,
|
|
||||||
currentUserAgent: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
return this.sessionRepository
|
|
||||||
.findOne({
|
|
||||||
where: { sessionId },
|
|
||||||
select: ['userAgent'],
|
|
||||||
})
|
|
||||||
.then((session) =>
|
|
||||||
session ? session.userAgent === currentUserAgent : false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkSessionLimit(userId: string): Promise<DeleteResult> {
|
|
||||||
return this.sessionRepository
|
|
||||||
.createQueryBuilder('session')
|
|
||||||
.leftJoinAndSelect('session.userCredentials', 'userCredentials')
|
|
||||||
.where('userCredentials.id = :userId', { userId })
|
|
||||||
.orderBy('session.expiresAt', 'ASC')
|
|
||||||
.getMany()
|
|
||||||
.then((userSessions) => {
|
|
||||||
if (userSessions.length >= 5) {
|
|
||||||
return this.sessionRepository.delete(userSessions[0].id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public invalidateAllSessionsForUser(userId: string): Promise<DeleteResult> {
|
|
||||||
return this.sessionRepository.delete({ userCredentials: userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
public extendSessionExpiration(sessionId: string): Promise<Session> {
|
|
||||||
return this.sessionRepository
|
|
||||||
.findOne({ where: { sessionId } })
|
|
||||||
.then((session) => {
|
|
||||||
if (session) {
|
|
||||||
session.expiresAt = new Date(
|
|
||||||
session.expiresAt.setMinutes(session.expiresAt.getMinutes() + 30)
|
|
||||||
);
|
|
||||||
return this.sessionRepository.save(session);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearExpiredSessions(): Promise<DeleteResult> {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
return this.sessionRepository.delete({ expiresAt: LessThan(now) });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,9 +12,9 @@ export class UserCredentialsRepository {
|
||||||
|
|
||||||
public async createUser(
|
public async createUser(
|
||||||
email: string,
|
email: string,
|
||||||
hash: string
|
hashedPassword: string
|
||||||
): Promise<UserCredentials> {
|
): Promise<UserCredentials> {
|
||||||
const user = this.repository.create({ email, hash });
|
const user = this.repository.create({ email, hashedPassword });
|
||||||
|
|
||||||
return this.repository.save(user);
|
return this.repository.save(user);
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,4 @@ export class UserCredentialsRepository {
|
||||||
): Promise<UserCredentials | undefined> {
|
): Promise<UserCredentials | undefined> {
|
||||||
return this.repository.findOne({ where: { id: userId } });
|
return this.repository.findOne({ where: { id: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateUserRefreshToken(
|
|
||||||
userId: string,
|
|
||||||
refreshToken: string | null
|
|
||||||
): Promise<number> {
|
|
||||||
const result = await this.repository.update(userId, { refreshToken });
|
|
||||||
|
|
||||||
return result.affected ?? 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,25 @@
|
||||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
import {
|
||||||
import { Response, Request } from 'express';
|
ConflictException,
|
||||||
import { Session } from 'src/entities';
|
ForbiddenException,
|
||||||
import { EncryptionService } from 'src/shared';
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UserCredentials } from 'src/entities';
|
||||||
|
import { SessionService } from 'src/modules/session/services/session.service';
|
||||||
|
import { EncryptionService, SuccessDto } from 'src/shared';
|
||||||
|
|
||||||
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
||||||
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
||||||
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
||||||
import {
|
import { SigninResponseDto, UserCredentialsDto } from '../models/dto';
|
||||||
AccessTokenDto,
|
|
||||||
LoginResponseDto,
|
|
||||||
UserCredentialsDto,
|
|
||||||
} from '../models/dto';
|
|
||||||
import { TokenPayload } from '../models/types';
|
|
||||||
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
||||||
|
|
||||||
import { SessionService } from './session.service';
|
|
||||||
import { TokenManagementService } from './token-management.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly userCredentialsRepository: UserCredentialsRepository,
|
private readonly userCredentialsRepository: UserCredentialsRepository,
|
||||||
private readonly userDataRepository: UserDataRepository,
|
private readonly userDataRepository: UserDataRepository,
|
||||||
private readonly tokenManagementService: TokenManagementService,
|
|
||||||
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
|
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
|
||||||
private readonly emailVerificationService: EmailVerificationService,
|
private readonly emailVerificationService: EmailVerificationService,
|
||||||
private readonly sessionService: SessionService
|
private readonly sessionService: SessionService
|
||||||
|
@ -30,151 +27,134 @@ export class AuthService {
|
||||||
|
|
||||||
public async signup(
|
public async signup(
|
||||||
userCredentials: UserCredentialsDto
|
userCredentials: UserCredentialsDto
|
||||||
): Promise<LoginResponseDto> {
|
): Promise<SuccessDto> {
|
||||||
const passwordHashed = await EncryptionService.hashData(
|
try {
|
||||||
userCredentials.password
|
const existingUser = await this.userCredentialsRepository.findUserByEmail(
|
||||||
);
|
userCredentials.email
|
||||||
|
|
||||||
const user = await this.userCredentialsRepository.createUser(
|
|
||||||
userCredentials.email,
|
|
||||||
passwordHashed
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.userDataRepository.createInitialUserData(user);
|
|
||||||
|
|
||||||
const token =
|
|
||||||
await this.emailVerificationService.generateEmailVerificationToken(
|
|
||||||
user.id
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.passwordConfirmationMailService.sendPasswordConfirmationMail(
|
if (existingUser) {
|
||||||
user.email,
|
throw new ConflictException('User already exists');
|
||||||
token
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return this.generateAndPersistTokens(user.id, user.email);
|
const passwordHashed = await EncryptionService.hashData(
|
||||||
|
userCredentials.password
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await this.userCredentialsRepository.createUser(
|
||||||
|
userCredentials.email,
|
||||||
|
passwordHashed
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.userDataRepository.createInitialUserData(user);
|
||||||
|
|
||||||
|
const token =
|
||||||
|
await this.emailVerificationService.generateEmailVerificationToken(
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.passwordConfirmationMailService.sendPasswordConfirmationMail(
|
||||||
|
user.email,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'User already exists. Please try to login instead.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'Error while signing up',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signin(
|
public async validateUser(
|
||||||
userCredentials: UserCredentialsDto,
|
|
||||||
response: Response,
|
|
||||||
request: Request
|
|
||||||
): Promise<LoginResponseDto> {
|
|
||||||
const user = await this.userCredentialsRepository.findUserByEmail(
|
|
||||||
userCredentials.email
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new ForbiddenException('Access Denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordMatch = await EncryptionService.compareHash(
|
|
||||||
userCredentials.password,
|
|
||||||
user.hash
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!passwordMatch) {
|
|
||||||
throw new ForbiddenException('Access Denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sessionService.checkSessionLimit(user.id);
|
|
||||||
|
|
||||||
const sesseionId = await this.sessionService.createSession(
|
|
||||||
user.id,
|
|
||||||
request.headers['user-agent']
|
|
||||||
);
|
|
||||||
|
|
||||||
this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
|
|
||||||
|
|
||||||
return this.generateAndPersistTokens(user.id, user.email, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async logout(userId: string): Promise<boolean> {
|
|
||||||
const affected =
|
|
||||||
await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
|
|
||||||
|
|
||||||
await this.sessionService.invalidateAllSessionsForUser(userId);
|
|
||||||
|
|
||||||
return affected > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async refresh(request: Request): Promise<AccessTokenDto> {
|
|
||||||
const sessionId = request.cookies['session_id'];
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
throw new ForbiddenException('Session ID missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
const session: Session =
|
|
||||||
await this.sessionService.findSessionBySessionId(sessionId);
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw new ForbiddenException('Invalid session');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
|
|
||||||
sessionId,
|
|
||||||
request.headers['user-agent']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isUserAgentValid) {
|
|
||||||
throw new ForbiddenException('Invalid session - User agent mismatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.sessionService.extendSessionExpiration(sessionId);
|
|
||||||
|
|
||||||
const decodedToken: TokenPayload = await this.validateRefreshToken(
|
|
||||||
session.userCredentials['id']
|
|
||||||
);
|
|
||||||
|
|
||||||
const newTokens = await this.generateAndPersistTokens(
|
|
||||||
decodedToken.sub,
|
|
||||||
decodedToken.email,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
return { access_token: newTokens.access_token };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async generateAndPersistTokens(
|
|
||||||
userId: string,
|
|
||||||
email: string,
|
email: string,
|
||||||
updateRefreshToken: boolean = false
|
password: string
|
||||||
): Promise<LoginResponseDto> {
|
): Promise<UserCredentials> {
|
||||||
const tokens = await this.tokenManagementService.generateTokens(
|
try {
|
||||||
userId,
|
const user = await this.userCredentialsRepository.findUserByEmail(email);
|
||||||
email
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updateRefreshToken) {
|
if (!user) {
|
||||||
await this.userCredentialsRepository.updateUserRefreshToken(
|
throw new ForbiddenException('Access Denied');
|
||||||
userId,
|
}
|
||||||
tokens.refresh_token
|
|
||||||
|
const passwordMatch = await EncryptionService.compareHash(
|
||||||
|
password,
|
||||||
|
user.hashedPassword
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return { access_token: tokens.access_token, email: email, userId: userId };
|
if (!passwordMatch) {
|
||||||
|
throw new ForbiddenException('Access Denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ForbiddenException) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'E-Mail address or password is incorrect. Please try again.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'Error while validating user credentials',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateRefreshToken(userId: string): Promise<TokenPayload> {
|
public async signout(sessionId: string): Promise<{ success: boolean }> {
|
||||||
const user = await this.userCredentialsRepository.findUserById(userId);
|
try {
|
||||||
|
this.sessionService.deleteSessionBySessionId(sessionId);
|
||||||
if (!user || !user.refreshToken) {
|
return { success: true };
|
||||||
throw new Error('No refresh token found');
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Fehler beim Logout',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const decodedToken = await this.tokenManagementService.verifyRefreshToken(
|
public async checkAuthStatus(
|
||||||
user.refreshToken
|
sessionId: string,
|
||||||
);
|
userAgend: string
|
||||||
|
): Promise<SuccessDto> {
|
||||||
|
try {
|
||||||
|
const session =
|
||||||
|
await this.sessionService.findSessionBySessionId(sessionId);
|
||||||
|
|
||||||
if (decodedToken.exp < Date.now() / 1000) {
|
if (!session) {
|
||||||
throw new Error('Token expired');
|
throw new ForbiddenException('Session not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgendFromSession = JSON.parse(session.json).passport.user
|
||||||
|
.userAgent;
|
||||||
|
|
||||||
|
if (userAgendFromSession !== userAgend) {
|
||||||
|
throw new ForbiddenException('User-Agent does not match');
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Error while checking auth status',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (decodedToken.sub !== user.id) {
|
public getLoginResponse(
|
||||||
throw new Error('Token subject mismatch');
|
user: SigninResponseDto & { userAgent: string }
|
||||||
}
|
): SigninResponseDto {
|
||||||
|
const { id, email }: SigninResponseDto = user;
|
||||||
|
const responseData: SigninResponseDto = { id, email };
|
||||||
|
|
||||||
return decodedToken;
|
return responseData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Response } from 'express';
|
|
||||||
import { Session } from 'src/entities';
|
|
||||||
import { DeleteResult } from 'typeorm';
|
|
||||||
|
|
||||||
import { SessionRepository } from '../repositories/session.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SessionService {
|
|
||||||
public constructor(private readonly sessionRepository: SessionRepository) {}
|
|
||||||
|
|
||||||
public createSession(userId: string, userAgent: string): Promise<Session> {
|
|
||||||
return this.sessionRepository.createSession(userId, userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public validateSessionUserAgent(
|
|
||||||
sessionId: string,
|
|
||||||
currentUserAgent: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
return this.sessionRepository.validateSessionUserAgent(
|
|
||||||
sessionId,
|
|
||||||
currentUserAgent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkSessionLimit(userId: string): Promise<DeleteResult> {
|
|
||||||
return this.sessionRepository.checkSessionLimit(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public invalidateAllSessionsForUser(userId: string): Promise<DeleteResult> {
|
|
||||||
return this.sessionRepository.invalidateAllSessionsForUser(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearExpiredSessions(): Promise<DeleteResult> {
|
|
||||||
return this.sessionRepository.clearExpiredSessions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public extendSessionExpiration(sessionId: string): Promise<Session> {
|
|
||||||
return this.sessionRepository.extendSessionExpiration(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public findSessionBySessionId(sessionId: string): Promise<Session> {
|
|
||||||
return this.sessionRepository.findSessionBySessionId(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public attachSessionToResponse(response: Response, sessionId: string): void {
|
|
||||||
this.sessionRepository.attachSessionToResponse(response, sessionId);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
|
|
||||||
import { TokenPayload, Tokens } from '../models/types';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TokenManagementService {
|
|
||||||
private readonly ACCESS_TOKEN_EXPIRY: string;
|
|
||||||
private readonly REFRESH_TOKEN_EXPIRY: string;
|
|
||||||
private readonly JWT_SECRET_AT: string;
|
|
||||||
private readonly JWT_SECRET_RT: string;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private readonly jwt: JwtService,
|
|
||||||
private readonly configService: ConfigService
|
|
||||||
) {
|
|
||||||
this.ACCESS_TOKEN_EXPIRY = this.configService.get<string>(
|
|
||||||
'ACCESS_TOKEN_EXPIRY'
|
|
||||||
);
|
|
||||||
this.REFRESH_TOKEN_EXPIRY = this.configService.get<string>(
|
|
||||||
'REFRESH_TOKEN_EXPIRY'
|
|
||||||
);
|
|
||||||
this.JWT_SECRET_AT = this.configService.get<string>('JWT_SECRET_AT');
|
|
||||||
this.JWT_SECRET_RT = this.configService.get<string>('JWT_SECRET_RT');
|
|
||||||
}
|
|
||||||
|
|
||||||
public async generateTokens(userId: string, email: string): Promise<Tokens> {
|
|
||||||
const access_token: string = await this.createAccessToken(userId, email);
|
|
||||||
const refresh_token: string = await this.createRefreshToken(userId, email);
|
|
||||||
|
|
||||||
return { access_token, refresh_token };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async verifyRefreshToken(token: string): Promise<TokenPayload> {
|
|
||||||
return this.jwt.verifyAsync(token, {
|
|
||||||
secret: this.JWT_SECRET_RT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createAccessToken(
|
|
||||||
userId: string,
|
|
||||||
email: string
|
|
||||||
): Promise<string> {
|
|
||||||
return this.jwt.signAsync(
|
|
||||||
{ sub: userId, email },
|
|
||||||
{
|
|
||||||
expiresIn: this.ACCESS_TOKEN_EXPIRY,
|
|
||||||
secret: this.JWT_SECRET_AT,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createRefreshToken(
|
|
||||||
userId: string,
|
|
||||||
email: string
|
|
||||||
): Promise<string> {
|
|
||||||
return this.jwt.signAsync(
|
|
||||||
{ sub: userId, email },
|
|
||||||
{
|
|
||||||
expiresIn: this.REFRESH_TOKEN_EXPIRY,
|
|
||||||
secret: this.JWT_SECRET_RT,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
|
||||||
import { Strategy, ExtractJwt } from 'passport-jwt';
|
|
||||||
|
|
||||||
import { JwtPayload } from '../models/types';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AccessTokenStrategy extends PassportStrategy(
|
|
||||||
Strategy,
|
|
||||||
'jwt-access-token'
|
|
||||||
) {
|
|
||||||
public constructor(private readonly configService: ConfigService) {
|
|
||||||
super(AccessTokenStrategy.getJwtConfig(configService));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getJwtConfig(configService: ConfigService): any {
|
|
||||||
return {
|
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
secretOrKey: configService.get<string>('JWT_SECRET_AT'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async validate(payload: JwtPayload): Promise<JwtPayload> {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './access-token.strategie';
|
|
||||||
export * from './refresh-token.strategie';
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { Strategy } from 'passport-local';
|
||||||
|
|
||||||
|
import { SigninResponseDto } from '../models/dto';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
|
public constructor(private readonly authService: AuthService) {
|
||||||
|
super({
|
||||||
|
usernameField: 'email',
|
||||||
|
passwordField: 'password',
|
||||||
|
passReqToCallback: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validate(
|
||||||
|
request: Request,
|
||||||
|
email: string,
|
||||||
|
password: string
|
||||||
|
): Promise<SigninResponseDto & { userAgent: string }> {
|
||||||
|
const user = await this.authService.validateUser(email, password);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = request.headers['user-agent'];
|
||||||
|
|
||||||
|
return { id: user.id, email: user.email, userAgent: userAgent };
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
import { Injectable, ForbiddenException } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
|
||||||
import { Request } from 'express';
|
|
||||||
import { Strategy, ExtractJwt } from 'passport-jwt';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RefreshTokenStrategy extends PassportStrategy(
|
|
||||||
Strategy,
|
|
||||||
'jwt-refresh-token'
|
|
||||||
) {
|
|
||||||
public constructor(private readonly configService: ConfigService) {
|
|
||||||
super(RefreshTokenStrategy.createJwtStrategyOptions(configService));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static createJwtStrategyOptions(configService: ConfigService): any {
|
|
||||||
return {
|
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
secretOrKey: configService.get<string>('JWT_SECRET_RT'),
|
|
||||||
passReqToCallback: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async validate(req: Request, payload: any) {
|
|
||||||
const refresh_token: string = req
|
|
||||||
?.get('authorization')
|
|
||||||
?.replace('Bearer', '')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (!refresh_token) {
|
|
||||||
throw new ForbiddenException('Refresh token malformed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
refresh_token,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './session.guard';
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { SessionService } from '../services/session.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionGuard implements CanActivate {
|
||||||
|
public constructor(private readonly sessionService: SessionService) {}
|
||||||
|
|
||||||
|
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const sessionId = request.session.id;
|
||||||
|
const currentAgent = request.headers['user-agent'];
|
||||||
|
const session = await this.sessionService.findSessionBySessionId(sessionId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new UnauthorizedException('Session not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpired = await this.sessionService.isSessioExpired(session);
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
throw new UnauthorizedException('Session expired.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgentInSession = JSON.parse(session.json).passport.user
|
||||||
|
.userAgent as string;
|
||||||
|
|
||||||
|
if (userAgentInSession !== currentAgent) {
|
||||||
|
throw new UnauthorizedException('User agent mismatch.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Session } from 'src/entities';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionRepository {
|
||||||
|
public constructor(
|
||||||
|
@InjectRepository(Session)
|
||||||
|
private readonly repository: Repository<Session>,
|
||||||
|
private readonly configService: ConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async findSessionsByUserId(userId: string): Promise<Session[]> {
|
||||||
|
return await this.repository
|
||||||
|
.createQueryBuilder('session')
|
||||||
|
.withDeleted()
|
||||||
|
.where('session.json ::jsonb @> :jsonFilter', {
|
||||||
|
jsonFilter: { passport: { user: { id: userId } } },
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findSessionBySessionId(
|
||||||
|
sessionId: string
|
||||||
|
): Promise<Session | null> {
|
||||||
|
return this.repository.findOne({ where: { id: sessionId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAllExpiredSessions(): Promise<void> {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
await this.repository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.delete()
|
||||||
|
.from(Session)
|
||||||
|
.where('expiredAt < :currentTime', { currentTime })
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAllSessionsForUser(userId: string): Promise<void> {
|
||||||
|
await this.repository
|
||||||
|
.createQueryBuilder('session')
|
||||||
|
.delete()
|
||||||
|
.where('session.json ::jsonb @> :jsonFilter', {
|
||||||
|
jsonFilter: { passport: { user: { id: userId } } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isSessionExpired(session: Session): Promise<boolean> {
|
||||||
|
return session.expiredAt < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteSessionBySessionId(sessionId: string): Promise<void> {
|
||||||
|
await this.repository.delete(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enforceSessionLimit(userId: string): Promise<void> {
|
||||||
|
const sessionLimit = this.configService.get<number>('SESSION_LIMIT');
|
||||||
|
const sessions = await this.repository
|
||||||
|
.createQueryBuilder('session')
|
||||||
|
.withDeleted()
|
||||||
|
.where('session.json ::jsonb @> :jsonFilter', {
|
||||||
|
jsonFilter: { passport: { user: { id: userId } } },
|
||||||
|
})
|
||||||
|
.orderBy('session.expiredAt', 'ASC')
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (sessions.length > sessionLimit) {
|
||||||
|
const sessionsToDelete = sessions.slice(
|
||||||
|
0,
|
||||||
|
sessions.length - sessionLimit
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.repository.remove(sessionsToDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './session-init.service';
|
||||||
|
export * from './session-serializer.service';
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeormStore } from 'connect-typeorm';
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
import * as session from 'express-session';
|
||||||
|
import { Session } from 'src/entities';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionInitService {
|
||||||
|
public constructor(
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
private readonly configService: ConfigService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public initSession(): RequestHandler<unknown> {
|
||||||
|
return session({
|
||||||
|
secret: [this.configService.get<string>('SESSION_SECRET')],
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: new TypeormStore({
|
||||||
|
cleanupLimit: 2,
|
||||||
|
limitSubquery: false,
|
||||||
|
ttl: 86400,
|
||||||
|
}).connect(this.dataSource.getRepository(Session)),
|
||||||
|
cookie: {
|
||||||
|
maxAge: 86400000,
|
||||||
|
httpOnly: true,
|
||||||
|
secure:
|
||||||
|
this.configService.get<string>('NODE_ENV') === 'development'
|
||||||
|
? false
|
||||||
|
: true,
|
||||||
|
|
||||||
|
sameSite:
|
||||||
|
this.configService.get<string>('NODE_ENV') === 'development'
|
||||||
|
? 'strict'
|
||||||
|
: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PassportSerializer } from '@nestjs/passport';
|
||||||
|
import { UserCredentials } from 'src/entities';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionSerializerService extends PassportSerializer {
|
||||||
|
public serializeUser(
|
||||||
|
user: UserCredentials & { userAgent: string },
|
||||||
|
done: (err: Error, user: any) => void
|
||||||
|
): void {
|
||||||
|
done(null, { id: user.id, userAgent: user.userAgent });
|
||||||
|
}
|
||||||
|
|
||||||
|
public deserializeUser(
|
||||||
|
payload: any,
|
||||||
|
done: (err: Error, payload: string) => void
|
||||||
|
): void {
|
||||||
|
done(null, payload);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Session } from 'src/entities';
|
||||||
|
import { UriEncoderService } from 'src/shared';
|
||||||
|
|
||||||
|
import { SessionRepository } from '../repository/session.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SessionService {
|
||||||
|
public constructor(private readonly sessionRepository: SessionRepository) {}
|
||||||
|
|
||||||
|
public async enforceSessionLimit(userId: string): Promise<void> {
|
||||||
|
return this.sessionRepository.enforceSessionLimit(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAllExpiredSessions(): Promise<void> {
|
||||||
|
return this.sessionRepository.deleteAllExpiredSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findSessionsByUserId(userId: string): Promise<Session[]> {
|
||||||
|
return this.sessionRepository.findSessionsByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isSessioExpired(session: Session): Promise<boolean> {
|
||||||
|
return this.sessionRepository.isSessionExpired(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteSessionBySessionId(sessionId: string): Promise<void> {
|
||||||
|
return this.sessionRepository.deleteSessionBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findSessionBySessionId(
|
||||||
|
sessionId: string
|
||||||
|
): Promise<Session | null> {
|
||||||
|
return this.sessionRepository.findSessionBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractSessionIdFromCookie(cookie: string): string | null {
|
||||||
|
try {
|
||||||
|
const decodedCookie = UriEncoderService.decodeUri(cookie);
|
||||||
|
const sessionIdPart = decodedCookie.split('.')[0];
|
||||||
|
|
||||||
|
if (sessionIdPart.startsWith('s:')) {
|
||||||
|
return sessionIdPart.substring(2);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Extrahieren der Session-ID:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Session } from 'src/entities';
|
||||||
|
|
||||||
|
import { SessionGuard } from './guard';
|
||||||
|
import { SessionRepository } from './repository/session.repository';
|
||||||
|
import { SessionInitService, SessionSerializerService } from './services';
|
||||||
|
import { SessionService } from './services/session.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Session])],
|
||||||
|
providers: [
|
||||||
|
SessionInitService,
|
||||||
|
SessionSerializerService,
|
||||||
|
SessionRepository,
|
||||||
|
SessionGuard,
|
||||||
|
SessionService,
|
||||||
|
],
|
||||||
|
controllers: [],
|
||||||
|
exports: [SessionService, SessionGuard],
|
||||||
|
})
|
||||||
|
export class SessionModule {}
|
|
@ -15,16 +15,16 @@ export class UserDataRepository {
|
||||||
): Promise<UserData> {
|
): Promise<UserData> {
|
||||||
const userData = new UserData();
|
const userData = new UserData();
|
||||||
|
|
||||||
userData.user = userCredentials;
|
userData.userCredentials = userCredentials;
|
||||||
userData.isEmailConfirmed = false;
|
userData.isEmailConfirmed = false;
|
||||||
|
|
||||||
return this.repository.save(userData);
|
return this.repository.save(userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateEmailVerificationStatus(userId: string): Promise<void> {
|
// public async updateEmailVerificationStatus(userId: string): Promise<void> {
|
||||||
await this.repository.update(
|
// await this.repository.update(
|
||||||
{ user: { id: userId } },
|
// { user: { id: userId } },
|
||||||
{ isEmailConfirmed: true }
|
// { isEmailConfirmed: true }
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,9 +42,9 @@ export class EmailVerificationService {
|
||||||
await this.deleteEmailVerificationToken(tokenToVerify);
|
await this.deleteEmailVerificationToken(tokenToVerify);
|
||||||
|
|
||||||
if (emailVerification && emailVerification.user) {
|
if (emailVerification && emailVerification.user) {
|
||||||
await this.userDataRepository.updateEmailVerificationStatus(
|
// await this.userDataRepository.updateEmailVerificationStatus(
|
||||||
emailVerification.user.id
|
// emailVerification.user.id
|
||||||
);
|
// );
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './utils/index';
|
export * from './utils/index';
|
||||||
export * from './decorator/index';
|
export * from './decorator/index';
|
||||||
|
export * from './models/index';
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './success.dto';
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class SuccessDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Success status',
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
@IsBoolean()
|
||||||
|
public success: boolean;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './dto/index';
|
|
@ -1,7 +1,6 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterOutlet, Router } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
import { AuthService } from './shared/service';
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
@ -10,24 +9,6 @@ import { AuthService } from './shared/service';
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent {
|
||||||
public constructor(
|
public constructor() {}
|
||||||
private readonly authService: AuthService,
|
|
||||||
private readonly router: Router
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public ngOnInit(): void {
|
|
||||||
this.checkAuthentication();
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkAuthentication(): void {
|
|
||||||
this.authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
|
|
||||||
if (isAuthenticated) {
|
|
||||||
console.log('User is authenticated');
|
|
||||||
this.router.navigateByUrl('dashboard');
|
|
||||||
} else {
|
|
||||||
this.router.navigateByUrl('signup');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,18 @@ import { ApplicationConfig } from '@angular/core';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||||
|
|
||||||
|
import { Configuration } from './api';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { AuthInterceptor } from './shared/interceptors/auth.interceptor';
|
import { ApiConfiguration } from './config/api-configuration';
|
||||||
|
|
||||||
|
const apiConfiguration = new ApiConfiguration({
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptors([AuthInterceptor])),
|
{ provide: Configuration, useValue: apiConfiguration },
|
||||||
|
provideHttpClient(withInterceptors([])),
|
||||||
provideRouter(routes, withComponentInputBinding()),
|
provideRouter(routes, withComponentInputBinding()),
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
import { AuthGuard } from './shared/guard/auth.guard';
|
|
||||||
|
|
||||||
const publicRoutes: Routes = [
|
const publicRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./app.component').then((m) => m.AppComponent),
|
loadComponent: () =>
|
||||||
|
import('./pages/home-root/home-root.component').then(
|
||||||
|
(m) => m.HomeComponent
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'signup',
|
path: 'signup',
|
||||||
|
@ -30,7 +31,7 @@ const protectedRoutes: Routes = [
|
||||||
import('./pages/dashboard-root/dashboard-root.component').then(
|
import('./pages/dashboard-root/dashboard-root.component').then(
|
||||||
(m) => m.DashboardRootComponent
|
(m) => m.DashboardRootComponent
|
||||||
),
|
),
|
||||||
canActivate: [AuthGuard],
|
canActivate: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Configuration, ConfigurationParameters } from '../api';
|
||||||
|
|
||||||
|
export class ApiConfiguration extends Configuration {
|
||||||
|
public constructor(params?: Partial<ConfigurationParameters>) {
|
||||||
|
super({
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { SuccessDtoApiModel } from '../../api';
|
||||||
|
import { AuthService } from '../../shared/service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-foo',
|
||||||
|
standalone: true,
|
||||||
|
providers: [],
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './home-root.component.html',
|
||||||
|
styleUrl: './home-root.component.scss',
|
||||||
|
})
|
||||||
|
export class HomeComponent implements OnInit {
|
||||||
|
public constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.authService.status().subscribe(
|
||||||
|
(response: SuccessDtoApiModel) => {
|
||||||
|
if (response.success) {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error: HttpErrorResponse) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
this.router.navigate(['signup'], {
|
||||||
|
queryParams: { login: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,94 +1,110 @@
|
||||||
<div id="background">
|
<div id="background">
|
||||||
<div class="img-zone">
|
<div class="img-zone">
|
||||||
<div class="img-wrapper">
|
<div class="img-wrapper">
|
||||||
<h1>Hi, Welcome to Ticket App.</h1>
|
@if (userSignupSuccess()) {
|
||||||
|
<div class="success">
|
||||||
|
<h1>Danke für deine Registrierung!</h1>
|
||||||
|
<h2>
|
||||||
|
Wir haben dir eine Mail geschickt an
|
||||||
|
{{ form?.get('email')?.value }}. Bitte bestätige deine
|
||||||
|
E-Mail-Adresse um fortzufahren.
|
||||||
|
</h2>
|
||||||
|
<p>Du kannst diesen Tab nun schließen</p>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="headline">
|
||||||
|
<h1>Hi, Welcome to Ticket App.</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-zone">
|
|
||||||
<h1>
|
|
||||||
@if (isSignupSignal()) {
|
|
||||||
Anmelden
|
|
||||||
} @else if (isRegisterSignal()) {
|
|
||||||
Registrieren
|
|
||||||
} @else {
|
|
||||||
Erste Schritte
|
|
||||||
}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
@if (isDisplayButtons()) {
|
@if (!userSignupSuccess()) {
|
||||||
<div class="action">
|
<div class="content-zone">
|
||||||
<button
|
<h1>
|
||||||
pButton
|
@if (isSignupSignal()) {
|
||||||
type="button"
|
Anmelden
|
||||||
label="Anmelden"
|
} @else if (isRegisterSignal()) {
|
||||||
(click)="toggleAction('signup')"></button>
|
Registrieren
|
||||||
<button
|
} @else {
|
||||||
pButton
|
Erste Schritte
|
||||||
type="button"
|
|
||||||
label="Registrieren"
|
|
||||||
(click)="toggleAction('register')"></button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (isSignupSignal() || isRegisterSignal()) {
|
|
||||||
<div class="register-wrapper">
|
|
||||||
@if (form) {
|
|
||||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
||||||
<div class="e-mail">
|
|
||||||
<div class="label">
|
|
||||||
<label for="email">E-Mail</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
pInputText
|
|
||||||
id="email"
|
|
||||||
formControlName="email"
|
|
||||||
aria-describedby="e-mail" />
|
|
||||||
</div>
|
|
||||||
<div class="password">
|
|
||||||
<div class="label">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
</div>
|
|
||||||
<p-password
|
|
||||||
class="custom-p-password"
|
|
||||||
id="password"
|
|
||||||
formControlName="password"
|
|
||||||
aria-describedby="password"
|
|
||||||
[toggleMask]="true"></p-password>
|
|
||||||
</div>
|
|
||||||
@if (isRegisterSignal()) {
|
|
||||||
<div class="terms">
|
|
||||||
<p-checkbox
|
|
||||||
formControlName="terms"
|
|
||||||
label="Ich habe die AGB gelesen und stimme zu."
|
|
||||||
name="terms"
|
|
||||||
[binary]="true"></p-checkbox>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="signup">
|
|
||||||
<button
|
|
||||||
pButton
|
|
||||||
type="submit"
|
|
||||||
[label]="
|
|
||||||
isSignupSignal()
|
|
||||||
? 'Anmelden'
|
|
||||||
: '✨ Jetzt KOSTENFREI loslegen ✨'
|
|
||||||
"></button>
|
|
||||||
</div>
|
|
||||||
<div class="change-mask">
|
|
||||||
<a
|
|
||||||
(click)="switchMask()"
|
|
||||||
(keyup.enter)="switchMask()"
|
|
||||||
tabindex="0">
|
|
||||||
@if (isSignupSignal()) {
|
|
||||||
Kein Account? Erstellen Sie jetzt KOSTENFREI einen!
|
|
||||||
} @else {
|
|
||||||
Schon einen Account? Hier einloggen
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
}
|
||||||
</div>
|
</h1>
|
||||||
}
|
|
||||||
</div>
|
@if (isDisplayButtons()) {
|
||||||
|
<div class="action">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="Anmelden"
|
||||||
|
(click)="toggleAction('signup')"></button>
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
label="Registrieren"
|
||||||
|
(click)="toggleAction('register')"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (isSignupSignal() || isRegisterSignal()) {
|
||||||
|
<div class="register-wrapper">
|
||||||
|
@if (form) {
|
||||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
|
<div class="e-mail">
|
||||||
|
<div class="label">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
pInputText
|
||||||
|
id="email"
|
||||||
|
formControlName="email"
|
||||||
|
aria-describedby="e-mail" />
|
||||||
|
</div>
|
||||||
|
<div class="password">
|
||||||
|
<div class="label">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
</div>
|
||||||
|
<p-password
|
||||||
|
class="custom-p-password"
|
||||||
|
id="password"
|
||||||
|
formControlName="password"
|
||||||
|
aria-describedby="password"
|
||||||
|
[toggleMask]="true"></p-password>
|
||||||
|
</div>
|
||||||
|
@if (isRegisterSignal()) {
|
||||||
|
<div class="terms">
|
||||||
|
<p-checkbox
|
||||||
|
formControlName="terms"
|
||||||
|
label="Ich habe die AGB gelesen und stimme zu."
|
||||||
|
name="terms"
|
||||||
|
[binary]="true"></p-checkbox>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="signup">
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="submit"
|
||||||
|
[label]="
|
||||||
|
isSignupSignal()
|
||||||
|
? 'Anmelden'
|
||||||
|
: '✨ Jetzt KOSTENFREI loslegen ✨'
|
||||||
|
"></button>
|
||||||
|
</div>
|
||||||
|
<div class="change-mask">
|
||||||
|
<a
|
||||||
|
(click)="switchMask()"
|
||||||
|
(keyup.enter)="switchMask()"
|
||||||
|
tabindex="0">
|
||||||
|
@if (isSignupSignal()) {
|
||||||
|
Kein Account? Erstellen Sie jetzt KOSTENFREI einen!
|
||||||
|
} @else {
|
||||||
|
Schon einen Account? Hier einloggen
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,10 +12,20 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
h1 {
|
.success {
|
||||||
font-size: 4em;
|
margin-left: 4em;
|
||||||
margin-left: 1em;
|
h1 {
|
||||||
|
font-size: 4em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
h1 {
|
||||||
|
font-size: 4em;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,14 @@ import { CheckboxModule } from 'primeng/checkbox';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { PasswordModule } from 'primeng/password';
|
import { PasswordModule } from 'primeng/password';
|
||||||
|
|
||||||
import { AuthService } from '../../shared/service';
|
import {
|
||||||
import { LoginCredentials } from '../../shared/types';
|
Configuration,
|
||||||
|
SigninResponseDtoApiModel,
|
||||||
|
SuccessDtoApiModel,
|
||||||
|
UserCredentialsDtoApiModel,
|
||||||
|
} from '../../api';
|
||||||
|
import { ApiConfiguration } from '../../config/api-configuration';
|
||||||
|
import { AuthService, SessionStorageService } from '../../shared/service';
|
||||||
import {
|
import {
|
||||||
customEmailValidator,
|
customEmailValidator,
|
||||||
customPasswordValidator,
|
customPasswordValidator,
|
||||||
|
@ -47,13 +53,20 @@ type AuthAction = 'register' | 'signup';
|
||||||
PasswordModule,
|
PasswordModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: Configuration,
|
||||||
|
useFactory: (): unknown =>
|
||||||
|
new ApiConfiguration({ withCredentials: true }),
|
||||||
|
},
|
||||||
|
],
|
||||||
templateUrl: './register-root.component.html',
|
templateUrl: './register-root.component.html',
|
||||||
styleUrl: './register-root.component.scss',
|
styleUrl: './register-root.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class RegisterRootComponent implements OnInit {
|
export class RegisterRootComponent implements OnInit {
|
||||||
public verified: InputSignal<boolean> = input<boolean>(false);
|
public verified: InputSignal<boolean> = input<boolean>(false);
|
||||||
|
public login: InputSignal<boolean> = input<boolean>(false);
|
||||||
public email: InputSignal<string> = input<string>('');
|
public email: InputSignal<string> = input<string>('');
|
||||||
public form: FormGroup | undefined;
|
public form: FormGroup | undefined;
|
||||||
public isRegisterSignal: WritableSignal<boolean> = signal(false);
|
public isRegisterSignal: WritableSignal<boolean> = signal(false);
|
||||||
|
@ -62,12 +75,14 @@ export class RegisterRootComponent implements OnInit {
|
||||||
public emailInvalid: WritableSignal<string | null> = signal(null);
|
public emailInvalid: WritableSignal<string | null> = signal(null);
|
||||||
public passwordInvalid: WritableSignal<string | null> = signal(null);
|
public passwordInvalid: WritableSignal<string | null> = signal(null);
|
||||||
public termsInvalid: WritableSignal<string | null> = signal(null);
|
public termsInvalid: WritableSignal<string | null> = signal(null);
|
||||||
|
public userSignupSuccess: WritableSignal<boolean> = signal(false);
|
||||||
private removeQueryParams: WritableSignal<boolean> = signal(false);
|
private removeQueryParams: WritableSignal<boolean> = signal(false);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly formBuilder: FormBuilder,
|
private readonly formBuilder: FormBuilder,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly router: Router
|
private readonly router: Router,
|
||||||
|
private readonly sessionStorageService: SessionStorageService
|
||||||
) {
|
) {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (this.form) {
|
if (this.form) {
|
||||||
|
@ -90,13 +105,22 @@ export class RegisterRootComponent implements OnInit {
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.initializeForm();
|
this.initializeForm();
|
||||||
this.setupValueChanges();
|
this.setupValueChanges();
|
||||||
|
this.preselectForm();
|
||||||
|
|
||||||
if (this.email() || this.verified()) {
|
if ((this.email() && this.verified()) || this.login()) {
|
||||||
this.handleRedirect();
|
this.handleRedirect();
|
||||||
this.removeQueryParams.set(true);
|
this.removeQueryParams.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public preselectForm(): void {
|
||||||
|
if (!this.email() || !this.verified()) {
|
||||||
|
const email = this.sessionStorageService.getItem('email');
|
||||||
|
|
||||||
|
this.form?.get('email')?.setValue(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public toggleAction(action: AuthAction): void {
|
public toggleAction(action: AuthAction): void {
|
||||||
if (action === 'register') {
|
if (action === 'register') {
|
||||||
this.isRegisterSignal.set(true);
|
this.isRegisterSignal.set(true);
|
||||||
|
@ -113,7 +137,7 @@ export class RegisterRootComponent implements OnInit {
|
||||||
|
|
||||||
if (this.form?.valid) {
|
if (this.form?.valid) {
|
||||||
if (this.isRegisterSignal()) {
|
if (this.isRegisterSignal()) {
|
||||||
this.register(this.form.value);
|
this.signup(this.form.value);
|
||||||
} else {
|
} else {
|
||||||
this.signin(this.form.value);
|
this.signin(this.form.value);
|
||||||
}
|
}
|
||||||
|
@ -150,7 +174,6 @@ export class RegisterRootComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRedirect(): void {
|
private handleRedirect(): void {
|
||||||
console.log('handleRedirect');
|
|
||||||
if (this.verified()) {
|
if (this.verified()) {
|
||||||
this.isDisplayButtons.set(false);
|
this.isDisplayButtons.set(false);
|
||||||
this.isRegisterSignal.set(false);
|
this.isRegisterSignal.set(false);
|
||||||
|
@ -159,6 +182,12 @@ export class RegisterRootComponent implements OnInit {
|
||||||
if (this.email()) {
|
if (this.email()) {
|
||||||
this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email())));
|
this.form?.get('email')?.setValue(decodeURIComponent(atob(this.email())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.login()) {
|
||||||
|
this.isSignupSignal.set(true);
|
||||||
|
this.isDisplayButtons.set(false);
|
||||||
|
this.isRegisterSignal.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearRouteParams(): void {
|
private clearRouteParams(): void {
|
||||||
|
@ -237,11 +266,23 @@ export class RegisterRootComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private signin(logiCredentials: LoginCredentials): void {
|
private signin(logiCredentials: UserCredentialsDtoApiModel): void {
|
||||||
this.authService.signin(logiCredentials);
|
this.authService
|
||||||
|
.signin(logiCredentials)
|
||||||
|
.subscribe((response: SigninResponseDtoApiModel) => {
|
||||||
|
if (response) {
|
||||||
|
this.router.navigate(['/dashboard']);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private register(logiCredentials: LoginCredentials): void {
|
private signup(logiCredentials: UserCredentialsDtoApiModel): void {
|
||||||
this.authService.signup(logiCredentials);
|
this.authService
|
||||||
|
.signup(logiCredentials)
|
||||||
|
.subscribe((response: SuccessDtoApiModel) => {
|
||||||
|
if (response.success) {
|
||||||
|
this.userSignupSuccess.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
CanActivateFn,
|
|
||||||
Router,
|
|
||||||
RouterStateSnapshot,
|
|
||||||
UrlTree,
|
|
||||||
} from '@angular/router';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../service';
|
|
||||||
|
|
||||||
export const AuthGuard: CanActivateFn = (
|
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot
|
|
||||||
):
|
|
||||||
| Observable<boolean | UrlTree>
|
|
||||||
| Promise<boolean | UrlTree>
|
|
||||||
| boolean
|
|
||||||
| UrlTree => {
|
|
||||||
const authService: AuthService = inject(AuthService);
|
|
||||||
const router: Router = inject(Router);
|
|
||||||
|
|
||||||
authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
router.navigateByUrl('signup');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
|
@ -1,79 +0,0 @@
|
||||||
import {
|
|
||||||
HttpInterceptorFn,
|
|
||||||
HttpRequest,
|
|
||||||
HttpHandlerFn,
|
|
||||||
HttpEvent,
|
|
||||||
HttpErrorResponse,
|
|
||||||
} from '@angular/common/http';
|
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
import { Observable, throwError } from 'rxjs';
|
|
||||||
import { catchError, switchMap } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { AuthService } from '../service';
|
|
||||||
|
|
||||||
export const AuthInterceptor: HttpInterceptorFn = (
|
|
||||||
request: HttpRequest<unknown>,
|
|
||||||
next: HttpHandlerFn
|
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
const router = inject(Router);
|
|
||||||
const authService = inject(AuthService);
|
|
||||||
|
|
||||||
const handleRequest = (
|
|
||||||
req: HttpRequest<unknown>
|
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
const accessToken = authService.access_token;
|
|
||||||
|
|
||||||
if (accessToken) {
|
|
||||||
req = addAuthHeader(req, accessToken);
|
|
||||||
}
|
|
||||||
return next(req);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAuthHeader = (
|
|
||||||
req: HttpRequest<unknown>,
|
|
||||||
token: string
|
|
||||||
): HttpRequest<unknown> => {
|
|
||||||
return req.clone({
|
|
||||||
setHeaders: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handle401Error = (
|
|
||||||
req: HttpRequest<unknown>
|
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
console.log(authService.refresh_token);
|
|
||||||
if (!authService.refresh_token) {
|
|
||||||
router.navigateByUrl('signup');
|
|
||||||
return throwError(() => new Error('Authentication required'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return authService.refreshToken().pipe(
|
|
||||||
switchMap((tokens) => {
|
|
||||||
req = addAuthHeader(req, tokens.access_token);
|
|
||||||
return next(req);
|
|
||||||
}),
|
|
||||||
catchError((refreshError) => {
|
|
||||||
router.navigateByUrl('signup');
|
|
||||||
return throwError(() => new Error(refreshError));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = (
|
|
||||||
error: HttpErrorResponse,
|
|
||||||
req: HttpRequest<unknown>
|
|
||||||
): Observable<HttpEvent<unknown>> => {
|
|
||||||
if (error.status === 401) {
|
|
||||||
return handle401Error(req);
|
|
||||||
}
|
|
||||||
return throwError(() => new Error('Unhandled error'));
|
|
||||||
};
|
|
||||||
|
|
||||||
return handleRequest(request).pipe(
|
|
||||||
catchError((error) => handleError(error, request))
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, tap } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SigninResponseDtoApiModel,
|
||||||
|
UserCredentialsDtoApiModel,
|
||||||
|
} from '../../api';
|
||||||
import { AuthenticationApiService } from '../../api/api/authentication.api.service';
|
import { AuthenticationApiService } from '../../api/api/authentication.api.service';
|
||||||
import { LoginCredentials, Tokens } from '../types';
|
|
||||||
|
|
||||||
import { LocalStorageService } from './local-storage.service';
|
type SuccessResponse = {
|
||||||
import { SessionStorageService } from './session-storage.service';
|
success: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
|
@ -14,73 +18,28 @@ import { SessionStorageService } from './session-storage.service';
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public isAuthenticated$: BehaviorSubject<boolean> =
|
public isAuthenticated$: BehaviorSubject<boolean> =
|
||||||
new BehaviorSubject<boolean>(false);
|
new BehaviorSubject<boolean>(false);
|
||||||
private _access_token: string | null = null;
|
|
||||||
private _refresh_token: string | null = null;
|
|
||||||
|
|
||||||
public get access_token(): string | null {
|
|
||||||
return this._access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get refresh_token(): string | null {
|
|
||||||
return this._refresh_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly localStorageService: LocalStorageService,
|
|
||||||
private readonly sessionStorageService: SessionStorageService,
|
|
||||||
private readonly authenticationApiService: AuthenticationApiService
|
private readonly authenticationApiService: AuthenticationApiService
|
||||||
) {
|
) {}
|
||||||
this._access_token =
|
|
||||||
this.localStorageService.getItem<string>('access_token');
|
public signup(
|
||||||
this._refresh_token =
|
credentials: UserCredentialsDtoApiModel
|
||||||
this.sessionStorageService.getItem<string>('refresh_token');
|
): Observable<SuccessResponse> {
|
||||||
|
return this.authenticationApiService.authControllerSignup(credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
public signin(credentials: LoginCredentials): void {
|
public signin(
|
||||||
this.authenticationApiService
|
credentials: UserCredentialsDtoApiModel
|
||||||
.authControllerSignin(credentials)
|
): Observable<SigninResponseDtoApiModel> {
|
||||||
.subscribe((response: Tokens) => {
|
return this.authenticationApiService.authControllerSignin(credentials);
|
||||||
this.handleSuccess(response);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public signup(credentials: LoginCredentials): void {
|
public signout(): Observable<SuccessResponse> {
|
||||||
this.authenticationApiService
|
return this.authenticationApiService.authControllerSignout();
|
||||||
.authControllerSignup(credentials)
|
|
||||||
.subscribe((response: Tokens) => {
|
|
||||||
this.handleSuccess(response);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshToken(): Observable<Tokens> {
|
public status(): Observable<SuccessResponse> {
|
||||||
if (this._refresh_token) {
|
return this.authenticationApiService.authControllerStatus();
|
||||||
return this.authenticationApiService
|
|
||||||
.authControllerRefresh(this._refresh_token)
|
|
||||||
.pipe(tap((response: Tokens) => this.handleSuccess(response)));
|
|
||||||
} else {
|
|
||||||
throw new Error('Refresh token is missing');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public signout(): void {
|
|
||||||
this.authenticationApiService
|
|
||||||
.authControllerLogout()
|
|
||||||
.subscribe((response: boolean) => {
|
|
||||||
if (response) {
|
|
||||||
this._access_token = null;
|
|
||||||
this._refresh_token = null;
|
|
||||||
this.localStorageService.removeItem('access_token');
|
|
||||||
this.sessionStorageService.removeItem('refresh_token');
|
|
||||||
this.isAuthenticated$.next(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSuccess(tokens: Tokens): void {
|
|
||||||
this._access_token = tokens.access_token;
|
|
||||||
this._refresh_token = tokens.refresh_token;
|
|
||||||
this.localStorageService.setItem('access_token', tokens.access_token);
|
|
||||||
this.sessionStorageService.setItem('refresh_token', tokens.refresh_token);
|
|
||||||
this.isAuthenticated$.next(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './login-credentials';
|
|
||||||
export * from './tokens';
|
|
|
@ -1,4 +0,0 @@
|
||||||
export type LoginCredentials = {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
export type Tokens = {
|
|
||||||
access_token: string;
|
|
||||||
refresh_token: string;
|
|
||||||
};
|
|
Loading…
Reference in New Issue