147 lines
4.3 KiB
TypeScript
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
|
|
}
|
|
}
|