rename entity colum, divide repo and service

This commit is contained in:
Igor Hrenowitsch Propisnov 2024-05-31 08:11:11 +02:00
parent 5769cf4f5a
commit a341196f37
6 changed files with 110 additions and 50 deletions

View File

@ -18,7 +18,7 @@ export class UserCredentials {
public hash: string;
@Column({ nullable: true })
public hashedRt?: string;
public refreshToken?: string;
@CreateDateColumn()
public createdAt: Date;

View File

@ -8,6 +8,7 @@ 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';
@ -27,6 +28,7 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
SessionService,
TokenManagementService,
UserCredentialsRepository,
SessionRepository,
AccessTokenStrategy,
RefreshTokenStrategy,
],

View File

@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Session } from 'src/entities';
import { Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class SessionRepository {
public constructor(
@InjectRepository(Session)
private sessionRepository: Repository<Session>
) {}
public async createSession(
userId: string,
userAgent: string
): Promise<Session> {
const sessionId = uuidv4();
const expirationDate = new Date();
expirationDate.setHours(expirationDate.getHours() + 1);
const session = this.sessionRepository.create({
userCredentials: userId,
sessionId,
expiresAt: expirationDate,
userAgent,
});
await this.sessionRepository.save(session);
return session;
}
public async validateSessionUserAgent(
sessionId: string,
currentUserAgent: string
): Promise<boolean> {
const session = await this.sessionRepository.findOne({
where: { sessionId: sessionId },
select: ['userAgent'],
});
if (!session) {
return false;
}
return session.userAgent === currentUserAgent;
}
}

View File

@ -31,11 +31,11 @@ export class UserCredentialsRepository {
return this.repository.findOne({ where: { id: userId } });
}
public async updateUserTokenHash(
public async updateUserRefreshToken(
userId: string,
hashedRt: string | null
refreshToken: string | null
): Promise<number> {
const result = await this.repository.update(userId, { hashedRt });
const result = await this.repository.update(userId, { refreshToken });
return result.affected ?? 0;
}

View File

@ -79,6 +79,8 @@ export class AuthService {
throw new ForbiddenException('Access Denied');
}
await this.sessionService.checkSessionLimit(user.id);
const sesseionId = await this.sessionService.createSession(
user.id,
request.headers['user-agent']
@ -90,11 +92,10 @@ export class AuthService {
}
public async logout(userId: string): Promise<boolean> {
// TODO Check if the user is logged out already
const affected = await this.userCredentialsRepository.updateUserTokenHash(
userId,
null
);
const affected =
await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
await this.sessionService.invalidateAllSessionsForUser(userId);
return affected > 0;
}
@ -118,19 +119,20 @@ export class AuthService {
request.headers['user-agent']
);
// TODO expand session expiration
if (!isUserAgentValid) {
throw new ForbiddenException('Invalid session - User agent mismatch');
}
await this.sessionService.extendSessionExpiration(sessionId);
const decodedToken: TokenPayload = await this.validateRefreshToken(
session.userCredentials['id']
);
const newTokens = await this.generateAndPersistTokens(
decodedToken.sub,
decodedToken.email
decodedToken.email,
false
);
return { access_token: newTokens.access_token };
@ -138,29 +140,33 @@ export class AuthService {
private async generateAndPersistTokens(
userId: string,
email: string
email: string,
updateRefreshToken: boolean = false
): Promise<LoginResponseDto> {
const tokens = await this.tokenManagementService.generateTokens(
userId,
email
);
await this.userCredentialsRepository.updateUserTokenHash(
userId,
tokens.refresh_token
);
if (updateRefreshToken) {
await this.userCredentialsRepository.updateUserRefreshToken(
userId,
tokens.refresh_token
);
}
return { access_token: tokens.access_token, email: email, userId: userId };
}
private async validateRefreshToken(userId: string): Promise<TokenPayload> {
const user = await this.userCredentialsRepository.findUserById(userId);
if (!user || !user.hashedRt) {
if (!user || !user.refreshToken) {
throw new Error('No refresh token found');
}
const decodedToken = await this.tokenManagementService.verifyRefreshToken(
user.hashedRt
user.refreshToken
);
if (decodedToken.exp < Date.now() / 1000) {
@ -170,6 +176,7 @@ export class AuthService {
if (decodedToken.sub !== user.id) {
throw new Error('Token subject mismatch');
}
return decodedToken;
}
}

View File

@ -3,68 +3,71 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Response } from 'express';
import { Session } from 'src/entities';
import { LessThan, Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { SessionRepository } from '../repositories/session.repository';
@Injectable()
export class SessionService {
public constructor(
@InjectRepository(Session)
private sessionRepository: Repository<Session>
private sessionRepository: Repository<Session>,
private readonly sessionRepository2: SessionRepository
) {}
public async createSession(
userId: string,
userAgent: string
): Promise<Session> {
const sessionId = uuidv4();
const expirationDate = new Date();
expirationDate.setHours(expirationDate.getHours() + 1);
const session = this.sessionRepository.create({
userCredentials: userId,
sessionId: sessionId,
expiresAt: expirationDate,
userAgent: userAgent,
});
await this.sessionRepository.save(session);
return session;
return this.sessionRepository2.createSession(userId, userAgent);
}
public async validateSessionUserAgent(
sessionId: string,
currentUserAgent: string
): Promise<boolean> {
const session = await this.sessionRepository.findOne({
where: { sessionId: sessionId },
select: ['userAgent'],
});
return this.sessionRepository2.validateSessionUserAgent(
sessionId,
currentUserAgent
);
}
if (!session) {
return false;
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);
}
return session.userAgent === currentUserAgent;
}
// TODO use method to invalidate session
public async invalidateSession(sessionId: string): Promise<void> {
await this.sessionRepository.delete({ sessionId: sessionId });
}
// TODO use method to invalidate all sessions for user
public async invalidateAllSessionsForUser(userId: string): Promise<void> {
await this.sessionRepository.delete({ userCredentials: userId });
}
// TODO use method to clear expired sessions
// TODO Add cron job to clear expired sessions
public async clearExpiredSessions(): Promise<void> {
const now = new Date();
await this.sessionRepository.delete({ expiresAt: LessThan(now) });
}
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);
}
}
public async findSessionBySessionId(sessionId: string): Promise<Session> {
return this.sessionRepository.findOne({
where: { sessionId: sessionId },