Feature / Refactor to session based auth in backend #9

Merged
igorpropisnov merged 7 commits from feature/frontend-dashboard into main 2024-06-02 13:09:16 +02:00
3 changed files with 64 additions and 41 deletions
Showing only changes of commit b5c850d178 - Show all commits

View File

@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Response } from 'express';
import { Session } from 'src/entities'; import { Session } from 'src/entities';
import { Repository } from 'typeorm'; import { LessThan, Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@Injectable() @Injectable()
@ -30,6 +31,21 @@ export class SessionRepository {
return session; return session;
} }
public async findSessionBySessionId(sessionId: string): Promise<Session> {
return await this.sessionRepository.findOne({
where: { sessionId: sessionId },
relations: ['userCredentials'],
});
}
public attachSessionToResponse(response: Response, sessionId: string): void {
response.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
}
public async validateSessionUserAgent( public async validateSessionUserAgent(
sessionId: string, sessionId: string,
currentUserAgent: string currentUserAgent: string
@ -45,4 +61,41 @@ export class SessionRepository {
return session.userAgent === currentUserAgent; return session.userAgent === currentUserAgent;
} }
public async checkSessionLimit(userId: string): Promise<void> {
const userSessions = await this.sessionRepository
.createQueryBuilder('session')
.leftJoinAndSelect('session.userCredentials', 'userCredentials')
.where('userCredentials.id = :userId', { userId })
.orderBy('session.expiresAt', 'ASC')
.getMany();
if (userSessions.length >= 5) {
await this.sessionRepository.delete(userSessions[0].id);
}
}
public async invalidateAllSessionsForUser(userId: string): Promise<void> {
await this.sessionRepository.delete({ userCredentials: userId });
}
public async extendSessionExpiration(sessionId: string): Promise<void> {
const session = await this.sessionRepository.findOne({
where: { sessionId },
});
if (session) {
session.expiresAt = new Date(
session.expiresAt.setMinutes(session.expiresAt.getMinutes() + 30)
);
await this.sessionRepository.save(session);
}
}
// TODO Add cron job to clear expired sessions
public async clearExpiredSessions(): Promise<void> {
const now = new Date();
await this.sessionRepository.delete({ expiresAt: LessThan(now) });
}
} }

View File

@ -88,7 +88,7 @@ export class AuthService {
this.sessionService.attachSessionToResponse(response, sesseionId.sessionId); this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
return this.generateAndPersistTokens(user.id, user.email); return this.generateAndPersistTokens(user.id, user.email, true);
} }
public async logout(userId: string): Promise<boolean> { public async logout(userId: string): Promise<boolean> {

View File

@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Response } from 'express'; import { Response } from 'express';
import { Session } from 'src/entities'; import { Session } from 'src/entities';
import { LessThan, Repository } from 'typeorm';
import { SessionRepository } from '../repositories/session.repository'; import { SessionRepository } from '../repositories/session.repository';
@ -10,76 +9,47 @@ import { SessionRepository } from '../repositories/session.repository';
export class SessionService { export class SessionService {
public constructor( public constructor(
@InjectRepository(Session) @InjectRepository(Session)
private sessionRepository: Repository<Session>, private readonly sessionRepository: SessionRepository
private readonly sessionRepository2: SessionRepository
) {} ) {}
public async createSession( public async createSession(
userId: string, userId: string,
userAgent: string userAgent: string
): Promise<Session> { ): Promise<Session> {
return this.sessionRepository2.createSession(userId, userAgent); return await this.sessionRepository.createSession(userId, userAgent);
} }
public async validateSessionUserAgent( public async validateSessionUserAgent(
sessionId: string, sessionId: string,
currentUserAgent: string currentUserAgent: string
): Promise<boolean> { ): Promise<boolean> {
return this.sessionRepository2.validateSessionUserAgent( return await this.sessionRepository.validateSessionUserAgent(
sessionId, sessionId,
currentUserAgent currentUserAgent
); );
} }
public async checkSessionLimit(userId: string): Promise<void> { public async checkSessionLimit(userId: string): Promise<void> {
const userSessions = await this.sessionRepository await this.sessionRepository.checkSessionLimit(userId);
.createQueryBuilder('session')
.leftJoinAndSelect('session.userCredentials', 'userCredentials')
.where('userCredentials.id = :userId', { userId })
.orderBy('session.expiresAt', 'ASC')
.getMany();
if (userSessions.length >= 5) {
await this.sessionRepository.delete(userSessions[0].id);
}
} }
public async invalidateAllSessionsForUser(userId: string): Promise<void> { public async invalidateAllSessionsForUser(userId: string): Promise<void> {
await this.sessionRepository.delete({ userCredentials: userId }); await this.sessionRepository.invalidateAllSessionsForUser(userId);
} }
// TODO Add cron job to clear expired sessions
public async clearExpiredSessions(): Promise<void> { public async clearExpiredSessions(): Promise<void> {
const now = new Date(); await this.sessionRepository.clearExpiredSessions();
await this.sessionRepository.delete({ expiresAt: LessThan(now) });
} }
public async extendSessionExpiration(sessionId: string): Promise<void> { public async extendSessionExpiration(sessionId: string): Promise<void> {
const session = await this.sessionRepository.findOne({ await this.sessionRepository.extendSessionExpiration(sessionId);
where: { sessionId },
});
if (session) {
session.expiresAt = new Date(
session.expiresAt.setMinutes(session.expiresAt.getMinutes() + 30)
);
await this.sessionRepository.save(session);
}
} }
public async findSessionBySessionId(sessionId: string): Promise<Session> { public async findSessionBySessionId(sessionId: string): Promise<Session> {
return this.sessionRepository.findOne({ return await this.sessionRepository.findSessionBySessionId(sessionId);
where: { sessionId: sessionId },
relations: ['userCredentials'],
});
} }
public attachSessionToResponse(response: Response, sessionId: string): void { public attachSessionToResponse(response: Response, sessionId: string): void {
response.cookie('session_id', sessionId, { this.sessionRepository.attachSessionToResponse(response, sessionId);
httpOnly: true,
secure: true,
sameSite: 'strict',
});
} }
} }