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('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('SESSION_FOLDER')}${id}` await this.redisService.del(key) return true } }