twitch-clone/backend/src/module/auth/session/session.service.ts
2025-07-05 09:47:38 +03:00

147 lines
4.3 KiB
TypeScript

import { PrismaService } from '@/src/core/prisma/prisma.service'
import { RedisService } from '@/src/core/redis/redis.service'
import { LoginInput } from '@/src/module/auth/session/inputs/login.input'
import { VerificationService } from '@/src/module/auth/verification/verification.service'
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
import { destroySession, saveSession } from '@/src/shared/util/session.util'
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { verify } from 'argon2'
import { Request } from 'express'
import { SessionData } from 'express-session'
import { TOTP } from 'otpauth'
@Injectable()
export class SessionService {
constructor(
private readonly prismaService: PrismaService,
private readonly redisService: RedisService,
private readonly configService: ConfigService,
private readonly verificationService: VerificationService,
) {}
public async findByUser(req: Request) {
const userId = req.user?.id
if (!userId) {
throw new NotFoundException('Пользователь не найден')
}
const keys = await this.redisService.keys('*')
const userSessions: Request['session'][] = []
for (const key of keys) {
const sessionData = await this.redisService.get(key)
if (sessionData) {
const session = JSON.parse(sessionData) as Request['session']
if (session.userId === userId) {
userSessions.push({
...session,
id: key.split(':')[1],
} as Request['session'])
}
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- todo fixme
// @ts-expect-error
userSessions.sort((a, b) => b.createdAt - a.createdAt)
return userSessions.filter(session => session.id !== req.session.id)
}
public async findCurrentSession(req: Request) {
const sessionId = req.session.id
const key = `${this.configService.getOrThrow<string>('SESSION_FOLDER')}${sessionId}`
const sessionData = await this.redisService.get(key)
if (!sessionData) {
throw new NotFoundException('Сессия не найдена')
}
const session = JSON.parse(sessionData) as SessionData
return {
...session,
id: sessionId,
}
}
public async login(req: Request, input: LoginInput, userAgent: string) {
const { login, password, pin } = input
const user = await this.prismaService.user.findFirst({ where: {
OR: [
{ name: { equals: login } },
{ email: { equals: login } },
],
} })
if (!user) {
throw new NotFoundException(`Пользователь не найден`)
}
const isValidPassword = await verify(user.password, password)
if (!isValidPassword) {
throw new UnauthorizedException('Логин или пароль неверный')
}
if (!user.isEmailVerified) {
await this.verificationService.sendVerificationToken(user)
throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения')
}
if (user.isTotpEnabled) {
if (!pin) {
return {
message: 'Необходимо ввести пин-код для завершения операции',
}
}
const totp = new TOTP({
issuer: 'TeaStream',
label: user.email,
algorithm: 'SHA-1',
digits: 6,
secret: user.totpSecret!,
})
const delta = totp.validate({ token: pin })
if (delta === null) {
throw new BadRequestException('Невереый код')
}
}
return saveSession(req, user, getSessionMetadata(req, userAgent))
}
public async logout(req: Request) {
return destroySession(req, this.configService)
}
public clearSession(req: Request) {
req.res?.clearCookie(this.configService.getOrThrow('SESSION_NAME'))
return true
}
public async remove(req: Request, id: string) {
if (req.session.id === id) {
throw new ConflictException('Текущую сессию удалить нельзя')
}
const key = `${this.configService.getOrThrow<string>('SESSION_FOLDER')}${id}`
await this.redisService.del(key)
return true
}
}