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 c47ea88..61046f8 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { SecurityHeadersMiddleware } from './middleware/security-middleware/secu import { AuthModule } from './modules/auth-module/auth.module'; 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'; @@ -26,6 +27,7 @@ import { VerifyModule } from './modules/verify-module/verify.module'; UserModule, SendgridModule, VerifyModule, + SessionModule, ], controllers: [AppController], providers: [AppService, ClearExpiredSessionsCron], diff --git a/backend/src/cron/clear-expired-sesstions.cron.ts b/backend/src/cron/clear-expired-sesstions.cron.ts index 7681936..8b11f8f 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 { @@ -14,6 +14,6 @@ export class ClearExpiredSessionsCron { }) public handleCron(): void { this.logger.log('Cronjob Executed: Clear-Expired-Sessions'); - this.sessionService.clearExpiredSessions(); + this.sessionService.deleteAllExpiredSessions(); } } 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/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/modules/auth-module/auth.module.ts b/backend/src/modules/auth-module/auth.module.ts index f62de82..de78797 100644 --- a/backend/src/modules/auth-module/auth.module.ts +++ b/backend/src/modules/auth-module/auth.module.ts @@ -1,35 +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 { 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, ], + providers: [AuthService, UserCredentialsRepository, LocalStrategy], controllers: [AuthController], - exports: [SessionService], + exports: [], }) export class AuthModule {} diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts index 03fd70b..991279b 100644 --- a/backend/src/modules/auth-module/controller/auth.controller.ts +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -1,7 +1,17 @@ -import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; +import { + Controller, + Post, + Body, + HttpCode, + HttpStatus, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger'; +import { Request } from 'express'; import { Public } from 'src/shared/decorator'; +import { LocalAuthGuard } from '../guard'; import { LoginResponseDto, UserCredentialsDto } from '../models/dto'; import { AuthService } from '../services/auth.service'; @@ -23,20 +33,19 @@ export class AuthController { return this.authService.signup(userCredentials); } - // @ApiCreatedResponse({ - // description: 'User signin successfully', - // type: LoginResponseDto, - // }) - // @HttpCode(HttpStatus.OK) - // @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); - // } + @ApiCreatedResponse({ + description: 'User signin successfully', + type: LoginResponseDto, + }) + @HttpCode(HttpStatus.OK) + @Public() + @UseGuards(LocalAuthGuard) + @Post('signin') + public async signin(@Req() request: Request): Promise { + // console.log('request', userCredentials); + console.log('request', request.user); + //return await this.authService.signin(userCredentials); + } // @ApiCreatedResponse({ // description: 'User tokens refreshed successfully', 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..099d27b --- /dev/null +++ b/backend/src/modules/auth-module/guard/local.auth.guard.ts @@ -0,0 +1,13 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class LocalAuthGuard extends AuthGuard('local') { + public async canActivate(context: ExecutionContext): Promise { + const result = (await super.canActivate(context)) as boolean; + const request = context.switchToHttp().getRequest(); + + await super.logIn(request); + 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..5c635c8 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'; diff --git a/backend/src/modules/auth-module/models/dto/login-response.dto.ts b/backend/src/modules/auth-module/models/dto/login-response.dto.ts index d1a5569..5670e43 100644 --- a/backend/src/modules/auth-module/models/dto/login-response.dto.ts +++ b/backend/src/modules/auth-module/models/dto/login-response.dto.ts @@ -9,7 +9,7 @@ export class LoginResponseDto { }) @IsNotEmpty() @IsString() - public access_token: string; + public access_token?: string; @ApiProperty({ title: 'Email', 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 4c5ce30..0000000 --- a/backend/src/modules/auth-module/repositories/session.repository.ts +++ /dev/null @@ -1,105 +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 findSessionByUserId(userId: string): Promise { - return this.sessionRepository - .createQueryBuilder('session') - .where('session.userCredentialsId = :userId', { - userId, - }) - .getMany(); - } - - 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/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index 285fed3..391084f 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -1,29 +1,26 @@ import { ConflictException, + ForbiddenException, HttpException, HttpStatus, Injectable, } from '@nestjs/common'; +import { UserCredentials } from 'src/entities'; import { EncryptionService } 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 { LoginResponseDto, UserCredentialsDto } from '../models/dto'; +import { 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 + private readonly emailVerificationService: EmailVerificationService ) {} public async signup( @@ -76,39 +73,63 @@ export class AuthService { } } - // public async signin( - // userCredentials: UserCredentialsDto, - // response: Response, - // request: Request - // ): Promise { - // const user = await this.userCredentialsRepository.findUserByEmail( - // userCredentials.email - // ); + public async validateUser( + email: string, + password: string + ): Promise { + try { + const user = await this.userCredentialsRepository.findUserByEmail(email); - // if (!user) { - // throw new ForbiddenException('Access Denied'); - // } + if (!user) { + throw new ForbiddenException('Access Denied'); + } - // const passwordMatch = await EncryptionService.compareHash( - // userCredentials.password, - // user.hash - // ); + const passwordMatch = await EncryptionService.compareHash( + password, + user.hashedPassword + ); - // if (!passwordMatch) { - // throw new ForbiddenException('Access Denied'); - // } + if (!passwordMatch) { + throw new ForbiddenException('Access Denied'); + } - // await this.sessionService.checkSessionLimit(user.id); + return user; + } catch (error) { + if (error instanceof ForbiddenException) { + throw new ForbiddenException( + 'Die eingebenen Daten sind nicht korrekt. Bitte versuchen Sie es erneut.' + ); + } else { + throw new HttpException( + 'Fehler beim Login', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + } - // 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 signin(userCredentials: UserCredentialsDto): 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 = @@ -157,17 +178,17 @@ export class AuthService { // return { access_token: newTokens.access_token }; // } - private async generateAndPersistTokens( - userId: string, - email: string - ): Promise { - const tokens = await this.tokenManagementService.generateTokens( - userId, - email - ); + // private async generateAndPersistTokens( + // userId: string, + // email: string + // ): Promise { + // const tokens = await this.tokenManagementService.generateTokens( + // userId, + // email + // ); - return { access_token: tokens.access_token, email: email, userId: userId }; - } + // return { access_token: tokens.access_token, email: email, userId: userId }; + // } // private async validateRefreshToken(userId: string): Promise { // const user = await this.userCredentialsRepository.findUserById(userId); 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 efab889..0000000 --- a/backend/src/modules/auth-module/services/session.service.ts +++ /dev/null @@ -1,53 +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 findSessionByUserId(userId: string): Promise { - return this.sessionRepository.findSessionByUserId(userId); - } - - 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 5e10e1c..0000000 --- a/backend/src/modules/auth-module/services/token-management.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { JwtService } from '@nestjs/jwt'; - -import { AccessTokenDto } from '../models/dto'; -import { TokenPayload } from '../models/types'; - -@Injectable() -export class TokenManagementService { - private readonly ACCESS_TOKEN_EXPIRY: string; - private readonly JWT_SECRET_AT: string; - - public constructor( - private readonly jwt: JwtService, - private readonly configService: ConfigService - ) { - this.ACCESS_TOKEN_EXPIRY = this.configService.get( - 'ACCESS_TOKEN_EXPIRY' - ); - this.JWT_SECRET_AT = this.configService.get('JWT_SECRET_AT'); - } - - public async generateTokens( - userId: string, - email: string - ): Promise { - const access_token: string = await this.createAccessToken(userId, email); - - return { access_token }; - } - - public async verifyRefreshToken(token: string): Promise { - return this.jwt.verifyAsync(token, { - secret: this.JWT_SECRET_AT, - }); - } - - 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, - } - ); - } -} 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..f0bafd4 --- /dev/null +++ b/backend/src/modules/auth-module/strategies/local.strategy.ts @@ -0,0 +1,32 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-local'; +import { UserCredentials } from 'src/entities'; +import { SessionService } from 'src/modules/session/services/session.service'; + +import { AuthService } from '../services/auth.service'; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + public constructor( + private readonly authService: AuthService, + private readonly sessionService: SessionService + ) { + super({ usernameField: 'email', passwordField: 'password' }); + } + + public async validate( + email: string, + password: string + ): Promise { + const user = await this.authService.validateUser(email, password); + + if (!user) { + throw new UnauthorizedException(); + } + + await this.sessionService.enforceSessionLimit(user.id); + + return user; + } +} 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..115de1a --- /dev/null +++ b/backend/src/modules/session/repository/session.repository.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +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 + ) {} + + public async findSessionByUserId(id: string): Promise { + return this.repository.findOne({ where: { id } }); + } + + // TODO Fix select() + public async findUserIdBySessionId(id: string): Promise { + return this.repository + .createQueryBuilder('session') + .select('session.json::jsonb -> "passport" -> "user" ->> "id"', 'userId') + .where('session.id = :id', { id: id }) + .getRawOne(); + } + + public async deleteAllExpiredSessions(): Promise { + const currentTime = Date.now(); + + await this.repository + .createQueryBuilder() + .delete() + .from(Session) + .where('expiredAt < :currentTime', { currentTime }) + .execute(); + } + + // TODO Fix where() + public async deleteAllSessionsForUser(userId: string): Promise { + await this.repository + .createQueryBuilder() + .delete() + .from(Session) + .where('json::jsonb -> "passport" -> "user" ->> "id" = :userId', { + userId, + }) + .execute(); + } + + public async enforceSessionLimit(userId: string): Promise { + 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 >= 5) { + const sessionsToDelete = sessions.slice(0, sessions.length - 5); + + 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..80e4315 --- /dev/null +++ b/backend/src/modules/session/services/session-init.service.ts @@ -0,0 +1,37 @@ +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') === 'production' + ? true + : false, + sameSite: 'strict', + }, + }); + } +} 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..96253c5 --- /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, + done: (err: Error, user: any) => void + ): void { + done(null, { id: user.id }); + } + + 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..fec359b --- /dev/null +++ b/backend/src/modules/session/services/session.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +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(); + } + + 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..407c59f --- /dev/null +++ b/backend/src/modules/session/session.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Session } from 'src/entities'; + +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, + SessionService, + ], + controllers: [], + exports: [SessionService], +}) +export class SessionModule {}