diff --git a/backend/package.json b/backend/package.json index 313a392..cb56f83 100644 --- a/backend/package.json +++ b/backend/package.json @@ -36,9 +36,13 @@ "argon2": "^0.40.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "connect-typeorm": "^2.0.0", "cookie-parser": "^1.4.6", + "express-session": "^1.18.0", + "install": "^0.13.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pg": "^8.11.5", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -53,8 +57,10 @@ "@types/argon2": "^0.15.0", "@types/cookie-parser": "^1.4.7", "@types/express": "^4.17.17", + "@types/express-session": "^1.18.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 6ed72dd..8fb00ea 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -44,15 +44,27 @@ dependencies: class-validator: specifier: ^0.14.1 version: 0.14.1 + connect-typeorm: + specifier: ^2.0.0 + version: 2.0.0(typeorm@0.3.20) cookie-parser: specifier: ^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: specifier: ^0.7.0 version: 0.7.0 passport-jwt: specifier: ^4.0.1 version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 pg: specifier: ^8.11.5 version: 8.11.5 @@ -91,12 +103,18 @@ devDependencies: '@types/express': specifier: ^4.17.17 version: 4.17.21 + '@types/express-session': + specifier: ^1.18.0 + version: 1.18.0 '@types/jest': specifier: ^29.5.2 version: 29.5.12 '@types/node': specifier: ^20.3.1 version: 20.12.4 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 '@types/supertest': specifier: ^6.0.0 version: 6.0.2 @@ -1304,13 +1322,11 @@ packages: dependencies: '@types/connect': 3.4.38 '@types/node': 20.12.4 - dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: '@types/node': 20.12.4 - dev: true /@types/cookie-parser@1.4.7: resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==} @@ -1322,6 +1338,10 @@ packages: resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} dev: true + /@types/debug@0.0.31: + resolution: {integrity: sha512-LS1MCPaQKqspg7FvexuhmDbWUhE2yIJ+4AgVIyObfc06/UKZ8REgxGNjZc82wPLWmbeOm7S+gSsLgo75TanG4A==} + dev: false + /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -1347,7 +1367,11 @@ packages: '@types/qs': 6.9.14 '@types/range-parser': 1.2.7 '@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: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} @@ -1356,7 +1380,6 @@ packages: '@types/express-serve-static-core': 4.17.43 '@types/qs': 6.9.14 '@types/serve-static': 1.15.7 - dev: true /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -1366,7 +1389,6 @@ packages: /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - dev: true /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -1411,27 +1433,44 @@ packages: /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - dev: true /@types/node@20.12.4: resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==} dependencies: 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: resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==} - dev: true /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - dev: true /@types/send@0.17.4: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 '@types/node': 20.12.4 - dev: true /@types/serve-static@1.15.7: resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} @@ -1439,7 +1478,6 @@ packages: '@types/http-errors': 2.0.4 '@types/node': 20.12.4 '@types/send': 0.17.4 - dev: true /@types/stack-utils@2.0.3: resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -2254,6 +2292,20 @@ packages: readable-stream: 2.3.8 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: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} @@ -2282,6 +2334,10 @@ packages: /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + /cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + dev: false + /cookie@0.4.1: resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} engines: {node: '>= 0.6'} @@ -2709,6 +2765,22 @@ packages: jest-util: 29.7.0 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: resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} @@ -3224,6 +3296,11 @@ packages: wrap-ansi: 6.2.0 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: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} @@ -4259,6 +4336,11 @@ packages: dependencies: 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: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -4379,6 +4461,13 @@ packages: passport-strategy: 1.0.0 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: resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} engines: {node: '>= 0.4.0'} @@ -4616,6 +4705,11 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -5457,6 +5551,13 @@ packages: engines: {node: '>=14.17'} 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: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index aebae39..61046f8 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -10,9 +10,9 @@ import { CspMiddleware } from './middleware/csp-middleware/csp.middleware'; import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware'; import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware'; 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 { SendgridModule } from './modules/sendgrid-module/sendgrid.module'; +import { SessionModule } from './modules/session/session.module'; import { UserModule } from './modules/user-module/user.module'; import { VerifyModule } from './modules/verify-module/verify.module'; @@ -27,13 +27,10 @@ import { VerifyModule } from './modules/verify-module/verify.module'; UserModule, SendgridModule, VerifyModule, + SessionModule, ], controllers: [AppController], - providers: [ - AppService, - { provide: 'APP_GUARD', useClass: AccessTokenGuard }, - ClearExpiredSessionsCron, - ], + providers: [AppService, ClearExpiredSessionsCron], }) export class AppModule { public configure(consumer: MiddlewareConsumer): void { diff --git a/backend/src/cron/clear-expired-sesstions.cron.ts b/backend/src/cron/clear-expired-sesstions.cron.ts index 7681936..8d5a56e 100644 --- a/backend/src/cron/clear-expired-sesstions.cron.ts +++ b/backend/src/cron/clear-expired-sesstions.cron.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; 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() export class ClearExpiredSessionsCron { @@ -8,12 +8,13 @@ export class ClearExpiredSessionsCron { public constructor(private readonly sessionService: SessionService) {} - @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, { + @Cron(CronExpression.EVERY_12_HOURS, { name: 'Clear-Expired-Sessions', timeZone: 'Europe/Berlin', }) public handleCron(): void { - this.logger.log('Cronjob Executed: Clear-Expired-Sessions'); - this.sessionService.clearExpiredSessions(); + this.logger.log('-Cronjob Executed: Delete-Expired-Sessions-'); + this.sessionService.deleteAllExpiredSessions(); + this.logger.log('-------------------------------------------'); } } diff --git a/backend/src/entities/session.entity.ts b/backend/src/entities/session.entity.ts index 80a3bfb..60e7f84 100644 --- a/backend/src/entities/session.entity.ts +++ b/backend/src/entities/session.entity.ts @@ -1,34 +1,29 @@ +import { ISession } from 'connect-typeorm'; import { Entity, - PrimaryGeneratedColumn, Column, - ManyToOne, - JoinColumn, CreateDateColumn, UpdateDateColumn, + BaseEntity, + Index, + DeleteDateColumn, + PrimaryColumn, } from 'typeorm'; -import { UserCredentials } from './user-credentials.entity'; - @Entity() -export class Session { - @PrimaryGeneratedColumn('uuid') +export class Session extends BaseEntity implements ISession { + @PrimaryColumn('varchar', { length: 255 }) public id: string; - @Column() - public sessionId: string; + @Index() + @Column('bigint') + public expiredAt: number; - @Column({ type: 'timestamp' }) - public expiresAt: Date; + @Column({ type: 'text' }) + public json: string; - @Column({}) - public userAgent: string; - - @ManyToOne(() => UserCredentials, (userCredentials) => userCredentials.id, { - nullable: false, - }) - @JoinColumn({ name: 'userCredentialsId' }) - public userCredentials: UserCredentials['id']; + @DeleteDateColumn() + public destroyedAt?: Date; @CreateDateColumn() public createdAt: Date; diff --git a/backend/src/entities/user-credentials.entity.ts b/backend/src/entities/user-credentials.entity.ts index 2f8eff4..efa21e3 100644 --- a/backend/src/entities/user-credentials.entity.ts +++ b/backend/src/entities/user-credentials.entity.ts @@ -15,10 +15,7 @@ export class UserCredentials { public email: string; @Column() - public hash: string; - - @Column({ nullable: true }) - public refreshToken?: string; + public hashedPassword: string; @CreateDateColumn() public createdAt: Date; diff --git a/backend/src/entities/user-data.entity.ts b/backend/src/entities/user-data.entity.ts index b0ab112..6471ad1 100644 --- a/backend/src/entities/user-data.entity.ts +++ b/backend/src/entities/user-data.entity.ts @@ -18,9 +18,9 @@ export class UserData { @Column({ default: false }) public isEmailConfirmed: boolean; - @OneToOne(() => UserCredentials) - @JoinColumn({ name: 'userCredentialsId' }) - public user: UserCredentials; + @OneToOne(() => UserCredentials, { eager: true }) // eager: true lädt UserCredentials automatisch, wenn Sie UserData laden + @JoinColumn() // Diese Dekoration sagt TypeORM, welche Spalte der Fremdschlüssel ist + public userCredentials: UserCredentials; @CreateDateColumn() public createdAt: Date; diff --git a/backend/src/main.ts b/backend/src/main.ts index b07d58f..2e64907 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -7,6 +7,7 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import * as cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; +import { SessionInitService } from './modules/session/services'; async function setupSwagger(app: INestApplication): Promise { const config = new DocumentBuilder() @@ -31,6 +32,12 @@ async function setupSwagger(app: INestApplication): Promise { ); } +async function setupSessions(app: INestApplication): Promise { + const sessionService = app.get(SessionInitService); + + app.use(sessionService.initSession()); +} + async function setupCookieParser(app: INestApplication): Promise { app.use(cookieParser()); } @@ -50,6 +57,7 @@ async function bootstrap(): Promise { await setupSwagger(app); await setupPrefix(app); await setupClassValidator(app); + await setupSessions(app); await app.listen(3000); } bootstrap(); diff --git a/backend/src/middleware/cors-middleware/cors.middlware.ts b/backend/src/middleware/cors-middleware/cors.middlware.ts index 653da38..8850df1 100644 --- a/backend/src/middleware/cors-middleware/cors.middlware.ts +++ b/backend/src/middleware/cors-middleware/cors.middlware.ts @@ -14,6 +14,7 @@ export class CorsMiddleware implements NestMiddleware { const requestOrigin = req.headers.origin; if (!requestOrigin || allowedOrigins.includes(requestOrigin)) { + res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Origin', requestOrigin || '*'); res.header( 'Access-Control-Allow-Methods', diff --git a/backend/src/middleware/csp-middleware/csp.middleware.ts b/backend/src/middleware/csp-middleware/csp.middleware.ts index 3754fa4..71597db 100644 --- a/backend/src/middleware/csp-middleware/csp.middleware.ts +++ b/backend/src/middleware/csp-middleware/csp.middleware.ts @@ -12,6 +12,7 @@ export class CspMiddleware implements NestMiddleware { if (cspDirectives) { res.setHeader('Content-Security-Policy', cspDirectives); } + next(); } } diff --git a/backend/src/modules/auth-module/auth.module.ts b/backend/src/modules/auth-module/auth.module.ts index fa74b66..de78797 100644 --- a/backend/src/modules/auth-module/auth.module.ts +++ b/backend/src/modules/auth-module/auth.module.ts @@ -1,38 +1,31 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Session, UserCredentials } from 'src/entities'; +import { UserCredentials } from 'src/entities'; import { SendgridModule } from '../sendgrid-module/sendgrid.module'; +import { SessionModule } from '../session/session.module'; import { UserModule } from '../user-module/user.module'; import { VerifyModule } from '../verify-module/verify.module'; import { AuthController } from './controller/auth.controller'; -import { SessionRepository } from './repositories/session.repository'; import { UserCredentialsRepository } from './repositories/user-credentials.repository'; import { AuthService } from './services/auth.service'; -import { SessionService } from './services/session.service'; -import { TokenManagementService } from './services/token-management.service'; -import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies'; +import { LocalStrategy } from './strategies/local.strategy'; @Module({ imports: [ + JwtModule.register({}), + TypeOrmModule.forFeature([UserCredentials]), + PassportModule, + SessionModule, UserModule, SendgridModule, VerifyModule, - JwtModule.register({}), - TypeOrmModule.forFeature([UserCredentials, Session]), - ], - providers: [ - AuthService, - SessionService, - TokenManagementService, - UserCredentialsRepository, - SessionRepository, - AccessTokenStrategy, - RefreshTokenStrategy, ], + providers: [AuthService, UserCredentialsRepository, LocalStrategy], controllers: [AuthController], - exports: [SessionService], + exports: [], }) export class AuthModule {} diff --git a/backend/src/modules/auth-module/common/decorators/get-user-id.decorator.ts b/backend/src/modules/auth-module/common/decorators/get-user-id.decorator.ts deleted file mode 100644 index 4a82dc5..0000000 --- a/backend/src/modules/auth-module/common/decorators/get-user-id.decorator.ts +++ /dev/null @@ -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; - } -); diff --git a/backend/src/modules/auth-module/common/decorators/get-user.decorator.ts b/backend/src/modules/auth-module/common/decorators/get-user.decorator.ts deleted file mode 100644 index f03ed49..0000000 --- a/backend/src/modules/auth-module/common/decorators/get-user.decorator.ts +++ /dev/null @@ -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]; -// } -// ); diff --git a/backend/src/modules/auth-module/common/decorators/index.ts b/backend/src/modules/auth-module/common/decorators/index.ts index 6e85616..e69de29 100644 --- a/backend/src/modules/auth-module/common/decorators/index.ts +++ b/backend/src/modules/auth-module/common/decorators/index.ts @@ -1,2 +0,0 @@ -export * from './get-user-id.decorator'; -// export * from './get-user.decorator'; diff --git a/backend/src/modules/auth-module/common/guards/access-token.guard.ts b/backend/src/modules/auth-module/common/guards/access-token.guard.ts deleted file mode 100644 index df44b08..0000000 --- a/backend/src/modules/auth-module/common/guards/access-token.guard.ts +++ /dev/null @@ -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 | Observable { - // Check if the current route is marked as public - const isPublic = this.reflector.getAllAndOverride('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); - } -} diff --git a/backend/src/modules/auth-module/common/guards/index.ts b/backend/src/modules/auth-module/common/guards/index.ts deleted file mode 100644 index 50511b9..0000000 --- a/backend/src/modules/auth-module/common/guards/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './access-token.guard'; -export * from './refresh-token.guard'; diff --git a/backend/src/modules/auth-module/common/guards/refresh-token.guard.ts b/backend/src/modules/auth-module/common/guards/refresh-token.guard.ts deleted file mode 100644 index e9b853f..0000000 --- a/backend/src/modules/auth-module/common/guards/refresh-token.guard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AuthGuard } from '@nestjs/passport'; - -export class RefreshTokenGuard extends AuthGuard('jwt-refresh-token') { - public constructor() { - super(); - } -} diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts index c4c96e6..5b0baa2 100644 --- a/backend/src/modules/auth-module/controller/auth.controller.ts +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -1,22 +1,21 @@ import { Controller, Post, + Get, Body, HttpCode, HttpStatus, - Res, Req, + UseGuards, } from '@nestjs/common'; -import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; -import { Response, Request } from 'express'; +import { ApiBody, ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; +import { Request } from 'express'; +import { SessionGuard } from 'src/modules/session/guard'; +import { SuccessDto } from 'src/shared'; import { Public } from 'src/shared/decorator'; -import { GetCurrentUserId } from '../common/decorators'; -import { - AccessTokenDto, - LoginResponseDto, - UserCredentialsDto, -} from '../models/dto'; +import { LocalAuthGuard } from '../guard'; +import { SigninResponseDto, UserCredentialsDto } from '../models/dto'; import { AuthService } from '../services/auth.service'; @ApiTags('Authentication') @@ -26,50 +25,53 @@ export class AuthController { @ApiCreatedResponse({ description: 'User signed up successfully', - type: LoginResponseDto, + type: SuccessDto, }) - @Public() @Post('signup') @HttpCode(HttpStatus.CREATED) + @Public() public async signup( @Body() userCredentials: UserCredentialsDto - ): Promise { + ): Promise { return this.authService.signup(userCredentials); } @ApiCreatedResponse({ description: 'User signin successfully', - type: LoginResponseDto, + type: SigninResponseDto, }) + @ApiBody({ type: UserCredentialsDto }) @HttpCode(HttpStatus.OK) + @UseGuards(LocalAuthGuard) @Public() @Post('signin') - public async signin( - @Res({ passthrough: true }) response: Response, - @Req() request: Request, - @Body() userCredentials: UserCredentialsDto - ): Promise { - return await this.authService.signin(userCredentials, response, request); + public async signin(@Req() request: Request): Promise { + return this.authService.getLoginResponse( + request.user as SigninResponseDto & { userAgent: string } + ); } @ApiCreatedResponse({ - description: 'User tokens refreshed successfully', - type: AccessTokenDto, + description: 'User signed out', }) @HttpCode(HttpStatus.OK) - @Public() - @Post('refresh') - public async refreshToken(@Req() request: Request): Promise { - return await this.authService.refresh(request); + @UseGuards(SessionGuard) + @Post('signout') + public async signout(@Req() request: Request): Promise { + return this.authService.signout(request.sessionID); } @ApiCreatedResponse({ - description: 'User signed out successfully', - type: Boolean, + description: 'Check if user is authenticated', + type: SuccessDto, }) @HttpCode(HttpStatus.OK) - @Post('logout') - public async logout(@GetCurrentUserId() userId: string): Promise { - return this.authService.logout(userId); + @UseGuards(SessionGuard) + @Get('status') + public status(@Req() request: Request): Promise { + return this.authService.checkAuthStatus( + request.sessionID, + request.headers['user-agent'] + ); } } diff --git a/backend/src/modules/auth-module/guard/index.ts b/backend/src/modules/auth-module/guard/index.ts new file mode 100644 index 0000000..cbc8888 --- /dev/null +++ b/backend/src/modules/auth-module/guard/index.ts @@ -0,0 +1 @@ +export * from './local.auth.guard'; diff --git a/backend/src/modules/auth-module/guard/local.auth.guard.ts b/backend/src/modules/auth-module/guard/local.auth.guard.ts new file mode 100644 index 0000000..d82d6d5 --- /dev/null +++ b/backend/src/modules/auth-module/guard/local.auth.guard.ts @@ -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 { + 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; + } +} diff --git a/backend/src/modules/auth-module/models/dto/access-token.dto.ts b/backend/src/modules/auth-module/models/dto/access-token.dto.ts deleted file mode 100644 index 4a8ea9a..0000000 --- a/backend/src/modules/auth-module/models/dto/access-token.dto.ts +++ /dev/null @@ -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; -} diff --git a/backend/src/modules/auth-module/models/dto/index.ts b/backend/src/modules/auth-module/models/dto/index.ts index 4b4a3fc..e0a3cb5 100644 --- a/backend/src/modules/auth-module/models/dto/index.ts +++ b/backend/src/modules/auth-module/models/dto/index.ts @@ -1,3 +1,2 @@ export * from './user-credentials.dto'; -export * from './login-response.dto'; -export * from './access-token.dto'; +export * from './signin-response.dto'; diff --git a/backend/src/modules/auth-module/models/dto/login-response.dto.ts b/backend/src/modules/auth-module/models/dto/signin-response.dto.ts similarity index 63% rename from backend/src/modules/auth-module/models/dto/login-response.dto.ts rename to backend/src/modules/auth-module/models/dto/signin-response.dto.ts index d1a5569..ea21c77 100644 --- a/backend/src/modules/auth-module/models/dto/login-response.dto.ts +++ b/backend/src/modules/auth-module/models/dto/signin-response.dto.ts @@ -1,16 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; -export class LoginResponseDto { - @ApiProperty({ - title: 'Access token', - description: 'Access token', - example: 'eyJhbGci', - }) - @IsNotEmpty() - @IsString() - public access_token: string; - +export class SigninResponseDto { @ApiProperty({ title: 'Email', description: 'User Email', @@ -28,5 +19,5 @@ export class LoginResponseDto { @IsNotEmpty() @IsString() @IsEmail() - public userId: string; + public id: string; } diff --git a/backend/src/modules/auth-module/models/types/index.ts b/backend/src/modules/auth-module/models/types/index.ts index 697195c..e69de29 100644 --- a/backend/src/modules/auth-module/models/types/index.ts +++ b/backend/src/modules/auth-module/models/types/index.ts @@ -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'; diff --git a/backend/src/modules/auth-module/models/types/jwt-payload-with-refresh-token.type.ts b/backend/src/modules/auth-module/models/types/jwt-payload-with-refresh-token.type.ts deleted file mode 100644 index 6b6c80a..0000000 --- a/backend/src/modules/auth-module/models/types/jwt-payload-with-refresh-token.type.ts +++ /dev/null @@ -1,3 +0,0 @@ -// import { JwtPayload } from './jwt-payload.type'; - -// export type JwtPayloadWithRefreshToken = JwtPayload & { refresh_token: string }; diff --git a/backend/src/modules/auth-module/models/types/jwt-payload.type.ts b/backend/src/modules/auth-module/models/types/jwt-payload.type.ts deleted file mode 100644 index dce426b..0000000 --- a/backend/src/modules/auth-module/models/types/jwt-payload.type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type JwtPayload = { - email: string; - sub: number; -}; diff --git a/backend/src/modules/auth-module/models/types/token-payload.type.ts b/backend/src/modules/auth-module/models/types/token-payload.type.ts deleted file mode 100644 index 911ef0d..0000000 --- a/backend/src/modules/auth-module/models/types/token-payload.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type TokenPayload = { - sub: string; - email: string; - iat: number; - exp: number; -}; diff --git a/backend/src/modules/auth-module/models/types/tokens.type.ts b/backend/src/modules/auth-module/models/types/tokens.type.ts deleted file mode 100644 index 1c0a510..0000000 --- a/backend/src/modules/auth-module/models/types/tokens.type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Tokens = { - access_token: string; - refresh_token: string; -}; diff --git a/backend/src/modules/auth-module/repositories/session.repository.ts b/backend/src/modules/auth-module/repositories/session.repository.ts deleted file mode 100644 index b329765..0000000 --- a/backend/src/modules/auth-module/repositories/session.repository.ts +++ /dev/null @@ -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 - ) {} - - public createSession(userId: string, userAgent: string): Promise { - 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 { - 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 { - return this.sessionRepository - .findOne({ - where: { sessionId }, - select: ['userAgent'], - }) - .then((session) => - session ? session.userAgent === currentUserAgent : false - ); - } - - public checkSessionLimit(userId: string): Promise { - 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 { - return this.sessionRepository.delete({ userCredentials: userId }); - } - - public extendSessionExpiration(sessionId: string): Promise { - 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 { - const now = new Date(); - - return this.sessionRepository.delete({ expiresAt: LessThan(now) }); - } -} diff --git a/backend/src/modules/auth-module/repositories/user-credentials.repository.ts b/backend/src/modules/auth-module/repositories/user-credentials.repository.ts index a2bf7f0..11a963b 100644 --- a/backend/src/modules/auth-module/repositories/user-credentials.repository.ts +++ b/backend/src/modules/auth-module/repositories/user-credentials.repository.ts @@ -12,9 +12,9 @@ export class UserCredentialsRepository { public async createUser( email: string, - hash: string + hashedPassword: string ): Promise { - const user = this.repository.create({ email, hash }); + const user = this.repository.create({ email, hashedPassword }); return this.repository.save(user); } @@ -30,13 +30,4 @@ export class UserCredentialsRepository { ): Promise { return this.repository.findOne({ where: { id: userId } }); } - - public async updateUserRefreshToken( - userId: string, - refreshToken: string | null - ): Promise { - const result = await this.repository.update(userId, { refreshToken }); - - return result.affected ?? 0; - } } diff --git a/backend/src/modules/auth-module/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index c27bb3a..442c740 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -1,28 +1,25 @@ -import { ForbiddenException, Injectable } from '@nestjs/common'; -import { Response, Request } from 'express'; -import { Session } from 'src/entities'; -import { EncryptionService } from 'src/shared'; +import { + ConflictException, + ForbiddenException, + 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 { UserDataRepository } from '../../user-module/repositories/user-data.repository'; import { EmailVerificationService } from '../../verify-module/services/email-verification.service'; -import { - AccessTokenDto, - LoginResponseDto, - UserCredentialsDto, -} from '../models/dto'; -import { TokenPayload } from '../models/types'; +import { SigninResponseDto, UserCredentialsDto } from '../models/dto'; import { UserCredentialsRepository } from '../repositories/user-credentials.repository'; -import { SessionService } from './session.service'; -import { TokenManagementService } from './token-management.service'; - @Injectable() export class AuthService { public constructor( private readonly userCredentialsRepository: UserCredentialsRepository, private readonly userDataRepository: UserDataRepository, - private readonly tokenManagementService: TokenManagementService, private readonly passwordConfirmationMailService: PasswordConfirmationMailService, private readonly emailVerificationService: EmailVerificationService, private readonly sessionService: SessionService @@ -30,151 +27,134 @@ export class AuthService { public async signup( userCredentials: UserCredentialsDto - ): Promise { - 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 + ): Promise { + try { + const existingUser = await this.userCredentialsRepository.findUserByEmail( + userCredentials.email ); - await this.passwordConfirmationMailService.sendPasswordConfirmationMail( - user.email, - token - ); + if (existingUser) { + throw new ConflictException('User already exists'); + } - 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( - userCredentials: UserCredentialsDto, - response: Response, - request: Request - ): Promise { - 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 { - const affected = - await this.userCredentialsRepository.updateUserRefreshToken(userId, null); - - await this.sessionService.invalidateAllSessionsForUser(userId); - - return affected > 0; - } - - public async refresh(request: Request): Promise { - 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, + public async validateUser( email: string, - updateRefreshToken: boolean = false - ): Promise { - const tokens = await this.tokenManagementService.generateTokens( - userId, - email - ); + password: string + ): Promise { + try { + const user = await this.userCredentialsRepository.findUserByEmail(email); - if (updateRefreshToken) { - await this.userCredentialsRepository.updateUserRefreshToken( - userId, - tokens.refresh_token + if (!user) { + throw new ForbiddenException('Access Denied'); + } + + 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 { - const user = await this.userCredentialsRepository.findUserById(userId); - - if (!user || !user.refreshToken) { - throw new Error('No refresh token found'); + public async signout(sessionId: string): Promise<{ success: boolean }> { + try { + this.sessionService.deleteSessionBySessionId(sessionId); + return { success: true }; + } catch (error) { + throw new HttpException( + 'Fehler beim Logout', + HttpStatus.INTERNAL_SERVER_ERROR + ); } + } - const decodedToken = await this.tokenManagementService.verifyRefreshToken( - user.refreshToken - ); + public async checkAuthStatus( + sessionId: string, + userAgend: string + ): Promise { + try { + const session = + await this.sessionService.findSessionBySessionId(sessionId); - if (decodedToken.exp < Date.now() / 1000) { - throw new Error('Token expired'); + if (!session) { + 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) { - throw new Error('Token subject mismatch'); - } + public getLoginResponse( + user: SigninResponseDto & { userAgent: string } + ): SigninResponseDto { + const { id, email }: SigninResponseDto = user; + const responseData: SigninResponseDto = { id, email }; - return decodedToken; + return responseData; } } diff --git a/backend/src/modules/auth-module/services/session.service.ts b/backend/src/modules/auth-module/services/session.service.ts deleted file mode 100644 index 6dca4f2..0000000 --- a/backend/src/modules/auth-module/services/session.service.ts +++ /dev/null @@ -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 { - return this.sessionRepository.createSession(userId, userAgent); - } - - public validateSessionUserAgent( - sessionId: string, - currentUserAgent: string - ): Promise { - return this.sessionRepository.validateSessionUserAgent( - sessionId, - currentUserAgent - ); - } - - public checkSessionLimit(userId: string): Promise { - return this.sessionRepository.checkSessionLimit(userId); - } - - public invalidateAllSessionsForUser(userId: string): Promise { - return this.sessionRepository.invalidateAllSessionsForUser(userId); - } - - public clearExpiredSessions(): Promise { - return this.sessionRepository.clearExpiredSessions(); - } - - public extendSessionExpiration(sessionId: string): Promise { - return this.sessionRepository.extendSessionExpiration(sessionId); - } - - public findSessionBySessionId(sessionId: string): Promise { - return this.sessionRepository.findSessionBySessionId(sessionId); - } - - public attachSessionToResponse(response: Response, sessionId: string): void { - this.sessionRepository.attachSessionToResponse(response, sessionId); - } -} diff --git a/backend/src/modules/auth-module/services/token-management.service.ts b/backend/src/modules/auth-module/services/token-management.service.ts deleted file mode 100644 index 6b56f8f..0000000 --- a/backend/src/modules/auth-module/services/token-management.service.ts +++ /dev/null @@ -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( - 'ACCESS_TOKEN_EXPIRY' - ); - this.REFRESH_TOKEN_EXPIRY = this.configService.get( - 'REFRESH_TOKEN_EXPIRY' - ); - this.JWT_SECRET_AT = this.configService.get('JWT_SECRET_AT'); - this.JWT_SECRET_RT = this.configService.get('JWT_SECRET_RT'); - } - - public async generateTokens(userId: string, email: string): Promise { - 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 { - return this.jwt.verifyAsync(token, { - secret: this.JWT_SECRET_RT, - }); - } - - private async createAccessToken( - userId: string, - email: string - ): Promise { - 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 { - return this.jwt.signAsync( - { sub: userId, email }, - { - expiresIn: this.REFRESH_TOKEN_EXPIRY, - secret: this.JWT_SECRET_RT, - } - ); - } -} diff --git a/backend/src/modules/auth-module/strategies/access-token.strategie.ts b/backend/src/modules/auth-module/strategies/access-token.strategie.ts deleted file mode 100644 index 186383a..0000000 --- a/backend/src/modules/auth-module/strategies/access-token.strategie.ts +++ /dev/null @@ -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('JWT_SECRET_AT'), - }; - } - - public async validate(payload: JwtPayload): Promise { - return payload; - } -} diff --git a/backend/src/modules/auth-module/strategies/index.ts b/backend/src/modules/auth-module/strategies/index.ts index c0e13f5..e69de29 100644 --- a/backend/src/modules/auth-module/strategies/index.ts +++ b/backend/src/modules/auth-module/strategies/index.ts @@ -1,2 +0,0 @@ -export * from './access-token.strategie'; -export * from './refresh-token.strategie'; diff --git a/backend/src/modules/auth-module/strategies/local.strategy.ts b/backend/src/modules/auth-module/strategies/local.strategy.ts new file mode 100644 index 0000000..a77367a --- /dev/null +++ b/backend/src/modules/auth-module/strategies/local.strategy.ts @@ -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 { + 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 }; + } +} diff --git a/backend/src/modules/auth-module/strategies/refresh-token.strategie.ts b/backend/src/modules/auth-module/strategies/refresh-token.strategie.ts deleted file mode 100644 index 4afeb7e..0000000 --- a/backend/src/modules/auth-module/strategies/refresh-token.strategie.ts +++ /dev/null @@ -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('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, - }; - } -} diff --git a/backend/src/modules/session/guard/index.ts b/backend/src/modules/session/guard/index.ts new file mode 100644 index 0000000..9fb91c1 --- /dev/null +++ b/backend/src/modules/session/guard/index.ts @@ -0,0 +1 @@ +export * from './session.guard'; diff --git a/backend/src/modules/session/guard/session.guard.ts b/backend/src/modules/session/guard/session.guard.ts new file mode 100644 index 0000000..06e55e0 --- /dev/null +++ b/backend/src/modules/session/guard/session.guard.ts @@ -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 { + 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; + } +} diff --git a/backend/src/modules/session/repository/session.repository.ts b/backend/src/modules/session/repository/session.repository.ts new file mode 100644 index 0000000..8c93c52 --- /dev/null +++ b/backend/src/modules/session/repository/session.repository.ts @@ -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, + private readonly configService: ConfigService + ) {} + + public async findSessionsByUserId(userId: string): Promise { + return await this.repository + .createQueryBuilder('session') + .withDeleted() + .where('session.json ::jsonb @> :jsonFilter', { + jsonFilter: { passport: { user: { id: userId } } }, + }) + .getMany(); + } + + public async findSessionBySessionId( + sessionId: string + ): Promise { + return this.repository.findOne({ where: { id: sessionId } }); + } + + public async deleteAllExpiredSessions(): Promise { + const currentTime = Date.now(); + + await this.repository + .createQueryBuilder() + .delete() + .from(Session) + .where('expiredAt < :currentTime', { currentTime }) + .execute(); + } + + public async deleteAllSessionsForUser(userId: string): Promise { + await this.repository + .createQueryBuilder('session') + .delete() + .where('session.json ::jsonb @> :jsonFilter', { + jsonFilter: { passport: { user: { id: userId } } }, + }); + } + + public async isSessionExpired(session: Session): Promise { + return session.expiredAt < Date.now(); + } + + public async deleteSessionBySessionId(sessionId: string): Promise { + await this.repository.delete(sessionId); + } + + public async enforceSessionLimit(userId: string): Promise { + const sessionLimit = this.configService.get('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); + } + } +} diff --git a/backend/src/modules/session/services/index.ts b/backend/src/modules/session/services/index.ts new file mode 100644 index 0000000..d53e726 --- /dev/null +++ b/backend/src/modules/session/services/index.ts @@ -0,0 +1,2 @@ +export * from './session-init.service'; +export * from './session-serializer.service'; diff --git a/backend/src/modules/session/services/session-init.service.ts b/backend/src/modules/session/services/session-init.service.ts new file mode 100644 index 0000000..9a4fb7b --- /dev/null +++ b/backend/src/modules/session/services/session-init.service.ts @@ -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 { + return session({ + secret: [this.configService.get('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('NODE_ENV') === 'development' + ? false + : true, + + sameSite: + this.configService.get('NODE_ENV') === 'development' + ? 'strict' + : 'none', + }, + }); + } +} diff --git a/backend/src/modules/session/services/session-serializer.service.ts b/backend/src/modules/session/services/session-serializer.service.ts new file mode 100644 index 0000000..9fbf25c --- /dev/null +++ b/backend/src/modules/session/services/session-serializer.service.ts @@ -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); + } +} diff --git a/backend/src/modules/session/services/session.service.ts b/backend/src/modules/session/services/session.service.ts new file mode 100644 index 0000000..0fd0b56 --- /dev/null +++ b/backend/src/modules/session/services/session.service.ts @@ -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 { + return this.sessionRepository.enforceSessionLimit(userId); + } + + public async deleteAllExpiredSessions(): Promise { + return this.sessionRepository.deleteAllExpiredSessions(); + } + + public async findSessionsByUserId(userId: string): Promise { + return this.sessionRepository.findSessionsByUserId(userId); + } + + public async isSessioExpired(session: Session): Promise { + return this.sessionRepository.isSessionExpired(session); + } + + public async deleteSessionBySessionId(sessionId: string): Promise { + return this.sessionRepository.deleteSessionBySessionId(sessionId); + } + + public async findSessionBySessionId( + sessionId: string + ): Promise { + 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; + } + } +} diff --git a/backend/src/modules/session/session.module.ts b/backend/src/modules/session/session.module.ts new file mode 100644 index 0000000..2ef9227 --- /dev/null +++ b/backend/src/modules/session/session.module.ts @@ -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 {} diff --git a/backend/src/modules/user-module/repositories/user-data.repository.ts b/backend/src/modules/user-module/repositories/user-data.repository.ts index 767a91d..11056eb 100644 --- a/backend/src/modules/user-module/repositories/user-data.repository.ts +++ b/backend/src/modules/user-module/repositories/user-data.repository.ts @@ -15,16 +15,16 @@ export class UserDataRepository { ): Promise { const userData = new UserData(); - userData.user = userCredentials; + userData.userCredentials = userCredentials; userData.isEmailConfirmed = false; return this.repository.save(userData); } - public async updateEmailVerificationStatus(userId: string): Promise { - await this.repository.update( - { user: { id: userId } }, - { isEmailConfirmed: true } - ); - } + // public async updateEmailVerificationStatus(userId: string): Promise { + // await this.repository.update( + // { user: { id: userId } }, + // { isEmailConfirmed: true } + // ); + // } } diff --git a/backend/src/modules/verify-module/services/email-verification.service.ts b/backend/src/modules/verify-module/services/email-verification.service.ts index 272b065..288d2ef 100644 --- a/backend/src/modules/verify-module/services/email-verification.service.ts +++ b/backend/src/modules/verify-module/services/email-verification.service.ts @@ -42,9 +42,9 @@ export class EmailVerificationService { await this.deleteEmailVerificationToken(tokenToVerify); if (emailVerification && emailVerification.user) { - await this.userDataRepository.updateEmailVerificationStatus( - emailVerification.user.id - ); + // await this.userDataRepository.updateEmailVerificationStatus( + // emailVerification.user.id + // ); return true; } else { return false; diff --git a/backend/src/shared/index.ts b/backend/src/shared/index.ts index 054a5f9..2eefdce 100644 --- a/backend/src/shared/index.ts +++ b/backend/src/shared/index.ts @@ -1,2 +1,3 @@ export * from './utils/index'; export * from './decorator/index'; +export * from './models/index'; diff --git a/backend/src/shared/models/dto/index.ts b/backend/src/shared/models/dto/index.ts new file mode 100644 index 0000000..8160785 --- /dev/null +++ b/backend/src/shared/models/dto/index.ts @@ -0,0 +1 @@ +export * from './success.dto'; diff --git a/backend/src/shared/models/dto/success.dto.ts b/backend/src/shared/models/dto/success.dto.ts new file mode 100644 index 0000000..bf2b5f1 --- /dev/null +++ b/backend/src/shared/models/dto/success.dto.ts @@ -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; +} diff --git a/backend/src/shared/models/index.ts b/backend/src/shared/models/index.ts new file mode 100644 index 0000000..d58c363 --- /dev/null +++ b/backend/src/shared/models/index.ts @@ -0,0 +1 @@ +export * from './dto/index'; diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 763c832..9b8909b 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,7 +1,6 @@ -import { Component, OnInit } from '@angular/core'; -import { RouterOutlet, Router } from '@angular/router'; +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; -import { AuthService } from './shared/service'; @Component({ selector: 'app-root', standalone: true, @@ -10,24 +9,6 @@ import { AuthService } from './shared/service'; templateUrl: './app.component.html', styleUrl: './app.component.scss', }) -export class AppComponent implements OnInit { - 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'); - } - }); - } +export class AppComponent { + public constructor() {} } diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 9b95e78..f3f5066 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -3,12 +3,18 @@ import { ApplicationConfig } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { Configuration } from './api'; 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 = { providers: [ - provideHttpClient(withInterceptors([AuthInterceptor])), + { provide: Configuration, useValue: apiConfiguration }, + provideHttpClient(withInterceptors([])), provideRouter(routes, withComponentInputBinding()), provideAnimations(), ], diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 2608632..5175263 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,11 +1,12 @@ import { Routes } from '@angular/router'; -import { AuthGuard } from './shared/guard/auth.guard'; - const publicRoutes: Routes = [ { path: '', - loadComponent: () => import('./app.component').then((m) => m.AppComponent), + loadComponent: () => + import('./pages/home-root/home-root.component').then( + (m) => m.HomeComponent + ), }, { path: 'signup', @@ -30,7 +31,7 @@ const protectedRoutes: Routes = [ import('./pages/dashboard-root/dashboard-root.component').then( (m) => m.DashboardRootComponent ), - canActivate: [AuthGuard], + canActivate: [], }, ]; diff --git a/frontend/src/app/config/api-configuration.ts b/frontend/src/app/config/api-configuration.ts new file mode 100644 index 0000000..e99c72b --- /dev/null +++ b/frontend/src/app/config/api-configuration.ts @@ -0,0 +1,9 @@ +import { Configuration, ConfigurationParameters } from '../api'; + +export class ApiConfiguration extends Configuration { + public constructor(params?: Partial) { + super({ + ...params, + }); + } +} diff --git a/frontend/src/app/pages/home-root/home-root.component.html b/frontend/src/app/pages/home-root/home-root.component.html new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/pages/home-root/home-root.component.scss b/frontend/src/app/pages/home-root/home-root.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/pages/home-root/home-root.component.ts b/frontend/src/app/pages/home-root/home-root.component.ts new file mode 100644 index 0000000..535f2c4 --- /dev/null +++ b/frontend/src/app/pages/home-root/home-root.component.ts @@ -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 }, + }); + } + } + ); + } +} diff --git a/frontend/src/app/pages/register-root/register-root.component.html b/frontend/src/app/pages/register-root/register-root.component.html index 0e3bc6d..2c37a02 100644 --- a/frontend/src/app/pages/register-root/register-root.component.html +++ b/frontend/src/app/pages/register-root/register-root.component.html @@ -1,94 +1,110 @@
-

