Refactored Auth with Sessions #12

Merged
igorpropisnov merged 9 commits from feature/refactor-auth into main 2024-06-06 12:58:52 +02:00
9 changed files with 87 additions and 88 deletions
Showing only changes of commit bbe444ea5f - Show all commits

View File

@ -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('-------------------------------------------');
}
}

View File

@ -41,30 +41,9 @@ export class AuthController {
@Public()
@UseGuards(LocalAuthGuard)
@Post('signin')
public async signin(@Req() request: Request): Promise<void> {
// console.log('request', userCredentials);
console.log('request', request.user);
//return await this.authService.signin(userCredentials);
public async signin(@Req() request: Request): Promise<LoginResponseDto> {
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<AccessTokenDto> {
// 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<boolean> {
// return this.authService.logout(userId);
// }
}

View File

@ -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<boolean> {
const result = (await super.canActivate(context)) as boolean;
const request = context.switchToHttp().getRequest();
await super.logIn(request);
await this.sessionService.enforceSessionLimit(request.user.id);
return result;
}
}

View File

@ -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;
}

View File

@ -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<void> {
// 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<void> {
// 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<boolean> {

View File

@ -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<UserCredentials> {
): Promise<LoginResponseDto & { userAgent: string }> {
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 };
}
}

View File

@ -10,17 +10,20 @@ export class SessionRepository {
private readonly repository: Repository<Session>
) {}
public async findSessionByUserId(id: string): Promise<Session | undefined> {
return this.repository.findOne({ where: { id } });
public async findSessionsByUserId(userId: string): Promise<Session[]> {
return await this.repository
.createQueryBuilder('session')
.withDeleted()
.where('session.json ::jsonb @> :jsonFilter', {
jsonFilter: { passport: { user: { id: userId } } },
})
.getMany();
}
// TODO Fix select()
public async findUserIdBySessionId(id: string): Promise<string | undefined> {
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<Session | null> {
return this.repository.findOne({ where: { id: sessionId } });
}
public async deleteAllExpiredSessions(): Promise<void> {
@ -34,16 +37,13 @@ export class SessionRepository {
.execute();
}
// TODO Fix where()
public async deleteAllSessionsForUser(userId: string): Promise<void> {
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<void> {
@ -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);

View File

@ -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(

View File

@ -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<Session[]> {
return this.sessionRepository.findSessionsByUserId(userId);
}
public async findSessionBySessionId(
sessionId: string
): Promise<Session | null> {
return this.sessionRepository.findSessionBySessionId(sessionId);
}
private extractSessionIdFromCookie(cookie: string): string | null {
try {
const decodedCookie = UriEncoderService.decodeUri(cookie);