diff --git a/backend/src/cron/clear-expired-sesstions.cron.ts b/backend/src/cron/clear-expired-sesstions.cron.ts index 8b11f8f..8d5a56e 100644 --- a/backend/src/cron/clear-expired-sesstions.cron.ts +++ b/backend/src/cron/clear-expired-sesstions.cron.ts @@ -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.logger.log('-Cronjob Executed: Delete-Expired-Sessions-'); this.sessionService.deleteAllExpiredSessions(); + this.logger.log('-------------------------------------------'); } } diff --git a/backend/src/modules/auth-module/controller/auth.controller.ts b/backend/src/modules/auth-module/controller/auth.controller.ts index 991279b..a6fd6f4 100644 --- a/backend/src/modules/auth-module/controller/auth.controller.ts +++ b/backend/src/modules/auth-module/controller/auth.controller.ts @@ -41,30 +41,9 @@ export class AuthController { @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); + public async signin(@Req() request: Request): Promise { + return this.authService.getLoginResponse( + request.user as LoginResponseDto & { userAgent: string } + ); } - - // @ApiCreatedResponse({ - // description: 'User tokens refreshed successfully', - // type: AccessTokenDto, - // }) - // @HttpCode(HttpStatus.OK) - // @Public() - // @Post('refresh') - // public async refreshToken(@Req() request: Request): Promise { - // return await this.authService.refresh(request); - // } - - // @ApiCreatedResponse({ - // description: 'User signed out successfully', - // type: Boolean, - // }) - // @HttpCode(HttpStatus.OK) - // @Post('logout') - // public async logout(@GetCurrentUserId() userId: string): Promise { - // return this.authService.logout(userId); - // } } diff --git a/backend/src/modules/auth-module/guard/local.auth.guard.ts b/backend/src/modules/auth-module/guard/local.auth.guard.ts index 099d27b..d82d6d5 100644 --- a/backend/src/modules/auth-module/guard/local.auth.guard.ts +++ b/backend/src/modules/auth-module/guard/local.auth.guard.ts @@ -1,13 +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/login-response.dto.ts b/backend/src/modules/auth-module/models/dto/login-response.dto.ts index 5670e43..6424306 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 @@ -2,15 +2,6 @@ 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; - @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/services/auth.service.ts b/backend/src/modules/auth-module/services/auth.service.ts index 391084f..7d161cc 100644 --- a/backend/src/modules/auth-module/services/auth.service.ts +++ b/backend/src/modules/auth-module/services/auth.service.ts @@ -11,7 +11,7 @@ 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 { UserCredentialsDto } from '../models/dto'; +import { LoginResponseDto, UserCredentialsDto } from '../models/dto'; import { UserCredentialsRepository } from '../repositories/user-credentials.repository'; @Injectable() @@ -73,6 +73,29 @@ export class AuthService { } } + 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 validateUser( email: string, password: string @@ -108,27 +131,13 @@ export class AuthService { } } - 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 getLoginResponse( + user: LoginResponseDto & { userAgent: string } + ): LoginResponseDto { + const { id, email }: LoginResponseDto = user; + const responseData: LoginResponseDto = { id, email }; + + return responseData; } // public async logout(userId: string): Promise { diff --git a/backend/src/modules/auth-module/strategies/local.strategy.ts b/backend/src/modules/auth-module/strategies/local.strategy.ts index f0bafd4..251073d 100644 --- a/backend/src/modules/auth-module/strategies/local.strategy.ts +++ b/backend/src/modules/auth-module/strategies/local.strategy.ts @@ -1,32 +1,34 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; +import { Request } from 'express'; import { Strategy } from 'passport-local'; -import { UserCredentials } from 'src/entities'; -import { SessionService } from 'src/modules/session/services/session.service'; +import { LoginResponseDto } from '../models/dto'; 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 constructor(private readonly authService: AuthService) { + super({ + usernameField: 'email', + passwordField: 'password', + passReqToCallback: true, + }); } public async validate( + request: Request, email: string, password: string - ): Promise { + ): Promise { const user = await this.authService.validateUser(email, password); if (!user) { throw new UnauthorizedException(); } - await this.sessionService.enforceSessionLimit(user.id); + const userAgent = request.headers['user-agent']; - return user; + return { id: user.id, email: user.email, userAgent: userAgent }; } } diff --git a/backend/src/modules/session/repository/session.repository.ts b/backend/src/modules/session/repository/session.repository.ts index 115de1a..f970490 100644 --- a/backend/src/modules/session/repository/session.repository.ts +++ b/backend/src/modules/session/repository/session.repository.ts @@ -10,17 +10,20 @@ export class SessionRepository { private readonly repository: Repository ) {} - public async findSessionByUserId(id: string): Promise { - return this.repository.findOne({ where: { id } }); + public async findSessionsByUserId(userId: string): Promise { + return await this.repository + .createQueryBuilder('session') + .withDeleted() + .where('session.json ::jsonb @> :jsonFilter', { + jsonFilter: { passport: { user: { id: userId } } }, + }) + .getMany(); } - // 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 findSessionBySessionId( + sessionId: string + ): Promise { + return this.repository.findOne({ where: { id: sessionId } }); } public async deleteAllExpiredSessions(): Promise { @@ -34,16 +37,13 @@ export class SessionRepository { .execute(); } - // TODO Fix where() public async deleteAllSessionsForUser(userId: string): Promise { await this.repository - .createQueryBuilder() + .createQueryBuilder('session') .delete() - .from(Session) - .where('json::jsonb -> "passport" -> "user" ->> "id" = :userId', { - userId, - }) - .execute(); + .where('session.json ::jsonb @> :jsonFilter', { + jsonFilter: { passport: { user: { id: userId } } }, + }); } public async enforceSessionLimit(userId: string): Promise { @@ -56,7 +56,7 @@ export class SessionRepository { .orderBy('session.expiredAt', 'ASC') .getMany(); - if (sessions.length >= 5) { + 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/session-serializer.service.ts b/backend/src/modules/session/services/session-serializer.service.ts index 96253c5..9fbf25c 100644 --- a/backend/src/modules/session/services/session-serializer.service.ts +++ b/backend/src/modules/session/services/session-serializer.service.ts @@ -5,10 +5,10 @@ import { UserCredentials } from 'src/entities'; @Injectable() export class SessionSerializerService extends PassportSerializer { public serializeUser( - user: UserCredentials, + user: UserCredentials & { userAgent: string }, done: (err: Error, user: any) => void ): void { - done(null, { id: user.id }); + done(null, { id: user.id, userAgent: user.userAgent }); } public deserializeUser( diff --git a/backend/src/modules/session/services/session.service.ts b/backend/src/modules/session/services/session.service.ts index fec359b..3a92278 100644 --- a/backend/src/modules/session/services/session.service.ts +++ b/backend/src/modules/session/services/session.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { Session } from 'src/entities'; import { UriEncoderService } from 'src/shared'; import { SessionRepository } from '../repository/session.repository'; @@ -15,6 +16,16 @@ export class SessionService { return this.sessionRepository.deleteAllExpiredSessions(); } + public async findSessionsByUserId(userId: string): Promise { + return this.sessionRepository.findSessionsByUserId(userId); + } + + public async findSessionBySessionId( + sessionId: string + ): Promise { + return this.sessionRepository.findSessionBySessionId(sessionId); + } + private extractSessionIdFromCookie(cookie: string): string | null { try { const decodedCookie = UriEncoderService.decodeUri(cookie);