Hi, Welcome to Ticket App.

+ @if (userSignupSuccess()) { +
+

Danke für deine Registrierung!

+

+ Wir haben dir eine Mail geschickt an + {{ form?.get('email')?.value }}. Bitte bestätige deine + E-Mail-Adresse um fortzufahren. +

+

Du kannst diesen Tab nun schließen

+
+ } @else { +
+

Hi, Welcome to Ticket App.

+
+ }
-
-

- @if (isSignupSignal()) { - Anmelden - } @else if (isRegisterSignal()) { - Registrieren - } @else { - Erste Schritte - } -

- @if (isDisplayButtons()) { -
- - -
- } - - @if (isSignupSignal() || isRegisterSignal()) { -
- @if (form) { -
-
-
- -
- -
-
-
- -
- -
- @if (isRegisterSignal()) { -
- -
- } - - -
+ @if (!userSignupSuccess()) { +
+

+ @if (isSignupSignal()) { + Anmelden + } @else if (isRegisterSignal()) { + Registrieren + } @else { + Erste Schritte } -

- } -
+ + + @if (isDisplayButtons()) { +
+ + +
+ } + @if (isSignupSignal() || isRegisterSignal()) { +
+ @if (form) { +
+
+
+ +
+ +
+
+
+ +
+ +
+ @if (isRegisterSignal()) { +
+ +
+ } + + +
+ } +
+ } +
+ }
diff --git a/frontend/src/app/pages/register-root/register-root.component.scss b/frontend/src/app/pages/register-root/register-root.component.scss index 88633cf..ed1f330 100644 --- a/frontend/src/app/pages/register-root/register-root.component.scss +++ b/frontend/src/app/pages/register-root/register-root.component.scss @@ -12,10 +12,20 @@ display: flex; align-items: center; - h1 { - font-size: 4em; - margin-left: 1em; + .success { + margin-left: 4em; + h1 { + font-size: 4em; + } } + + .headline { + h1 { + font-size: 4em; + margin-left: 1em; + } + } + } } diff --git a/frontend/src/app/pages/register-root/register-root.component.ts b/frontend/src/app/pages/register-root/register-root.component.ts index f70ee29..67bddda 100644 --- a/frontend/src/app/pages/register-root/register-root.component.ts +++ b/frontend/src/app/pages/register-root/register-root.component.ts @@ -25,8 +25,14 @@ import { CheckboxModule } from 'primeng/checkbox'; import { InputTextModule } from 'primeng/inputtext'; import { PasswordModule } from 'primeng/password'; -import { AuthService } from '../../shared/service'; -import { LoginCredentials } from '../../shared/types'; +import { + Configuration, + SigninResponseDtoApiModel, + SuccessDtoApiModel, + UserCredentialsDtoApiModel, +} from '../../api'; +import { ApiConfiguration } from '../../config/api-configuration'; +import { AuthService, SessionStorageService } from '../../shared/service'; import { customEmailValidator, customPasswordValidator, @@ -47,13 +53,20 @@ type AuthAction = 'register' | 'signup'; PasswordModule, HttpClientModule, ], - providers: [], + providers: [ + { + provide: Configuration, + useFactory: (): unknown => + new ApiConfiguration({ withCredentials: true }), + }, + ], templateUrl: './register-root.component.html', styleUrl: './register-root.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegisterRootComponent implements OnInit { public verified: InputSignal = input(false); + public login: InputSignal = input(false); public email: InputSignal = input(''); public form: FormGroup | undefined; public isRegisterSignal: WritableSignal = signal(false); @@ -62,12 +75,14 @@ export class RegisterRootComponent implements OnInit { public emailInvalid: WritableSignal = signal(null); public passwordInvalid: WritableSignal = signal(null); public termsInvalid: WritableSignal = signal(null); + public userSignupSuccess: WritableSignal = signal(false); private removeQueryParams: WritableSignal = signal(false); public constructor( private readonly formBuilder: FormBuilder, private readonly authService: AuthService, - private readonly router: Router + private readonly router: Router, + private readonly sessionStorageService: SessionStorageService ) { effect(() => { if (this.form) { @@ -90,13 +105,22 @@ export class RegisterRootComponent implements OnInit { public ngOnInit(): void { this.initializeForm(); this.setupValueChanges(); + this.preselectForm(); - if (this.email() || this.verified()) { + if ((this.email() && this.verified()) || this.login()) { this.handleRedirect(); 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 { if (action === 'register') { this.isRegisterSignal.set(true); @@ -113,7 +137,7 @@ export class RegisterRootComponent implements OnInit { if (this.form?.valid) { if (this.isRegisterSignal()) { - this.register(this.form.value); + this.signup(this.form.value); } else { this.signin(this.form.value); } @@ -150,7 +174,6 @@ export class RegisterRootComponent implements OnInit { } private handleRedirect(): void { - console.log('handleRedirect'); if (this.verified()) { this.isDisplayButtons.set(false); this.isRegisterSignal.set(false); @@ -159,6 +182,12 @@ export class RegisterRootComponent implements OnInit { if (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 { @@ -237,11 +266,23 @@ export class RegisterRootComponent implements OnInit { } } - private signin(logiCredentials: LoginCredentials): void { - this.authService.signin(logiCredentials); + private signin(logiCredentials: UserCredentialsDtoApiModel): void { + this.authService + .signin(logiCredentials) + .subscribe((response: SigninResponseDtoApiModel) => { + if (response) { + this.router.navigate(['/dashboard']); + } + }); } - private register(logiCredentials: LoginCredentials): void { - this.authService.signup(logiCredentials); + private signup(logiCredentials: UserCredentialsDtoApiModel): void { + this.authService + .signup(logiCredentials) + .subscribe((response: SuccessDtoApiModel) => { + if (response.success) { + this.userSignupSuccess.set(true); + } + }); } } diff --git a/frontend/src/app/shared/guard/auth.guard.ts b/frontend/src/app/shared/guard/auth.guard.ts deleted file mode 100644 index 36fb5f3..0000000 --- a/frontend/src/app/shared/guard/auth.guard.ts +++ /dev/null @@ -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 - | Promise - | boolean - | UrlTree => { - const authService: AuthService = inject(AuthService); - const router: Router = inject(Router); - - authService.isAuthenticated$.subscribe((isAuthenticated: boolean) => { - if (!isAuthenticated) { - router.navigateByUrl('signup'); - } - }); - - return true; -}; diff --git a/frontend/src/app/shared/interceptors/auth.interceptor.ts b/frontend/src/app/shared/interceptors/auth.interceptor.ts deleted file mode 100644 index 00d0384..0000000 --- a/frontend/src/app/shared/interceptors/auth.interceptor.ts +++ /dev/null @@ -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, - next: HttpHandlerFn -): Observable> => { - const router = inject(Router); - const authService = inject(AuthService); - - const handleRequest = ( - req: HttpRequest - ): Observable> => { - const accessToken = authService.access_token; - - if (accessToken) { - req = addAuthHeader(req, accessToken); - } - return next(req); - }; - - const addAuthHeader = ( - req: HttpRequest, - token: string - ): HttpRequest => { - return req.clone({ - setHeaders: { - Authorization: `Bearer ${token}`, - }, - }); - }; - - const handle401Error = ( - req: HttpRequest - ): Observable> => { - 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 - ): Observable> => { - if (error.status === 401) { - return handle401Error(req); - } - return throwError(() => new Error('Unhandled error')); - }; - - return handleRequest(request).pipe( - catchError((error) => handleError(error, request)) - ); -}; diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index 381fed3..2f9b247 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -1,12 +1,16 @@ 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 { LoginCredentials, Tokens } from '../types'; -import { LocalStorageService } from './local-storage.service'; -import { SessionStorageService } from './session-storage.service'; +type SuccessResponse = { + success: boolean; +}; @Injectable({ providedIn: 'root', @@ -14,73 +18,28 @@ import { SessionStorageService } from './session-storage.service'; export class AuthService { public isAuthenticated$: BehaviorSubject = new BehaviorSubject(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( - private readonly localStorageService: LocalStorageService, - private readonly sessionStorageService: SessionStorageService, private readonly authenticationApiService: AuthenticationApiService - ) { - this._access_token = - this.localStorageService.getItem('access_token'); - this._refresh_token = - this.sessionStorageService.getItem('refresh_token'); + ) {} + + public signup( + credentials: UserCredentialsDtoApiModel + ): Observable { + return this.authenticationApiService.authControllerSignup(credentials); } - public signin(credentials: LoginCredentials): void { - this.authenticationApiService - .authControllerSignin(credentials) - .subscribe((response: Tokens) => { - this.handleSuccess(response); - }); + public signin( + credentials: UserCredentialsDtoApiModel + ): Observable { + return this.authenticationApiService.authControllerSignin(credentials); } - public signup(credentials: LoginCredentials): void { - this.authenticationApiService - .authControllerSignup(credentials) - .subscribe((response: Tokens) => { - this.handleSuccess(response); - }); + public signout(): Observable { + return this.authenticationApiService.authControllerSignout(); } - public refreshToken(): Observable { - if (this._refresh_token) { - 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); + public status(): Observable { + return this.authenticationApiService.authControllerStatus(); } } diff --git a/frontend/src/app/shared/types/index.ts b/frontend/src/app/shared/types/index.ts index c95db91..e69de29 100644 --- a/frontend/src/app/shared/types/index.ts +++ b/frontend/src/app/shared/types/index.ts @@ -1,2 +0,0 @@ -export * from './login-credentials'; -export * from './tokens'; diff --git a/frontend/src/app/shared/types/login-credentials.ts b/frontend/src/app/shared/types/login-credentials.ts deleted file mode 100644 index e37f277..0000000 --- a/frontend/src/app/shared/types/login-credentials.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type LoginCredentials = { - email: string; - password: string; -}; diff --git a/frontend/src/app/shared/types/tokens.ts b/frontend/src/app/shared/types/tokens.ts deleted file mode 100644 index 1c0a510..0000000 --- a/frontend/src/app/shared/types/tokens.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Tokens = { - access_token: string; - refresh_token: string; -};