rename entity colum, divide repo and service
This commit is contained in:
parent
5769cf4f5a
commit
a341196f37
|
@ -18,7 +18,7 @@ export class UserCredentials {
|
|||
public hash: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public hashedRt?: string;
|
||||
public refreshToken?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
|
|
Loading…
Reference in New Issue