Refactored Auth with Sessions #12
|
@ -8,12 +8,13 @@ export class ClearExpiredSessionsCron {
|
||||||
|
|
||||||
public constructor(private readonly sessionService: SessionService) {}
|
public constructor(private readonly sessionService: SessionService) {}
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, {
|
@Cron(CronExpression.EVERY_12_HOURS, {
|
||||||
name: 'Clear-Expired-Sessions',
|
name: 'Clear-Expired-Sessions',
|
||||||
timeZone: 'Europe/Berlin',
|
timeZone: 'Europe/Berlin',
|
||||||
})
|
})
|
||||||
public handleCron(): void {
|
public handleCron(): void {
|
||||||
this.logger.log('Cronjob Executed: Clear-Expired-Sessions');
|
this.logger.log('-Cronjob Executed: Delete-Expired-Sessions-');
|
||||||
this.sessionService.deleteAllExpiredSessions();
|
this.sessionService.deleteAllExpiredSessions();
|
||||||
|
this.logger.log('-------------------------------------------');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,30 +41,9 @@ export class AuthController {
|
||||||
@Public()
|
@Public()
|
||||||
@UseGuards(LocalAuthGuard)
|
@UseGuards(LocalAuthGuard)
|
||||||
@Post('signin')
|
@Post('signin')
|
||||||
public async signin(@Req() request: Request): Promise<void> {
|
public async signin(@Req() request: Request): Promise<LoginResponseDto> {
|
||||||
// console.log('request', userCredentials);
|
return this.authService.getLoginResponse(
|
||||||
console.log('request', request.user);
|
request.user as LoginResponseDto & { userAgent: string }
|
||||||
//return await this.authService.signin(userCredentials);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @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);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { SessionService } from 'src/modules/session/services/session.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalAuthGuard extends AuthGuard('local') {
|
export class LocalAuthGuard extends AuthGuard('local') {
|
||||||
|
public constructor(private readonly sessionService: SessionService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
public async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const result = (await super.canActivate(context)) as boolean;
|
const result = (await super.canActivate(context)) as boolean;
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
await super.logIn(request);
|
await super.logIn(request);
|
||||||
|
await this.sessionService.enforceSessionLimit(request.user.id);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class LoginResponseDto {
|
export class LoginResponseDto {
|
||||||
@ApiProperty({
|
|
||||||
title: 'Access token',
|
|
||||||
description: 'Access token',
|
|
||||||
example: 'eyJhbGci',
|
|
||||||
})
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
public access_token?: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
title: 'Email',
|
title: 'Email',
|
||||||
description: 'User Email',
|
description: 'User Email',
|
||||||
|
@ -28,5 +19,5 @@ export class LoginResponseDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
public userId: string;
|
public id: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { EncryptionService } from 'src/shared';
|
||||||
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
||||||
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
||||||
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
||||||
import { UserCredentialsDto } from '../models/dto';
|
import { LoginResponseDto, UserCredentialsDto } from '../models/dto';
|
||||||
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
||||||
|
|
||||||
@Injectable()
|
@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(
|
public async validateUser(
|
||||||
email: string,
|
email: string,
|
||||||
password: string
|
password: string
|
||||||
|
@ -108,27 +131,13 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signin(userCredentials: UserCredentialsDto): Promise<void> {
|
public getLoginResponse(
|
||||||
// const user = await this.userCredentialsRepository.findUserByEmail(
|
user: LoginResponseDto & { userAgent: string }
|
||||||
// userCredentials.email
|
): LoginResponseDto {
|
||||||
// );
|
const { id, email }: LoginResponseDto = user;
|
||||||
// if (!user) {
|
const responseData: LoginResponseDto = { id, email };
|
||||||
// throw new ForbiddenException('Access Denied');
|
|
||||||
// }
|
return responseData;
|
||||||
// const passwordMatch = await EncryptionService.compareHash(
|
|
||||||
// userCredentials.password,
|
|
||||||
// user.hash
|
|
||||||
// );
|
|
||||||
// if (!passwordMatch) {
|
|
||||||
// throw new ForbiddenException('Access Denied');
|
|
||||||
// }
|
|
||||||
// await this.sessionService.checkSessionLimit(user.id);
|
|
||||||
// const sesseionId = await this.sessionService.createSession(
|
|
||||||
// user.id,
|
|
||||||
// request.headers['user-agent']
|
|
||||||
// );
|
|
||||||
// this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
|
|
||||||
// return this.generateAndPersistTokens(user.id, user.email, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async logout(userId: string): Promise<boolean> {
|
// public async logout(userId: string): Promise<boolean> {
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Request } from 'express';
|
||||||
import { Strategy } from 'passport-local';
|
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';
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
public constructor(
|
public constructor(private readonly authService: AuthService) {
|
||||||
private readonly authService: AuthService,
|
super({
|
||||||
private readonly sessionService: SessionService
|
usernameField: 'email',
|
||||||
) {
|
passwordField: 'password',
|
||||||
super({ usernameField: 'email', passwordField: 'password' });
|
passReqToCallback: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validate(
|
public async validate(
|
||||||
|
request: Request,
|
||||||
email: string,
|
email: string,
|
||||||
password: string
|
password: string
|
||||||
): Promise<UserCredentials> {
|
): Promise<LoginResponseDto & { userAgent: string }> {
|
||||||
const user = await this.authService.validateUser(email, password);
|
const user = await this.authService.validateUser(email, password);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException();
|
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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,17 +10,20 @@ export class SessionRepository {
|
||||||
private readonly repository: Repository<Session>
|
private readonly repository: Repository<Session>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async findSessionByUserId(id: string): Promise<Session | undefined> {
|
public async findSessionsByUserId(userId: string): Promise<Session[]> {
|
||||||
return this.repository.findOne({ where: { id } });
|
return await this.repository
|
||||||
|
.createQueryBuilder('session')
|
||||||
|
.withDeleted()
|
||||||
|
.where('session.json ::jsonb @> :jsonFilter', {
|
||||||
|
jsonFilter: { passport: { user: { id: userId } } },
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Fix select()
|
public async findSessionBySessionId(
|
||||||
public async findUserIdBySessionId(id: string): Promise<string | undefined> {
|
sessionId: string
|
||||||
return this.repository
|
): Promise<Session | null> {
|
||||||
.createQueryBuilder('session')
|
return this.repository.findOne({ where: { id: sessionId } });
|
||||||
.select('session.json::jsonb -> "passport" -> "user" ->> "id"', 'userId')
|
|
||||||
.where('session.id = :id', { id: id })
|
|
||||||
.getRawOne();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAllExpiredSessions(): Promise<void> {
|
public async deleteAllExpiredSessions(): Promise<void> {
|
||||||
|
@ -34,16 +37,13 @@ export class SessionRepository {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Fix where()
|
|
||||||
public async deleteAllSessionsForUser(userId: string): Promise<void> {
|
public async deleteAllSessionsForUser(userId: string): Promise<void> {
|
||||||
await this.repository
|
await this.repository
|
||||||
.createQueryBuilder()
|
.createQueryBuilder('session')
|
||||||
.delete()
|
.delete()
|
||||||
.from(Session)
|
.where('session.json ::jsonb @> :jsonFilter', {
|
||||||
.where('json::jsonb -> "passport" -> "user" ->> "id" = :userId', {
|
jsonFilter: { passport: { user: { id: userId } } },
|
||||||
userId,
|
});
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enforceSessionLimit(userId: string): Promise<void> {
|
public async enforceSessionLimit(userId: string): Promise<void> {
|
||||||
|
@ -56,7 +56,7 @@ export class SessionRepository {
|
||||||
.orderBy('session.expiredAt', 'ASC')
|
.orderBy('session.expiredAt', 'ASC')
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
if (sessions.length >= 5) {
|
if (sessions.length > 5) {
|
||||||
const sessionsToDelete = sessions.slice(0, sessions.length - 5);
|
const sessionsToDelete = sessions.slice(0, sessions.length - 5);
|
||||||
|
|
||||||
await this.repository.remove(sessionsToDelete);
|
await this.repository.remove(sessionsToDelete);
|
||||||
|
|
|
@ -5,10 +5,10 @@ import { UserCredentials } from 'src/entities';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SessionSerializerService extends PassportSerializer {
|
export class SessionSerializerService extends PassportSerializer {
|
||||||
public serializeUser(
|
public serializeUser(
|
||||||
user: UserCredentials,
|
user: UserCredentials & { userAgent: string },
|
||||||
done: (err: Error, user: any) => void
|
done: (err: Error, user: any) => void
|
||||||
): void {
|
): void {
|
||||||
done(null, { id: user.id });
|
done(null, { id: user.id, userAgent: user.userAgent });
|
||||||
}
|
}
|
||||||
|
|
||||||
public deserializeUser(
|
public deserializeUser(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Session } from 'src/entities';
|
||||||
import { UriEncoderService } from 'src/shared';
|
import { UriEncoderService } from 'src/shared';
|
||||||
|
|
||||||
import { SessionRepository } from '../repository/session.repository';
|
import { SessionRepository } from '../repository/session.repository';
|
||||||
|
@ -15,6 +16,16 @@ export class SessionService {
|
||||||
return this.sessionRepository.deleteAllExpiredSessions();
|
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 {
|
private extractSessionIdFromCookie(cookie: string): string | null {
|
||||||
try {
|
try {
|
||||||
const decodedCookie = UriEncoderService.decodeUri(cookie);
|
const decodedCookie = UriEncoderService.decodeUri(cookie);
|
||||||
|
|
Loading…
Reference in New Issue