diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs index 858c654..ad98d3d 100644 --- a/backend/eslint.config.mjs +++ b/backend/eslint.config.mjs @@ -1,113 +1,108 @@ -// @ts-check -import eslint from '@eslint/js'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; -import stylistic from '@stylistic/eslint-plugin' +import config from 'eslint-config-ksv741'; -export default tseslint.config( +export default [ + ...config, { - ignores: [ - 'node_modules', - 'dist' + name: 'my-rules', + files: [ + '**/*.js', + '**/*.ts', + '**/*.tsx', ], - }, - eslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - stylistic.configs.recommended, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - sourceType: 'commonjs', - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - plugins: { - '@stylistic': stylistic, - } - }, - { rules: { + 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], + 'import/order': [ + 'error', { + pathGroups: [ + { + pattern: '__spec__/**', + group: 'builtin', + position: 'before', + }, + { + pattern: 'apps/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'pages/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'widgets/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'features/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'entities/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'shared/**', + group: 'internal', + position: 'after', + }, + ], + distinctGroup: true, + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'object', + 'index', + 'type', + ], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + 'no-await-in-loop': 'off', + 'no-void': ['error', { allowAsStatement: true }], + // "@typescript-eslint/no-extraneous-class": ['error', {allowEmpty: true}], + // "@typescript-eslint/parameter-properties": ['error', { "allow": ["private readonly"] }], + '@typescript-eslint/max-params': 'off', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + // todo fixme + 'import/max-dependencies': 'off', + '@typescript-eslint/no-unsafe-type-assertion': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + 'import/no-cycle': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', + 'import/extensions': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', + 'max-classes-per-file': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unnecessary-type-conversion': 'off', + '@typescript-eslint/consistent-return': 'off', + '@typescript-eslint/class-methods-use-this': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn' + '@typescript-eslint/member-ordering': 'off', + 'no-undefined': 'off', + '@typescript-eslint/no-misused-spread': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/parameter-properties': 'off', + '@typescript-eslint/no-extraneous-class': 'off', + '@stylistic/max-len': 'off', + '@typescript-eslint/no-unnecessary-type-parameters': 'off', + 'import/no-extraneous-dependencies': 'off', }, }, -); - -// import config from 'eslint-config-ksv741/ts-base' -// -// export default [ -// ...config, -// { -// name: 'my-custom', -// files: [ -// '**/*.js', -// '**/*.ts' -// ], -// rules: { -// 'func-style': ["error", "declaration", { "allowArrowFunctions": true }], -// 'import/order': [ -// 'error', { -// pathGroups: [ -// { -// pattern: '__spec__/**', -// group: 'builtin', -// position: 'before', -// }, -// { -// pattern: 'apps/**', -// group: 'internal', -// position: 'after', -// }, -// { -// pattern: 'pages/**', -// group: 'internal', -// position: 'after', -// }, -// { -// pattern: 'widgets/**', -// group: 'internal', -// position: 'after', -// }, -// { -// pattern: 'features/**', -// group: 'internal', -// position: 'after', -// }, -// { -// pattern: 'entities/**', -// group: 'internal', -// position: 'after', -// }, -// { -// pattern: 'shared/**', -// group: 'internal', -// position: 'after', -// }, -// ], -// distinctGroup: true, -// groups: [ -// 'builtin', -// 'external', -// 'internal', -// 'parent', -// 'sibling', -// 'object', -// 'index', -// 'type', -// ], -// 'newlines-between': 'always', -// alphabetize: { -// order: 'asc', -// caseInsensitive: true, -// }, -// }, -// ], -// } -// } -// ]; +]; diff --git a/backend/package.json b/backend/package.json index c4c30f6..f21f3c5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -66,15 +66,13 @@ "rxjs": "^7.8.1", "sharp": "^0.34.2", "stripe": "^18.3.0", - "telegraf": "^4.16.3" + "telegraf": "^4.16.3", + "uuid": "^11.1.0" }, "devDependencies": { - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.18.0", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", - "@stylistic/eslint-plugin": "^5.0.0", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", "@types/cookie-parser": "^1.4.9", @@ -96,8 +94,7 @@ "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.3", - "typescript-eslint": "^8.20.0" + "typescript": "^5.7.3" }, "jest": { "moduleFileExtensions": [ diff --git a/backend/src/core/config/graphql.config.ts b/backend/src/core/config/graphql.config.ts index 4650fc1..1d7be5f 100644 --- a/backend/src/core/config/graphql.config.ts +++ b/backend/src/core/config/graphql.config.ts @@ -1,14 +1,16 @@ -import { isDev } from '@/src/shared/util/is-dev.util' -import { ApolloDriverConfig } from '@nestjs/apollo' -import { ConfigService } from '@nestjs/config' -import { Request, Response } from 'express' -import * as path from 'node:path' -import { ProcessEnv } from '../../shared/types/env' +import * as path from 'node:path'; + +import { isDev } from '@/src/shared/util/is-dev.util'; + +import type { ProcessEnv } from '../../shared/types/env'; +import type { ApolloDriverConfig } from '@nestjs/apollo'; +import type { ConfigService } from '@nestjs/config'; +import type { Request, Response } from 'express'; type ContextType = { - req: Request - res: Response -} + req: Request; + res: Response; +}; export function getGraphQLConfig(configService: ConfigService): ApolloDriverConfig { return { @@ -18,5 +20,5 @@ export function getGraphQLConfig(configService: ConfigService): Apol sortSchema: true, context: ({ req, res }: ContextType) => ({ req, res }), installSubscriptionHandlers: true, - } + }; } diff --git a/backend/src/core/config/livekit.config.ts b/backend/src/core/config/livekit.config.ts index 2905820..71d0391 100644 --- a/backend/src/core/config/livekit.config.ts +++ b/backend/src/core/config/livekit.config.ts @@ -1,11 +1,11 @@ -import { TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type' -import { ConfigService } from '@nestjs/config' -import { ProcessEnv } from '../../shared/types/env' +import type { ProcessEnv } from '../../shared/types/env'; +import type { TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type'; +import type { ConfigService } from '@nestjs/config'; export function getLiveKitConfig(configService: ConfigService): TypeLiveKitOptions { return { apiSecret: configService.getOrThrow('LIVEKIT_API_SECRET'), apiKey: configService.getOrThrow('LIVEKIT_API_KEY'), apiUrl: configService.getOrThrow('LIVEKIT_URL'), - } + }; } diff --git a/backend/src/core/config/stripe.config.ts b/backend/src/core/config/stripe.config.ts index 511c069..acc9376 100644 --- a/backend/src/core/config/stripe.config.ts +++ b/backend/src/core/config/stripe.config.ts @@ -1,6 +1,6 @@ -import { TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type' -import { ProcessEnv } from '@/src/shared/types/env' -import { ConfigService } from '@nestjs/config' +import type { TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type'; +import type { ProcessEnv } from '@/src/shared/types/env'; +import type { ConfigService } from '@nestjs/config'; export function getStripeConfig(configService: ConfigService): TypeStripeOptions { return { @@ -8,5 +8,5 @@ export function getStripeConfig(configService: ConfigService): TypeS apiVersion: '2025-06-30.basil', }, apiKey: configService.getOrThrow('STRIPE_SECRET_KEY'), - } + }; } diff --git a/backend/src/core/config/telegraf.config.ts b/backend/src/core/config/telegraf.config.ts index e0848c5..a4c07ca 100644 --- a/backend/src/core/config/telegraf.config.ts +++ b/backend/src/core/config/telegraf.config.ts @@ -1,9 +1,9 @@ -import { ConfigService } from '@nestjs/config' -import { TelegrafModuleOptions } from 'nestjs-telegraf' -import { ProcessEnv } from '../../shared/types/env'; +import type { ProcessEnv } from '../../shared/types/env'; +import type { ConfigService } from '@nestjs/config'; +import type { TelegrafModuleOptions } from 'nestjs-telegraf'; export function getTelegrafOptions(configService: ConfigService): TelegrafModuleOptions { return { token: configService.getOrThrow('TELEGRAM_BOT_TOKEN'), - } + }; } diff --git a/backend/src/core/core.module.ts b/backend/src/core/core.module.ts index 4639cd1..55aca0e 100644 --- a/backend/src/core/core.module.ts +++ b/backend/src/core/core.module.ts @@ -1,37 +1,39 @@ -import { getGraphQLConfig } from '@/src/core/config/graphql.config' -import { getLiveKitConfig } from '@/src/core/config/livekit.config' +import { ApolloDriver } from '@nestjs/apollo'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { GraphQLModule } from '@nestjs/graphql'; + +import { getGraphQLConfig } from '@/src/core/config/graphql.config'; +import { getLiveKitConfig } from '@/src/core/config/livekit.config'; import { getStripeConfig } from '@/src/core/config/stripe.config'; -import { AccountModule } from '@/src/module/auth/account/account.module' -import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module' -import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module' -import { ProfileModule } from '@/src/module/auth/profile/profile.module' -import { SessionModule } from '@/src/module/auth/session/session.module' -import { TotpModule } from '@/src/module/auth/totp/totp.module' -import { VerificationModule } from '@/src/module/auth/verification/verification.module' -import { CategoryModule } from '@/src/module/category/category.module' -import { ChannelModule } from '@/src/module/channel/channel.module' -import { ChatModule } from '@/src/module/chat/chat.module' -import { CronModule } from '@/src/module/cron/cron.module' -import { FollowModule } from '@/src/module/follow/follow.module' -import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module' -import { MailModule } from '@/src/module/libs/mail/mail.module' -import { StorageModule } from '@/src/module/libs/storage/storage.module' +import { AccountModule } from '@/src/module/auth/account/account.module'; +import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module'; +import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module'; +import { ProfileModule } from '@/src/module/auth/profile/profile.module'; +import { SessionModule } from '@/src/module/auth/session/session.module'; +import { TotpModule } from '@/src/module/auth/totp/totp.module'; +import { VerificationModule } from '@/src/module/auth/verification/verification.module'; +import { CategoryModule } from '@/src/module/category/category.module'; +import { ChannelModule } from '@/src/module/channel/channel.module'; +import { ChatModule } from '@/src/module/chat/chat.module'; +import { CronModule } from '@/src/module/cron/cron.module'; +import { FollowModule } from '@/src/module/follow/follow.module'; +import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module'; +import { MailModule } from '@/src/module/libs/mail/mail.module'; +import { StorageModule } from '@/src/module/libs/storage/storage.module'; import { StripeModule } from '@/src/module/libs/stripe/stripe.module'; -import { TelegramModule } from '@/src/module/libs/telegram/telegram.module' -import { NotificationModule } from '@/src/module/notification/notification.module' +import { TelegramModule } from '@/src/module/libs/telegram/telegram.module'; +import { NotificationModule } from '@/src/module/notification/notification.module'; import { PlanModule } from '@/src/module/sponsorship/plan/plan.module'; import { SubscriptionModule } from '@/src/module/sponsorship/subscription/subscription.module'; import { TransactionModule } from '@/src/module/sponsorship/transaction/transaction.module'; -import { IngressModule } from '@/src/module/stream/ingress/ingress.module' -import { StreamModule } from '@/src/module/stream/stream.module' -import { WebhookModule } from '@/src/module/webhook/webhook.module' -import { IS_DEV } from '@/src/shared/util/is-dev.util' -import { ApolloDriver } from '@nestjs/apollo' -import { Module } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' -import { GraphQLModule } from '@nestjs/graphql' -import { PrismaModule } from './prisma/prisma.module' -import { RedisModule } from './redis/redis.module' +import { IngressModule } from '@/src/module/stream/ingress/ingress.module'; +import { StreamModule } from '@/src/module/stream/stream.module'; +import { WebhookModule } from '@/src/module/webhook/webhook.module'; +import { IS_DEV } from '@/src/shared/util/is-dev.util'; + +import { PrismaModule } from './prisma/prisma.module'; +import { RedisModule } from './redis/redis.module'; @Module({ imports: [ diff --git a/backend/src/core/prisma/data/categories.data.ts b/backend/src/core/prisma/data/categories.data.ts index 49ef6e9..9a49347 100644 --- a/backend/src/core/prisma/data/categories.data.ts +++ b/backend/src/core/prisma/data/categories.data.ts @@ -125,4 +125,4 @@ export const CATEGORIES = [ 'Погрузитесь в захватывающий мир спорта, где страсть, соревнование и дух командной игры сливаются воедино! Эта категория предназначена для всех поклонников активного образа жизни и спортивных событий, где вы сможете следить за увлекательными матчами, обсуждать стратегии команд и делиться своим опытом с другими фанатами. Присоединяйтесь к стримам, следите за последними новостями и результатами, а также находите единомышленников для совместных тренировок и обсуждений. Откройте для себя удивительный мир спорта, где каждое соревнование — это возможность проявить свои силы и стремление к победе!', thumbnailUrl: '/categories/sport.webp', }, -] +]; diff --git a/backend/src/core/prisma/data/streams.data.ts b/backend/src/core/prisma/data/streams.data.ts index 5009056..cc8d163 100644 --- a/backend/src/core/prisma/data/streams.data.ts +++ b/backend/src/core/prisma/data/streams.data.ts @@ -1,5 +1,5 @@ export const STREAMS = { - 'minecraft': [ + minecraft: [ 'Проходим Minecraft: выживание с нуля!', 'Строим эпические постройки в Minecraft', 'Тайны Майнкрафт: приключения начинаются!', @@ -23,7 +23,7 @@ export const STREAMS = { 'Обзор новых машин и тюнинга в GTA Online', 'Создаем свой бизнес в Лос-Сантосе', ], - 'rust': [ + rust: [ 'Рейдим базы в Rust', 'Выживание в пустоши: Rust', 'Новые механики Rust: тестируем!', @@ -71,7 +71,7 @@ export const STREAMS = { 'Становимся охотниками за головами в RDR2', 'Обзор легендарных зверей и трофеев', ], - 'learning': [ + learning: [ 'Изучаем основы фотографии', 'Как стать мастером ораторского искусства', 'Погружаемся в искусство рисования', @@ -83,7 +83,7 @@ export const STREAMS = { 'Изучаем основы кулинарии: готовим вместе', 'Учим основы финансовой грамотности', ], - 'fortnite': [ + fortnite: [ 'Стрим по Fortnite: королевская битва!', 'Секреты строительства в Fortnite', 'Лучшие тактики для победы в Fortnite', @@ -107,7 +107,7 @@ export const STREAMS = { 'Киберспортивные команды: следим за матчами', 'Участвуем в тренировочных матчах CS:GO', ], - 'programming': [ + programming: [ 'Программируем на JavaScript: от простого к сложному', 'Разработка игр на Python: шаг за шагом', 'Создание веб-приложений с React: практическое руководство', @@ -155,7 +155,7 @@ export const STREAMS = { 'Стримим клановые битвы в Clash Royale', 'Побеждаем в дуэлях и соревнованиях', ], - 'music': [ + music: [ 'Музыкальные новинки: обсуждаем хиты!', 'Слушаем и обсуждаем любимые альбомы', 'Обсуждаем музыкальные жанры: что слушать?', @@ -203,7 +203,7 @@ export const STREAMS = { 'Проходим ранговые игры в League of Legends', 'Обзор новых скинов и событий в League of Legends', ], - 'sport': [ + sport: [ 'Обсуждаем спортивные события: последние новости!', 'Спортивные тренировки: секреты успеха', 'Лучшие моменты в мире спорта', @@ -215,4 +215,4 @@ export const STREAMS = { 'Реакции на последние спортивные трансляции', 'Участвуем в спортивных турнирах и соревнованиях', ], -} +}; diff --git a/backend/src/core/prisma/data/users.data.ts b/backend/src/core/prisma/data/users.data.ts index 84d6d66..97a7ede 100644 --- a/backend/src/core/prisma/data/users.data.ts +++ b/backend/src/core/prisma/data/users.data.ts @@ -101,4 +101,4 @@ export const USERNAMES = [ 'tristan', 'ulysses', 'violet', -] +]; diff --git a/backend/src/core/prisma/prisma.module.ts b/backend/src/core/prisma/prisma.module.ts index 4501415..d30ce68 100644 --- a/backend/src/core/prisma/prisma.module.ts +++ b/backend/src/core/prisma/prisma.module.ts @@ -1,5 +1,6 @@ -import { Global, Module } from '@nestjs/common' -import { PrismaService } from './prisma.service' +import { Global, Module } from '@nestjs/common'; + +import { PrismaService } from './prisma.service'; @Global() @Module({ diff --git a/backend/src/core/prisma/prisma.seed.ts b/backend/src/core/prisma/prisma.seed.ts index f24e3bb..98f8962 100644 --- a/backend/src/core/prisma/prisma.seed.ts +++ b/backend/src/core/prisma/prisma.seed.ts @@ -1,10 +1,11 @@ -import { BadRequestException, Logger } from '@nestjs/common' -import { hash } from 'argon2' -import { Prisma, PrismaClient } from '../../../prisma/generated' +import { BadRequestException, Logger } from '@nestjs/common'; +import { hash } from 'argon2'; -import { CATEGORIES } from './data/categories.data' -import { USERNAMES } from './data/users.data' -import { STREAMS } from './data/streams.data' +import { Prisma, PrismaClient } from '@/prisma/generated'; + +import { CATEGORIES } from './data/categories.data'; +import { STREAMS } from './data/streams.data'; +import { USERNAMES } from './data/users.data'; const prisma = new PrismaClient({ transactionOptions: { @@ -12,25 +13,25 @@ const prisma = new PrismaClient({ timeout: 10000, isolationLevel: Prisma.TransactionIsolationLevel.Serializable, }, -}) +}); async function main() { try { - Logger.log('Начало заполнения базы данных') + Logger.log('Начало заполнения базы данных'); await prisma.$transaction([ prisma.user.deleteMany({}), prisma.socialLink.deleteMany({}), prisma.stream.deleteMany({}), prisma.category.deleteMany({}), - ]) + ]); - await prisma.category.createMany({ data: CATEGORIES }) - Logger.log('Категории успешно созданы') - const categories = await prisma.category.findMany() + await prisma.category.createMany({ data: CATEGORIES }); + Logger.log('Категории успешно созданы'); + const categories = await prisma.category.findMany(); const categoriesBySlug = Object.fromEntries( - categories.map(category => [category.slug, category]), - ) + categories.map((category) => [category.slug, category]), + ); await prisma.$transaction(async (tx) => { for (const name of USERNAMES) { @@ -38,9 +39,9 @@ async function main() { Object.keys(categoriesBySlug)[ Math.floor(Math.random() * Object.keys(categoriesBySlug).length) ] - ] + ]; - const userExists = await tx.user.findUnique({ where: { name } }) + const userExists = await tx.user.findUnique({ where: { name } }); if (!userExists) { const createdUser = await tx.user.create({ data: { @@ -70,10 +71,10 @@ async function main() { // create: {}, // }, }, - }) + }); - const randomTitles = STREAMS[randomCategory.slug] as string - const randomTitle = randomTitles[Math.floor(Math.random() * randomTitles.length)] + const randomTitles = STREAMS[randomCategory.slug] as string; + const randomTitle = randomTitles[Math.floor(Math.random() * randomTitles.length)]; await tx.stream.create({ data: { @@ -90,21 +91,20 @@ async function main() { }, }, }, - }) - Logger.log(`Пользователь "${createdUser.name}" и его стрим успешно созданы`) + }); + Logger.log(`Пользователь "${createdUser.name}" и его стрим успешно созданы`); } } - }) - } - catch (e) { - Logger.error(e) - throw new BadRequestException('Ошибка при заполнении базы данных') - } - finally { - Logger.log('Закрытие соединения с базой данных') - await prisma.$disconnect() - Logger.log('Соединение с базой данныз успешно закрыто') + }); + } catch (e) { + Logger.error(e); + + throw new BadRequestException('Ошибка при заполнении базы данных'); + } finally { + Logger.log('Закрытие соединения с базой данных'); + await prisma.$disconnect(); + Logger.log('Соединение с базой данныз успешно закрыто'); } } -main() +void main(); diff --git a/backend/src/core/prisma/prisma.service.ts b/backend/src/core/prisma/prisma.service.ts index 724a315..678d783 100644 --- a/backend/src/core/prisma/prisma.service.ts +++ b/backend/src/core/prisma/prisma.service.ts @@ -1,13 +1,14 @@ -import { PrismaClient } from '@/prisma/generated' -import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common' +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; + +import { PrismaClient } from '@/prisma/generated'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { async onModuleInit() { - await this.$connect() + await this.$connect(); } async onModuleDestroy() { - await this.$disconnect() + await this.$disconnect(); } } diff --git a/backend/src/core/redis/redis.module.ts b/backend/src/core/redis/redis.module.ts index 63a215d..d23ce09 100644 --- a/backend/src/core/redis/redis.module.ts +++ b/backend/src/core/redis/redis.module.ts @@ -1,5 +1,6 @@ -import { Global, Module } from '@nestjs/common' -import { RedisService } from './redis.service' +import { Global, Module } from '@nestjs/common'; + +import { RedisService } from './redis.service'; @Global() @Module({ diff --git a/backend/src/core/redis/redis.service.ts b/backend/src/core/redis/redis.service.ts index a79fa24..c2b415f 100644 --- a/backend/src/core/redis/redis.service.ts +++ b/backend/src/core/redis/redis.service.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import Redis from 'ioredis' +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Redis from 'ioredis'; + import { ProcessEnv } from '../../shared/types/env'; @Injectable() @@ -8,6 +9,6 @@ export class RedisService extends Redis { constructor( private readonly configService: ConfigService, ) { - super(configService.getOrThrow('REDIS_URI')) + super(configService.getOrThrow('REDIS_URI')); } } diff --git a/backend/src/main.ts b/backend/src/main.ts index 6262e38..399776b 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,27 +1,28 @@ -import { RedisService } from '@/src/core/redis/redis.service' -import { ms, StringValue } from '@/src/shared/util/ms.util' -import { parseBoolean } from '@/src/shared/util/parse-boolean.util' -import { ValidationPipe } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { NestFactory } from '@nestjs/core' -import RedisStore from 'connect-redis' -import * as cookieParser from 'cookie-parser' -import { CoreModule } from '@/src/core/core.module' -import * as session from 'express-session' -import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js' +import { ValidationPipe } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NestFactory } from '@nestjs/core'; +import RedisStore from 'connect-redis'; +import * as cookieParser from 'cookie-parser'; +import * as session from 'express-session'; +import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'; + +import { CoreModule } from '@/src/core/core.module'; +import { RedisService } from '@/src/core/redis/redis.service'; +import { ms } from '@/src/shared/util/ms.util'; +import { parseBoolean } from '@/src/shared/util/parse-boolean.util'; async function bootstrap() { - const app = await NestFactory.create(CoreModule, { rawBody: true }) + const app = await NestFactory.create(CoreModule, { rawBody: true }); - const config = app.get(ConfigService) - const redis = app.get(RedisService) + const config = app.get(ConfigService); + const redis = app.get(RedisService); - app.use(cookieParser(config.getOrThrow('COOKIE_SECRET'))) - app.use(config.getOrThrow('GRAPHQL_PREFIX'), graphqlUploadExpress()) + app.use(cookieParser(config.getOrThrow('COOKIE_SECRET'))); + app.use(config.getOrThrow('GRAPHQL_PREFIX'), graphqlUploadExpress()); app.useGlobalPipes(new ValidationPipe({ transform: true, - })) + })); app.use(session({ secret: config.getOrThrow('SESSION_SECRET'), @@ -39,14 +40,14 @@ async function bootstrap() { client: redis, prefix: config.getOrThrow('SESSION_FOLDER'), }), - })) + })); app.enableCors({ - origin: config.getOrThrow('ALLOWED_ORIGIN'), + origin: config.getOrThrow('ALLOWED_ORIGIN'), credentials: true, exposedHeaders: ['set-cookie'], - }) + }); - await app.listen(config.getOrThrow('APPLICATION_PORT')) + await app.listen(config.getOrThrow('APPLICATION_PORT')); } -void bootstrap() +void bootstrap(); diff --git a/backend/src/module/auth/account/account.module.ts b/backend/src/module/auth/account/account.module.ts index d3c92cd..78e7889 100644 --- a/backend/src/module/auth/account/account.module.ts +++ b/backend/src/module/auth/account/account.module.ts @@ -1,7 +1,9 @@ +import { Module } from '@nestjs/common'; + import { VerificationService } from '@/src/module/auth/verification/verification.service'; -import { Module } from '@nestjs/common' -import { AccountService } from './account.service' -import { AccountResolver } from './account.resolver' + +import { AccountResolver } from './account.resolver'; +import { AccountService } from './account.service'; @Module({ providers: [AccountResolver, AccountService, VerificationService], diff --git a/backend/src/module/auth/account/account.resolver.ts b/backend/src/module/auth/account/account.resolver.ts index 843a4e7..9d22f15 100644 --- a/backend/src/module/auth/account/account.resolver.ts +++ b/backend/src/module/auth/account/account.resolver.ts @@ -1,12 +1,16 @@ -import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input' -import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { User } from '@prisma/generated' -import { CreateUserInput } from './inputs/create-user.input' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { AccountService } from './account.service' -import { UserModel } from './models/user.model' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; + +import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input'; +import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { AccountService } from './account.service'; +import { CreateUserInput } from './inputs/create-user.input'; +import { UserModel } from './models/user.model'; @Resolver('Account') export class AccountResolver { @@ -14,13 +18,13 @@ export class AccountResolver { @Query(() => UserModel, { name: 'findProfile' }) @Authorization() - public me(@Authorized('id') id: UserModel['id']) { - return this.accountService.me(id) + public async me(@Authorized('id') id: UserModel['id']) { + return this.accountService.me(id); } @Mutation(() => UserModel, { name: 'createUser' }) public async create(@Args('data') input: CreateUserInput) { - return this.accountService.create(input) + return this.accountService.create(input); } @Authorization() @@ -29,7 +33,7 @@ export class AccountResolver { @Args('data') input: ChangeEmailInput, @Authorized() user: User, ) { - return this.accountService.changeEmail(user, input) + return this.accountService.changeEmail(user, input); } @Authorization() @@ -38,6 +42,6 @@ export class AccountResolver { @Args('data') input: ChangePasswordInput, @Authorized() user: User, ) { - return this.accountService.changePassword(user, input) + return this.accountService.changePassword(user, input); } } diff --git a/backend/src/module/auth/account/account.service.ts b/backend/src/module/auth/account/account.service.ts index 6597ace..ea16935 100644 --- a/backend/src/module/auth/account/account.service.ts +++ b/backend/src/module/auth/account/account.service.ts @@ -1,11 +1,12 @@ -import { User } from '@/prisma/generated' -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input' -import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input' -import { CreateUserInput } from '@/src/module/auth/account/inputs/create-user.input' -import { VerificationService } from '@/src/module/auth/verification/verification.service' -import { BadRequestException, ConflictException, Injectable } from '@nestjs/common' -import { hash, verify } from 'argon2' +import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; +import { hash, verify } from 'argon2'; + +import { User } from '@/prisma/generated'; +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input'; +import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input'; +import { CreateUserInput } from '@/src/module/auth/account/inputs/create-user.input'; +import { VerificationService } from '@/src/module/auth/verification/verification.service'; @Injectable() export class AccountService { @@ -20,29 +21,29 @@ export class AccountService { where: { id, }, - }) + }); } public async create(input: CreateUserInput) { - const { email, name, password } = input + const { email, name, password } = input; const isUserNameExists = await this.prismaService.user.findUnique({ where: { name, }, - }) + }); if (isUserNameExists) { - throw new ConflictException('Пользователь с таким именем уже существует') + throw new ConflictException('Пользователь с таким именем уже существует'); } const isUserEmailExists = await this.prismaService.user.findUnique({ where: { email, }, - }) + }); if (isUserEmailExists) { - throw new ConflictException('Пользователь с такоей почтой уже существует') + throw new ConflictException('Пользователь с такоей почтой уже существует'); } const user = await this.prismaService.user.create({ @@ -57,15 +58,15 @@ export class AccountService { }, }, }, - }) + }); - await this.verificationService.sendVerificationToken(user) + await this.verificationService.sendVerificationToken(user); - return user + return user; } public async changeEmail(user: User, input: ChangeEmailInput) { - const { email } = input + const { email } = input; return this.prismaService.user.update({ where: { @@ -74,15 +75,15 @@ export class AccountService { data: { email, }, - }) + }); } public async changePassword(user: User, input: ChangePasswordInput) { - const { newPassword, oldPassword } = input + const { newPassword, oldPassword } = input; - const isCorrectPassword = await verify(user.password, oldPassword) + const isCorrectPassword = await verify(user.password, oldPassword); if (!isCorrectPassword) { - throw new BadRequestException('Неверный пароль') + throw new BadRequestException('Неверный пароль'); } return this.prismaService.user.update({ @@ -92,6 +93,6 @@ export class AccountService { data: { password: await hash(newPassword), }, - }) + }); } } diff --git a/backend/src/module/auth/account/inputs/change-email.input.ts b/backend/src/module/auth/account/inputs/change-email.input.ts index c41bef6..351fd6d 100644 --- a/backend/src/module/auth/account/inputs/change-email.input.ts +++ b/backend/src/module/auth/account/inputs/change-email.input.ts @@ -1,5 +1,5 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsEmail, IsNotEmpty, IsString } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; @InputType() export class ChangeEmailInput { @@ -7,5 +7,5 @@ export class ChangeEmailInput { @IsString() @IsNotEmpty() @IsEmail() - email: string + email: string; } diff --git a/backend/src/module/auth/account/inputs/change-password.input.ts b/backend/src/module/auth/account/inputs/change-password.input.ts index d902e80..eedc47e 100644 --- a/backend/src/module/auth/account/inputs/change-password.input.ts +++ b/backend/src/module/auth/account/inputs/change-password.input.ts @@ -1,5 +1,5 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsString, MinLength } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString, MinLength } from 'class-validator'; @InputType() export class ChangePasswordInput { @@ -7,11 +7,11 @@ export class ChangePasswordInput { @IsString() @IsNotEmpty() @MinLength(8) - oldPassword: string + oldPassword: string; @Field(() => String) @IsString() @IsNotEmpty() @MinLength(8) - newPassword: string + newPassword: string; } diff --git a/backend/src/module/auth/account/inputs/create-user.input.ts b/backend/src/module/auth/account/inputs/create-user.input.ts index b1befbb..a6008de 100644 --- a/backend/src/module/auth/account/inputs/create-user.input.ts +++ b/backend/src/module/auth/account/inputs/create-user.input.ts @@ -1,5 +1,7 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsEmail, IsNotEmpty, IsString, Matches, MinLength } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { + IsEmail, IsNotEmpty, IsString, Matches, MinLength, +} from 'class-validator'; @InputType() export class CreateUserInput { @@ -7,17 +9,17 @@ export class CreateUserInput { @IsString() @IsNotEmpty() @Matches(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/) - name: string + name: string; @Field(() => String) @IsString() @IsNotEmpty() @IsEmail() - email: string + email: string; @Field(() => String) @IsString() @IsNotEmpty() @MinLength(8) - password: string + password: string; } diff --git a/backend/src/module/auth/account/models/auth.model.ts b/backend/src/module/auth/account/models/auth.model.ts index 53b635f..0c5f81a 100644 --- a/backend/src/module/auth/account/models/auth.model.ts +++ b/backend/src/module/auth/account/models/auth.model.ts @@ -1,11 +1,12 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { Field, ObjectType } from '@nestjs/graphql' +import { Field, ObjectType } from '@nestjs/graphql'; + +import { UserModel } from './user.model'; @ObjectType() export class AuthModel { @Field(() => UserModel, { nullable: true }) - public user?: UserModel + public user?: UserModel; @Field(() => String, { nullable: true }) - public message: string + public message: string; } diff --git a/backend/src/module/auth/account/models/user.model.ts b/backend/src/module/auth/account/models/user.model.ts index 657a759..4a593e6 100644 --- a/backend/src/module/auth/account/models/user.model.ts +++ b/backend/src/module/auth/account/models/user.model.ts @@ -1,76 +1,77 @@ -import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model' -import { FollowModel } from '@/src/module/follow/model/follow.model' -import { NotificationSettingsModel } from '@/src/module/notification/models/notification-settings.model' -import { NotificationModel } from '@/src/module/notification/models/notification.model' -import { StreamModel } from '@/src/module/stream/models/stream.model' -import { Field, ID, ObjectType } from '@nestjs/graphql' -import { User } from '@prisma/generated' +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { FollowModel } from '@/src/module/follow'; +import { NotificationModel, NotificationSettingsModel } from '@/src/module/notification'; +import { StreamModel } from '@/src/module/stream/models/stream.model'; +import { User } from '@prisma/generated'; + +import { SocialLinkModel } from '../../profile/models/social-link.model'; @ObjectType() export class UserModel implements User { @Field(() => ID) - id: string + id: string; @Field(() => String) - email: string + email: string; @Field(() => String) - password: string + password: string; @Field(() => String) - name: string + name: string; @Field(() => String) - displayName: string + displayName: string; @Field(() => String, { nullable: true }) - avatar: string + avatar: string; @Field(() => String, { nullable: true }) - bio: string + bio: string; @Field(() => Boolean) - isEmailVerified: boolean + isEmailVerified: boolean; @Field(() => Boolean) - isVerified: boolean + isVerified: boolean; @Field(() => Boolean) - isTotpEnabled: boolean + isTotpEnabled: boolean; @Field(() => Boolean) - isDeactivated: boolean + isDeactivated: boolean; @Field(() => Date, { nullable: true }) - deactivatedAt: Date + deactivatedAt: Date; @Field(() => String, { nullable: true }) - totpSecret: string + totpSecret: string; @Field(() => [SocialLinkModel]) - socialLink: SocialLinkModel[] + socialLink: SocialLinkModel[]; @Field(() => StreamModel) - stream: StreamModel + stream: StreamModel; @Field(() => [FollowModel]) - followers: FollowModel[] + followers: FollowModel[]; @Field(() => [FollowModel]) - followings: FollowModel[] + followings: FollowModel[]; @Field(() => String, { nullable: true }) - telegramId: string + telegramId: string; @Field(() => [NotificationModel]) - notification: NotificationModel[] + notification: NotificationModel[]; @Field(() => NotificationSettingsModel) - notificationSettings: NotificationSettingsModel + notificationSettings: NotificationSettingsModel; @Field(() => Date) - createdAt: Date + createdAt: Date; @Field(() => Date) - updatedAt: Date + updatedAt: Date; } diff --git a/backend/src/module/auth/deactivate/deactivate.module.ts b/backend/src/module/auth/deactivate/deactivate.module.ts index f644ce2..8dd53cf 100644 --- a/backend/src/module/auth/deactivate/deactivate.module.ts +++ b/backend/src/module/auth/deactivate/deactivate.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; -import { DeactivateService } from './deactivate.service'; + import { DeactivateResolver } from './deactivate.resolver'; +import { DeactivateService } from './deactivate.service'; @Module({ providers: [DeactivateResolver, DeactivateService], diff --git a/backend/src/module/auth/deactivate/deactivate.resolver.ts b/backend/src/module/auth/deactivate/deactivate.resolver.ts index abae648..35c4fab 100644 --- a/backend/src/module/auth/deactivate/deactivate.resolver.ts +++ b/backend/src/module/auth/deactivate/deactivate.resolver.ts @@ -1,12 +1,16 @@ -import { AuthModel } from '@/src/module/auth/account/models/auth.model' -import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { UserAgent } from '@/src/shared/decorators/user-agent.decorator' -import { GqlContext } from '@/src/shared/types/gql-context.types' -import { Args, Context, Mutation, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { DeactivateService } from './deactivate.service' +import { + Args, Context, Mutation, Resolver, +} from '@nestjs/graphql'; + +import { AuthModel } from '@/src/module/auth/account/models/auth.model'; +import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'; +import { GqlContext } from '@/src/shared/types/gql-context.types'; +import { User } from '@prisma/generated'; + +import { DeactivateService } from './deactivate.service'; @Resolver('Deactivate') export class DeactivateResolver { @@ -20,6 +24,6 @@ export class DeactivateResolver { @UserAgent() userAgent: string, @Authorized() user: User, ) { - return this.deactivateService.deactivate(req, input, user, userAgent) + return this.deactivateService.deactivate(req, input, user, userAgent); } } diff --git a/backend/src/module/auth/deactivate/deactivate.service.ts b/backend/src/module/auth/deactivate/deactivate.service.ts index f8cd031..605ae3e 100644 --- a/backend/src/module/auth/deactivate/deactivate.service.ts +++ b/backend/src/module/auth/deactivate/deactivate.service.ts @@ -1,16 +1,19 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input' -import { MailService } from '@/src/module/libs/mail/mail.service' +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { verify } from 'argon2'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input'; +import { MailService } from '@/src/module/libs/mail/mail.service'; import { TelegramService } from '@/src/module/libs/telegram/telegram.service'; -import { generateToken } from '@/src/shared/util/generate-token.util' -import { getSessionMetadata } from '@/src/shared/util/session-metadata.util' -import { destroySession } from '@/src/shared/util/session.util' -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { TokenType, User } from '@prisma/generated' -import { verify } from 'argon2' -import { Request } from 'express' -import { ProcessEnv } from '../../../shared/types/env' +import { generateToken } from '@/src/shared/util/generate-token.util'; +import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'; +import { destroySession } from '@/src/shared/util/session.util'; +import { TokenType, User } from '@prisma/generated'; + +import { ProcessEnv } from '../../../shared/types/env'; + +import type { Request } from 'express'; @Injectable() export class DeactivateService { @@ -18,66 +21,32 @@ export class DeactivateService { private readonly prismaService: PrismaService, private readonly configService: ConfigService, private readonly mailService: MailService, - private readonly telegramService: TelegramService + private readonly telegramService: TelegramService, ) { } public async deactivate(req: Request, input: DeactivateAccountInput, user: User, userAgent: string) { - const { email, password, pin } = input + const { email, password, pin } = input; if (email !== user.email) { - throw new BadRequestException('Неверная почта') + throw new BadRequestException('Неверная почта'); } - const isValidPassword = await verify(user.password, password) + const isValidPassword = await verify(user.password, password); if (!isValidPassword) { - throw new BadRequestException('Неверный пароль') + throw new BadRequestException('Неверный пароль'); } if (!pin) { - await this.sendDeactivationToken(req, user, userAgent) - return { message: 'Требуется ввести код подтверждения' } + await this.sendDeactivationToken(req, user, userAgent); + + return { message: 'Требуется ввести код подтверждения' }; } - await this.validateDeactivateToken(req, pin) + await this.validateDeactivateToken(req, pin); - return { user } - } - - private async validateDeactivateToken(req: Request, token: string) { - const existingToken = await this.prismaService.token.findUnique({ - where: { token, type: TokenType.DEACTIVATE_ACCOUNT }, - }) - - if (!existingToken) { - throw new NotFoundException('Токен не найден') - } - - const hasExpired = new Date(existingToken.expiresIn) < new Date() - - if (hasExpired) { - throw new BadRequestException('Токен истек') - } - - await this.prismaService.user.update({ - where: { - id: existingToken.userId!, // todo fixme - }, - data: { - isDeactivated: true, - deactivatedAt: new Date(), - }, - }) - - await this.prismaService.token.delete({ - where: { - id: existingToken.id, - type: TokenType.DEACTIVATE_ACCOUNT, - }, - }) - - return destroySession(req, this.configService) + return { user }; } public async sendDeactivationToken(req: Request, user: User, userAgent: string) { @@ -86,14 +55,48 @@ export class DeactivateService { user, TokenType.DEACTIVATE_ACCOUNT, false, - ) + ); - const metadata = getSessionMetadata(req, userAgent) - await this.mailService.sendDeactivateToken(user.email, deactivateToken.token, metadata) + const metadata = getSessionMetadata(req, userAgent); + await this.mailService.sendDeactivateToken(user.email, deactivateToken.token, metadata); - if (deactivateToken.user?.notificationSettings?.telegramNotifications && deactivateToken.user?.telegramId) { - await this.telegramService.sendDeactivateToken(deactivateToken.user.telegramId, deactivateToken.token, metadata) + if (deactivateToken.user?.notificationSettings?.telegramNotifications && deactivateToken.user.telegramId) { + await this.telegramService.sendDeactivateToken(deactivateToken.user.telegramId, deactivateToken.token, metadata); } - return true + + return true; + } + + private async validateDeactivateToken(req: Request, token: string) { + const existingToken = await this.prismaService.token.findUnique({ + where: { token, type: TokenType.DEACTIVATE_ACCOUNT }, + }); + + if (!existingToken?.userId) { + throw new NotFoundException('Токен не найден'); + } + + const hasExpired = new Date(existingToken.expiresIn) < new Date(); + + if (hasExpired) { + throw new BadRequestException('Токен истек'); + } + + await this.prismaService.user.update({ + where: { id: existingToken.userId }, + data: { + isDeactivated: true, + deactivatedAt: new Date(), + }, + }); + + await this.prismaService.token.delete({ + where: { + id: existingToken.id, + type: TokenType.DEACTIVATE_ACCOUNT, + }, + }); + + return destroySession(req, this.configService); } } diff --git a/backend/src/module/auth/deactivate/inputs/deactivate-account.input.ts b/backend/src/module/auth/deactivate/inputs/deactivate-account.input.ts index 0cc32b4..f7edf0a 100644 --- a/backend/src/module/auth/deactivate/inputs/deactivate-account.input.ts +++ b/backend/src/module/auth/deactivate/inputs/deactivate-account.input.ts @@ -1,5 +1,7 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsEmail, IsNotEmpty, IsOptional, IsString, Length, MinLength } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { + IsEmail, IsNotEmpty, IsOptional, IsString, Length, MinLength, +} from 'class-validator'; @InputType() export class DeactivateAccountInput { @@ -7,18 +9,18 @@ export class DeactivateAccountInput { @IsString() @IsNotEmpty() @IsEmail() - email: string + email: string; @Field(() => String) @IsString() @IsNotEmpty() @MinLength(8) - password: string + password: string; @Field(() => String, { nullable: true }) @IsString() @IsNotEmpty() @IsOptional() @Length(6, 6) - pin: string + pin: string; } diff --git a/backend/src/module/auth/index.ts b/backend/src/module/auth/index.ts new file mode 100644 index 0000000..b2190d3 --- /dev/null +++ b/backend/src/module/auth/index.ts @@ -0,0 +1,7 @@ +export { TotpModel } from './totp/models/totp.model'; +export { UserModel } from './account/models/user.model'; +export { AuthModel } from './account/models/auth.model'; +export { + DeviceModel, LocationModel, SessionMetadataModel, SessionModel, +} from './session/models/session.model'; +export { NewPasswordInput } from './password-recovery/inputs/new-password.input'; diff --git a/backend/src/module/auth/password-recovery/inputs/new-password.input.ts b/backend/src/module/auth/password-recovery/inputs/new-password.input.ts index 81b34ae..6026c29 100644 --- a/backend/src/module/auth/password-recovery/inputs/new-password.input.ts +++ b/backend/src/module/auth/password-recovery/inputs/new-password.input.ts @@ -1,8 +1,11 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { + IsNotEmpty, IsString, IsUUID, MinLength, Validate, +} from 'class-validator'; + import { IsPasswordMatchingConstraintDecorator, -} from '@/src/shared/decorators/is-password-matching-constraint.decorator' -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsString, IsUUID, MinLength, Validate } from 'class-validator' +} from '@/src/shared/decorators/is-password-matching-constraint.decorator'; @InputType() export class NewPasswordInput { @@ -10,17 +13,17 @@ export class NewPasswordInput { @IsString() @IsNotEmpty() @MinLength(8) - password: string + password: string; @Field(() => String) @IsString() @IsNotEmpty() @MinLength(8) @Validate(IsPasswordMatchingConstraintDecorator) - passwordRepeat: string + passwordRepeat: string; @Field(() => String) @IsUUID('4') @IsNotEmpty() - token: string + token: string; } diff --git a/backend/src/module/auth/password-recovery/inputs/reset-password.input.ts b/backend/src/module/auth/password-recovery/inputs/reset-password.input.ts index 4fd9f22..edd6619 100644 --- a/backend/src/module/auth/password-recovery/inputs/reset-password.input.ts +++ b/backend/src/module/auth/password-recovery/inputs/reset-password.input.ts @@ -3,9 +3,8 @@ import { IsEmail, IsNotEmpty } from 'class-validator'; @InputType() export class ResetPasswordInput { - @Field(() => String) @IsNotEmpty() @IsEmail() - email: string + email: string; } diff --git a/backend/src/module/auth/password-recovery/password-recovery.module.ts b/backend/src/module/auth/password-recovery/password-recovery.module.ts index 15b21de..740e319 100644 --- a/backend/src/module/auth/password-recovery/password-recovery.module.ts +++ b/backend/src/module/auth/password-recovery/password-recovery.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common' -import { PasswordRecoveryService } from './password-recovery.service' -import { PasswordRecoveryResolver } from './password-recovery.resolver' +import { Module } from '@nestjs/common'; + +import { PasswordRecoveryResolver } from './password-recovery.resolver'; +import { PasswordRecoveryService } from './password-recovery.service'; @Module({ providers: [PasswordRecoveryResolver, PasswordRecoveryService], diff --git a/backend/src/module/auth/password-recovery/password-recovery.resolver.ts b/backend/src/module/auth/password-recovery/password-recovery.resolver.ts index 0300f5b..5dda4d7 100644 --- a/backend/src/module/auth/password-recovery/password-recovery.resolver.ts +++ b/backend/src/module/auth/password-recovery/password-recovery.resolver.ts @@ -1,9 +1,13 @@ -import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input' -import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input' -import { UserAgent } from '@/src/shared/decorators/user-agent.decorator' -import { GqlContext } from '@/src/shared/types/gql-context.types' -import { Args, Context, Mutation, Resolver } from '@nestjs/graphql' -import { PasswordRecoveryService } from './password-recovery.service' +import { + Args, Context, Mutation, Resolver, +} from '@nestjs/graphql'; + +import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'; +import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input'; +import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'; +import { GqlContext } from '@/src/shared/types/gql-context.types'; + +import { PasswordRecoveryService } from './password-recovery.service'; @Resolver('PasswordRecovery') export class PasswordRecoveryResolver { @@ -15,11 +19,11 @@ export class PasswordRecoveryResolver { @Args('data') input: ResetPasswordInput, @UserAgent() userAgent: string, ) { - return this.passwordRecoveryService.resetPassword(req, input, userAgent) + return this.passwordRecoveryService.resetPassword(req, input, userAgent); } @Mutation(() => Boolean, { name: 'setNewPassword' }) public async setNewPassword(@Args('data') input: NewPasswordInput) { - return this.passwordRecoveryService.setNewPassword(input) + return this.passwordRecoveryService.setNewPassword(input); } } diff --git a/backend/src/module/auth/password-recovery/password-recovery.service.ts b/backend/src/module/auth/password-recovery/password-recovery.service.ts index 44401e3..df971a8 100644 --- a/backend/src/module/auth/password-recovery/password-recovery.service.ts +++ b/backend/src/module/auth/password-recovery/password-recovery.service.ts @@ -1,59 +1,61 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input' -import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input' -import { MailService } from '@/src/module/libs/mail/mail.service' +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { hash } from 'argon2'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'; +import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input'; +import { MailService } from '@/src/module/libs/mail/mail.service'; import { TelegramService } from '@/src/module/libs/telegram/telegram.service'; -import { generateToken } from '@/src/shared/util/generate-token.util' -import { getSessionMetadata } from '@/src/shared/util/session-metadata.util' -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' -import { TokenType } from '@prisma/generated' -import { hash } from 'argon2' -import { Request } from 'express' +import { generateToken } from '@/src/shared/util/generate-token.util'; +import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'; +import { TokenType } from '@prisma/generated'; + +import type { Request } from 'express'; @Injectable() export class PasswordRecoveryService { constructor( private readonly prismaService: PrismaService, private readonly mailService: MailService, - private readonly telegramService: TelegramService + private readonly telegramService: TelegramService, ) { } public async resetPassword(req: Request, input: ResetPasswordInput, userAgent: string) { - const { email } = input + const { email } = input; const user = await this.prismaService.user.findFirst({ where: { email }, include: { notificationSettings: true }, - }) + }); if (!user) { - throw new NotFoundException('Пользователь с такой почтой не найден') + throw new NotFoundException('Пользователь с такой почтой не найден'); } - const resetToken = await generateToken(this.prismaService, user, TokenType.PASSWORD_RESET) - const metadata = getSessionMetadata(req, userAgent) - await this.mailService.sendPasswordResetToken(user.email, resetToken.token, metadata) + const resetToken = await generateToken(this.prismaService, user, TokenType.PASSWORD_RESET); + const metadata = getSessionMetadata(req, userAgent); + await this.mailService.sendPasswordResetToken(user.email, resetToken.token, metadata); if (resetToken.user?.notificationSettings?.telegramNotifications && resetToken?.user.telegramId) { - await this.telegramService.sendPasswordResetToken(resetToken.user.telegramId, resetToken.token, metadata) + await this.telegramService.sendPasswordResetToken(resetToken.user.telegramId, resetToken.token, metadata); } - return true + return true; } public async setNewPassword(input: NewPasswordInput) { - const { password, token } = input + const { password, token } = input; const existingToken = await this.prismaService.token.findUnique({ where: { token, type: TokenType.PASSWORD_RESET }, - }) + }); if (!existingToken) { - throw new NotFoundException('Токен не найден') + throw new NotFoundException('Токен не найден'); } - const hasExpired = new Date(existingToken.expiresIn) < new Date() + const hasExpired = new Date(existingToken.expiresIn) < new Date(); if (hasExpired) { - throw new BadRequestException('Токен истек') + throw new BadRequestException('Токен истек'); } await this.prismaService.user.update({ @@ -63,15 +65,15 @@ export class PasswordRecoveryService { data: { password: await hash(password), }, - }) + }); await this.prismaService.token.delete({ where: { id: existingToken.id, type: TokenType.PASSWORD_RESET, }, - }) + }); - return true + return true; } } diff --git a/backend/src/module/auth/profile/inputs/change-profile-info.input.ts b/backend/src/module/auth/profile/inputs/change-profile-info.input.ts index 689c086..94747a5 100644 --- a/backend/src/module/auth/profile/inputs/change-profile-info.input.ts +++ b/backend/src/module/auth/profile/inputs/change-profile-info.input.ts @@ -1,5 +1,7 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsString, Matches, MaxLength } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { + IsNotEmpty, IsString, Matches, MaxLength, +} from 'class-validator'; @InputType() export class ChangeProfileInfoInput { @@ -7,16 +9,16 @@ export class ChangeProfileInfoInput { @IsString() @IsNotEmpty() @Matches(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/) - name: string + name: string; @Field(() => String) @IsString() @IsNotEmpty() - displayName: string + displayName: string; @Field(() => String) @IsString() @IsNotEmpty() @MaxLength(300) - bio: string + bio: string; } diff --git a/backend/src/module/auth/profile/inputs/models/social-link.model.ts b/backend/src/module/auth/profile/inputs/models/social-link.model.ts index b9b2b78..84ff799 100644 --- a/backend/src/module/auth/profile/inputs/models/social-link.model.ts +++ b/backend/src/module/auth/profile/inputs/models/social-link.model.ts @@ -1,27 +1,28 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { Field, ID, ObjectType } from '@nestjs/graphql' -import { SocialLink } from '@prisma/generated' +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { UserModel } from '@/src/module/auth/account/models/user.model'; +import { SocialLink } from '@prisma/generated'; @ObjectType() export class SocialLinkModel implements SocialLink { @Field(() => ID) - id: string + id: string; @Field(() => ID) - userId: UserModel['id'] + userId: UserModel['id']; @Field(() => String) - title: string + title: string; @Field(() => String) - url: string + url: string; @Field(() => Number) - position: number + position: number; @Field(() => Date) - updatedAt: Date + updatedAt: Date; @Field(() => Date) - createdAt: Date + createdAt: Date; } diff --git a/backend/src/module/auth/profile/inputs/social-link.input.ts b/backend/src/module/auth/profile/inputs/social-link.input.ts index 037c479..8090241 100644 --- a/backend/src/module/auth/profile/inputs/social-link.input.ts +++ b/backend/src/module/auth/profile/inputs/social-link.input.ts @@ -1,18 +1,20 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsNumber, IsString, IsUrl } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { + IsNotEmpty, IsNumber, IsString, IsUrl, +} from 'class-validator'; @InputType() export class SocialLinkInput { @Field(() => String) @IsString() @IsNotEmpty() - title: string + title: string; @Field(() => String) @IsString() @IsNotEmpty() @IsUrl() - url: string + url: string; } @InputType() @@ -20,10 +22,10 @@ export class SocialLinkOrderInput { @Field(() => String) @IsString() @IsNotEmpty() - id: string + id: string; @Field(() => Number) @IsNumber() @IsNotEmpty() - position: number + position: number; } diff --git a/backend/src/module/auth/profile/models/social-link.model.ts b/backend/src/module/auth/profile/models/social-link.model.ts new file mode 100644 index 0000000..ef74100 --- /dev/null +++ b/backend/src/module/auth/profile/models/social-link.model.ts @@ -0,0 +1,29 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { SocialLink } from '@prisma/generated'; + +import { UserModel } from '../../account/models/user.model'; + +@ObjectType() +export class SocialLinkModel implements SocialLink { + @Field(() => ID) + id: string; + + @Field(() => ID) + userId: UserModel['id']; + + @Field(() => String) + title: string; + + @Field(() => String) + url: string; + + @Field(() => Number) + position: number; + + @Field(() => Date) + updatedAt: Date; + + @Field(() => Date) + createdAt: Date; +} diff --git a/backend/src/module/auth/profile/profile.module.ts b/backend/src/module/auth/profile/profile.module.ts index 3f7850c..afef471 100644 --- a/backend/src/module/auth/profile/profile.module.ts +++ b/backend/src/module/auth/profile/profile.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common' -import { ProfileService } from './profile.service' -import { ProfileResolver } from './profile.resolver' +import { Module } from '@nestjs/common'; + +import { ProfileResolver } from './profile.resolver'; +import { ProfileService } from './profile.service'; @Module({ providers: [ProfileResolver, ProfileService], diff --git a/backend/src/module/auth/profile/profile.resolver.ts b/backend/src/module/auth/profile/profile.resolver.ts index f674580..9549d6f 100644 --- a/backend/src/module/auth/profile/profile.resolver.ts +++ b/backend/src/module/auth/profile/profile.resolver.ts @@ -1,18 +1,22 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input' -import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; +import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js'; +import * as Upload from 'graphql-upload/Upload.js'; + +import { User } from '@/prisma/generated'; +import { UserModel } from '@/src/module/auth/account/models/user.model'; +import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'; import { SocialLinkInput, SocialLinkOrderInput, -} from '@/src/module/auth/profile/inputs/social-link.input' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { ProfileService } from './profile.service' -import { User } from '@/prisma/generated' -import * as Upload from 'graphql-upload/Upload.js' -import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js' +} from '@/src/module/auth/profile/inputs/social-link.input'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe'; + +import { SocialLinkModel } from './models/social-link.model'; +import { ProfileService } from './profile.service'; @Resolver('Profile') export class ProfileResolver { @@ -24,13 +28,13 @@ export class ProfileResolver { @Authorized() user: User, @Args('avatar', { type: () => GraphQLUpload }, FileValidationPipe) file: Upload, ) { - return this.profileService.changeAvatar(user, file) + return this.profileService.changeAvatar(user, file); } @Authorization() @Mutation(() => Boolean, { name: 'removeProfileAvatar' }) public async removeAvatar(@Authorized() user: User) { - return this.profileService.removeAvatar(user) + return this.profileService.removeAvatar(user); } @Authorization() @@ -39,7 +43,7 @@ export class ProfileResolver { @Authorized() user: User, @Args('data') input: ChangeProfileInfoInput, ) { - return this.profileService.changeInfo(user, input) + return this.profileService.changeInfo(user, input); } @Authorization() @@ -48,7 +52,7 @@ export class ProfileResolver { @Authorized() user: User, @Args('data') input: SocialLinkInput, ) { - return this.profileService.createSocialLink(user, input) + return this.profileService.createSocialLink(user, input); } @Authorization() @@ -56,7 +60,7 @@ export class ProfileResolver { public async reorderSocialLinks( @Args('list', { type: () => [SocialLinkOrderInput] }) list: SocialLinkOrderInput[], ) { - return this.profileService.reorderSocialLinks(list) + return this.profileService.reorderSocialLinks(list); } @Authorization() @@ -65,7 +69,7 @@ export class ProfileResolver { @Args('id') id: string, @Args('data') input: SocialLinkInput, ) { - return this.profileService.updateSocialLink(id, input) + return this.profileService.updateSocialLink(id, input); } @Authorization() @@ -73,12 +77,12 @@ export class ProfileResolver { public async removeSocialLink( @Args('id') id: string, ) { - return this.profileService.removeSocialLink(id) + return this.profileService.removeSocialLink(id); } @Authorization() @Query(() => [SocialLinkModel], { name: 'findSocialLinks' }) public async findSocialLink(@Authorized() user: User) { - return this.profileService.findSocialLink(user) + return this.profileService.findSocialLink(user); } } diff --git a/backend/src/module/auth/profile/profile.service.ts b/backend/src/module/auth/profile/profile.service.ts index 44937de..5714a10 100644 --- a/backend/src/module/auth/profile/profile.service.ts +++ b/backend/src/module/auth/profile/profile.service.ts @@ -1,14 +1,16 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input' +import { ConflictException, Injectable } from '@nestjs/common'; +import * as Upload from 'graphql-upload/Upload.js'; +import sharp from 'sharp'; + +import { SocialLink, User } from '@/prisma/generated'; +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'; import { SocialLinkInput, SocialLinkOrderInput, -} from '@/src/module/auth/profile/inputs/social-link.input' -import { ConflictException, Injectable } from '@nestjs/common' -import sharp from 'sharp' -import { StorageService } from '../../libs/storage/storage.service' -import { SocialLink, User } from '@/prisma/generated' -import * as Upload from 'graphql-upload/Upload.js' +} from '@/src/module/auth/profile/inputs/social-link.input'; + +import { StorageService } from '../../libs/storage/storage.service'; @Injectable() export class ProfileService { @@ -20,70 +22,71 @@ export class ProfileService { public async changeAvatar(user: User, file: Upload) { if (user.avatar) { - await this.storageService.remove(user.avatar) + await this.storageService.remove(user.avatar); } - const chunks: Buffer[] = [] + const chunks: Buffer[] = []; for await (const chunk of file.createReadStream()) { - chunks.push(chunk) + chunks.push(chunk); } - const buffer = Buffer.concat(chunks) - const fileName = `/channels/${user.name}.webp` + const buffer = Buffer.concat(chunks); + const fileName = `/channels/${user.name}.webp`; const processedBuffer = await sharp(buffer, { animated: file.filename.endsWith('.gif') }) .resize(512, 512) .webp() - .toBuffer() + .toBuffer(); - await this.storageService.upload(processedBuffer, fileName, 'image/webp') + await this.storageService.upload(processedBuffer, fileName, 'image/webp'); await this.prismaService.user.update({ where: { id: user.id }, data: { avatar: fileName }, - }) + }); - return true + return true; } public async removeAvatar(user: User) { if (!user.avatar) { - return true + return true; } - await this.storageService.remove(user.avatar) + await this.storageService.remove(user.avatar); await this.prismaService.user.update({ where: { id: user.id }, data: { avatar: null }, - }) - return true + }); + + return true; } public async changeInfo(user: User, input: ChangeProfileInfoInput) { - const { bio = user.bio, displayName = user.displayName, name = user.name } = input + const { bio = user.bio, displayName = user.displayName, name = user.name } = input; const existingUser = await this.prismaService.user.findUnique({ where: { name }, - }) + }); if (existingUser && user.name !== name) { - throw new ConflictException('Пользователь с таким именем уже существует') + throw new ConflictException('Пользователь с таким именем уже существует'); } return this.prismaService.user.update({ where: { id: user.id }, data: { bio, displayName, name }, - }) + }); } public async createSocialLink(user: User, input: SocialLinkInput) { - const { title, url } = input + const { title, url } = input; const lastSocialLink = await this.prismaService.socialLink.findFirst({ where: { userId: user.id }, orderBy: { position: 'desc' }, - }) + }); return this.prismaService.socialLink.create({ data: { @@ -96,45 +99,44 @@ export class ProfileService { }, }, }, - }) + }); } public async findSocialLink(user: User) { return this.prismaService.socialLink.findMany({ where: { userId: user.id }, - }) + }); } public async reorderSocialLinks(list: SocialLinkOrderInput[]) { if (list.length === 0) { - return + return; } - const updatePromises = list.map((socialLink) => { - return this.prismaService.socialLink.update({ - where: { id: socialLink.id }, - data: { position: Number(socialLink.position) }, - }) - }) + const updatePromises = list.map(async (socialLink) => this.prismaService.socialLink.update({ + where: { id: socialLink.id }, + data: { position: Number(socialLink.position) }, + })); - await Promise.all(updatePromises) + await Promise.all(updatePromises); - return true + return true; } public async updateSocialLink(id: SocialLink['id'], input: SocialLinkInput) { - const { title, url } = input + const { title, url } = input; + return this.prismaService.socialLink.update({ where: { id }, data: { title, url }, - }) + }); } public async removeSocialLink(id: SocialLink['id']) { await this.prismaService.socialLink.delete({ where: { id }, - }) + }); - return true + return true; } } diff --git a/backend/src/module/auth/session/inputs/login.input.ts b/backend/src/module/auth/session/inputs/login.input.ts index db0cb4d..fb9d539 100644 --- a/backend/src/module/auth/session/inputs/login.input.ts +++ b/backend/src/module/auth/session/inputs/login.input.ts @@ -1,23 +1,25 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsOptional, IsString, Length, MinLength } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { + IsNotEmpty, IsOptional, IsString, Length, MinLength, +} from 'class-validator'; @InputType() export class LoginInput { @Field() @IsString() @IsNotEmpty() - login: string + login: string; @Field() @IsString() @IsNotEmpty() @MinLength(8) - password: string + password: string; @Field(() => String, { nullable: true }) @IsString() @IsNotEmpty() @IsOptional() @Length(6, 6) - pin?: string + pin?: string; } diff --git a/backend/src/module/auth/session/models/session.model.ts b/backend/src/module/auth/session/models/session.model.ts index 26ec05b..8861a91 100644 --- a/backend/src/module/auth/session/models/session.model.ts +++ b/backend/src/module/auth/session/models/session.model.ts @@ -1,56 +1,57 @@ -import { DeviceInfo, LocationInfo, SessionInfo } from '@/src/shared/types/session-metadata.types' -import { Field, ID, ObjectType } from '@nestjs/graphql' +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { DeviceInfo, LocationInfo, SessionInfo } from '@/src/shared/types/session-metadata.types'; @ObjectType() export class LocationModel implements LocationInfo { @Field(() => String) - country: string + country: string; @Field(() => String) - city: string + city: string; @Field(() => Number) - latitude: number + latitude: number; @Field(() => Number) - longitude: number + longitude: number; } @ObjectType() export class DeviceModel implements DeviceInfo { @Field(() => String) - browser: string + browser: string; @Field(() => String) - os: string + os: string; @Field(() => String) - type: string + type: string; } @ObjectType() export class SessionMetadataModel implements SessionInfo { @Field(() => LocationModel) - location: LocationModel + location: LocationModel; @Field(() => DeviceModel) - device: DeviceModel + device: DeviceModel; @Field(() => String) - ip: string + ip: string; } @ObjectType() export class SessionModel { @Field(() => ID) - id: string + id: string; @Field(() => String) - userId: string + userId: string; @Field(() => String) - createdAt: string + createdAt: string; @Field(() => SessionMetadataModel) - metadata: SessionMetadataModel + metadata: SessionMetadataModel; } diff --git a/backend/src/module/auth/session/session.module.ts b/backend/src/module/auth/session/session.module.ts index 943c061..af2ebca 100644 --- a/backend/src/module/auth/session/session.module.ts +++ b/backend/src/module/auth/session/session.module.ts @@ -1,7 +1,9 @@ +import { Module } from '@nestjs/common'; + import { VerificationService } from '@/src/module/auth/verification/verification.service'; -import { Module } from '@nestjs/common' -import { SessionService } from './session.service' -import { SessionResolver } from './session.resolver' + +import { SessionResolver } from './session.resolver'; +import { SessionService } from './session.service'; @Module({ providers: [SessionResolver, SessionService, VerificationService], diff --git a/backend/src/module/auth/session/session.resolver.ts b/backend/src/module/auth/session/session.resolver.ts index 0ae7136..adf843c 100644 --- a/backend/src/module/auth/session/session.resolver.ts +++ b/backend/src/module/auth/session/session.resolver.ts @@ -1,12 +1,15 @@ +import { + Args, Context, Mutation, Query, Resolver, +} from '@nestjs/graphql'; + import { AuthModel } from '@/src/module/auth/account/models/auth.model'; -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { LoginInput } from '@/src/module/auth/session/inputs/login.input' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { UserAgent } from '@/src/shared/decorators/user-agent.decorator' -import { GqlContext } from '@/src/shared/types/gql-context.types' -import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql' -import { SessionService } from './session.service' -import { SessionModel } from './models/session.model' +import { LoginInput } from '@/src/module/auth/session/inputs/login.input'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'; +import { GqlContext } from '@/src/shared/types/gql-context.types'; + +import { SessionModel } from './models/session.model'; +import { SessionService } from './session.service'; @Resolver('Session') export class SessionResolver { @@ -17,7 +20,7 @@ export class SessionResolver { public async findByUser( @Context() { req }: GqlContext, ) { - return this.sessionService.findByUser(req) + return this.sessionService.findByUser(req); } @Authorization() @@ -25,7 +28,7 @@ export class SessionResolver { public async findCurrent( @Context() { req }: GqlContext, ) { - return this.sessionService.findCurrentSession(req) + return this.sessionService.findCurrentSession(req); } @Mutation(() => AuthModel, { name: 'loginUser' }) @@ -34,26 +37,26 @@ export class SessionResolver { @Args('data') input: LoginInput, @UserAgent() userAgent: string, ) { - return this.sessionService.login(req, input, userAgent) + return this.sessionService.login(req, input, userAgent); } @Authorization() @Mutation(() => Boolean, { name: 'logoutUser' }) public async logout(@Context() { req }: GqlContext) { - return this.sessionService.logout(req) + return this.sessionService.logout(req); } @Mutation(() => Boolean, { name: 'clearSessionCookie' }) public clearSession(@Context() { req }: GqlContext) { - return this.sessionService.clearSession(req) + return this.sessionService.clearSession(req); } @Authorization() @Mutation(() => Boolean, { name: 'removeSession' }) - public remove( + public async remove( @Context() { req }: GqlContext, @Args('id') id: string, ) { - return this.sessionService.remove(req, id) + return this.sessionService.remove(req, id); } } diff --git a/backend/src/module/auth/session/session.service.ts b/backend/src/module/auth/session/session.service.ts index a39113e..c0acda7 100644 --- a/backend/src/module/auth/session/session.service.ts +++ b/backend/src/module/auth/session/session.service.ts @@ -1,22 +1,25 @@ -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' -import { ProcessEnv } from '../../../shared/types/env' +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { verify } from 'argon2'; +import { SessionData } from 'express-session'; +import { TOTP } from 'otpauth'; + +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 { ProcessEnv } from '../../../shared/types/env'; + +import type { Request } from 'express'; @Injectable() export class SessionService { @@ -28,82 +31,84 @@ export class SessionService { ) {} public async findByUser(req: Request) { - const userId = req.user?.id + const userId = req.user?.id; if (!userId) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } - const keys = await this.redisService.keys('*') - const userSessions: Request['session'][] = [] + const keys = await this.redisService.keys('*'); + const userSessions: Request['session'][] = []; for (const key of keys) { - const sessionData = await this.redisService.get(key) + const sessionData = await this.redisService.get(key); if (sessionData) { - const session = JSON.parse(sessionData) as Request['session'] + const session = JSON.parse(sessionData) as Request['session']; if (session.userId === userId) { userSessions.push({ ...session, id: key.split(':')[1], - } as Request['session']) + } 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) + userSessions.sort((a, b) => b.createdAt - a.createdAt); - return userSessions.filter(session => session.id !== req.session.id) + 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) + 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('Сессия не найдена') + throw new NotFoundException('Сессия не найдена'); } - const session = JSON.parse(sessionData) as SessionData + 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 { login, password, pin } = input; - const user = await this.prismaService.user.findFirst({ where: { - OR: [ - { name: { equals: login } }, - { email: { equals: login } }, - ], - } }) + const user = await this.prismaService.user.findFirst({ + where: { + OR: [ + { name: { equals: login } }, + { email: { equals: login } }, + ], + }, + }); if (!user) { - throw new NotFoundException(`Пользователь не найден`) + throw new NotFoundException('Пользователь не найден'); } - const isValidPassword = await verify(user.password, password) + const isValidPassword = await verify(user.password, password); if (!isValidPassword) { - throw new UnauthorizedException('Логин или пароль неверный') + throw new UnauthorizedException('Логин или пароль неверный'); } if (!user.isEmailVerified) { - await this.verificationService.sendVerificationToken(user) - throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения') + await this.verificationService.sendVerificationToken(user); + + throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения'); } if (user.isTotpEnabled) { if (!pin) { return { message: 'Необходимо ввести пин-код для завершения операции', - } + }; } const totp = new TOTP({ @@ -112,36 +117,36 @@ export class SessionService { algorithm: 'SHA-1', digits: 6, secret: user.totpSecret!, - }) + }); - const delta = totp.validate({ token: pin }) + const delta = totp.validate({ token: pin }); if (delta === null) { - throw new BadRequestException('Невереый код') + throw new BadRequestException('Невереый код'); } } - return saveSession(req, user, getSessionMetadata(req, userAgent)) + return saveSession(req, user, getSessionMetadata(req, userAgent)); } public async logout(req: Request) { - return destroySession(req, this.configService) + return destroySession(req, this.configService); } public clearSession(req: Request) { - req.res?.clearCookie(this.configService.getOrThrow('SESSION_NAME')) + req.res?.clearCookie(this.configService.getOrThrow('SESSION_NAME')); - return true + return true; } public async remove(req: Request, id: string) { if (req.session.id === id) { - throw new ConflictException('Текущую сессию удалить нельзя') + throw new ConflictException('Текущую сессию удалить нельзя'); } - const key = `${this.configService.getOrThrow('SESSION_FOLDER')}${id}` - await this.redisService.del(key) + const key = `${this.configService.getOrThrow('SESSION_FOLDER')}${id}`; + await this.redisService.del(key); - return true + return true; } } diff --git a/backend/src/module/auth/totp/inputs/enable-totp.input.ts b/backend/src/module/auth/totp/inputs/enable-totp.input.ts index ec1bdb9..0093f3b 100644 --- a/backend/src/module/auth/totp/inputs/enable-totp.input.ts +++ b/backend/src/module/auth/totp/inputs/enable-totp.input.ts @@ -1,16 +1,16 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsString, Length } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString, Length } from 'class-validator'; @InputType() export class EnableTotpInput { @Field(() => String) @IsString() @IsNotEmpty() - secret: string + secret: string; @Field(() => String) @IsString() @IsNotEmpty() @Length(6, 6) - pin: string + pin: string; } diff --git a/backend/src/module/auth/totp/models/totp.model.ts b/backend/src/module/auth/totp/models/totp.model.ts index e0edc80..f052dc7 100644 --- a/backend/src/module/auth/totp/models/totp.model.ts +++ b/backend/src/module/auth/totp/models/totp.model.ts @@ -3,8 +3,8 @@ import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() export class TotpModel { @Field(() => String) - public qrcodeUrl: string + public qrcodeUrl: string; @Field(() => String) - public secret: string + public secret: string; } diff --git a/backend/src/module/auth/totp/totp.module.ts b/backend/src/module/auth/totp/totp.module.ts index 644e67b..36097fc 100644 --- a/backend/src/module/auth/totp/totp.module.ts +++ b/backend/src/module/auth/totp/totp.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; -import { TotpService } from './totp.service'; + import { TotpResolver } from './totp.resolver'; +import { TotpService } from './totp.service'; @Module({ providers: [TotpResolver, TotpService], diff --git a/backend/src/module/auth/totp/totp.resolver.ts b/backend/src/module/auth/totp/totp.resolver.ts index 1984a1a..54838bd 100644 --- a/backend/src/module/auth/totp/totp.resolver.ts +++ b/backend/src/module/auth/totp/totp.resolver.ts @@ -1,10 +1,14 @@ -import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input' -import { TotpModel } from '@/src/module/auth/totp/models/totp.model' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { TotpService } from './totp.service' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; + +import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input'; +import { TotpModel } from '@/src/module/auth/totp/models/totp.model'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { TotpService } from './totp.service'; @Resolver('Totp') export class TotpResolver { @@ -13,7 +17,7 @@ export class TotpResolver { @Authorization() @Query(() => TotpModel, { name: 'generateTotpSecret' }) public async generate(@Authorized() user: User) { - return this.totpService.generate(user) + return this.totpService.generate(user); } @Authorization() @@ -22,12 +26,12 @@ export class TotpResolver { @Authorized() user: User, @Args('data') input: EnableTotpInput, ) { - return this.totpService.enable(user, input) + return this.totpService.enable(user, input); } @Authorization() @Mutation(() => Boolean, { name: 'disableTotp' }) public async disable(@Authorized() user: User) { - return this.totpService.disable(user) + return this.totpService.disable(user); } } diff --git a/backend/src/module/auth/totp/totp.service.ts b/backend/src/module/auth/totp/totp.service.ts index 7ebb0c1..994a233 100644 --- a/backend/src/module/auth/totp/totp.service.ts +++ b/backend/src/module/auth/totp/totp.service.ts @@ -1,11 +1,13 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input' -import { BadRequestException, Injectable } from '@nestjs/common' -import { User } from '@prisma/generated' -import { encode } from 'hi-base32' -import { randomBytes } from 'node:crypto' -import { TOTP } from 'otpauth' -import * as QRCode from 'qrcode' +import { randomBytes } from 'node:crypto'; + +import { BadRequestException, Injectable } from '@nestjs/common'; +import { encode } from 'hi-base32'; +import { TOTP } from 'otpauth'; +import * as QRCode from 'qrcode'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input'; +import { User } from '@prisma/generated'; @Injectable() export class TotpService { @@ -17,7 +19,7 @@ export class TotpService { async generate(user: User) { const secret = encode(randomBytes(15)) .replace(/=/g, '') - .substring(0, 24) + .substring(0, 24); const totp = new TOTP({ issuer: 'TeaStream', @@ -25,18 +27,18 @@ export class TotpService { algorithm: 'SHA-1', digits: 6, secret, - }) + }); - const qrcodeUrl = await QRCode.toDataURL(totp.toString()) + const qrcodeUrl = await QRCode.toDataURL(totp.toString()); return { qrcodeUrl, secret, - } + }; } async enable(user: User, input: EnableTotpInput) { - const { pin, secret } = input + const { pin, secret } = input; const totp = new TOTP({ issuer: 'TeaStream', @@ -44,12 +46,12 @@ export class TotpService { algorithm: 'SHA-1', digits: 6, secret, - }) + }); - const delta = totp.validate({ token: pin }) + const delta = totp.validate({ token: pin }); if (delta === null) { - throw new BadRequestException('Невереый код') + throw new BadRequestException('Невереый код'); } await this.prismaService.user.update({ @@ -58,9 +60,9 @@ export class TotpService { isTotpEnabled: true, totpSecret: secret, }, - }) + }); - return true + return true; } async disable(user: User) { @@ -70,8 +72,8 @@ export class TotpService { isTotpEnabled: false, totpSecret: null, }, - }) + }); - return true + return true; } } diff --git a/backend/src/module/auth/verification/inputs/verification.input.ts b/backend/src/module/auth/verification/inputs/verification.input.ts index cf72ae8..78c4e1b 100644 --- a/backend/src/module/auth/verification/inputs/verification.input.ts +++ b/backend/src/module/auth/verification/inputs/verification.input.ts @@ -1,10 +1,10 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsUUID } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsUUID } from 'class-validator'; @InputType() export class VerificationInput { @Field(() => String) @IsUUID('4') @IsNotEmpty() - public token: string + public token: string; } diff --git a/backend/src/module/auth/verification/verification.module.ts b/backend/src/module/auth/verification/verification.module.ts index 8f9be62..b196482 100644 --- a/backend/src/module/auth/verification/verification.module.ts +++ b/backend/src/module/auth/verification/verification.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common' -import { VerificationService } from './verification.service' -import { VerificationResolver } from './verification.resolver' +import { Module } from '@nestjs/common'; + +import { VerificationResolver } from './verification.resolver'; +import { VerificationService } from './verification.service'; @Module({ providers: [VerificationResolver, VerificationService], diff --git a/backend/src/module/auth/verification/verification.resolver.ts b/backend/src/module/auth/verification/verification.resolver.ts index 2e51be5..6f6c2a6 100644 --- a/backend/src/module/auth/verification/verification.resolver.ts +++ b/backend/src/module/auth/verification/verification.resolver.ts @@ -1,9 +1,13 @@ -import { AuthModel } from '@/src/module/auth/account/models/auth.model' -import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input' -import { UserAgent } from '@/src/shared/decorators/user-agent.decorator' -import { GqlContext } from '@/src/shared/types/gql-context.types' -import { Args, Context, Mutation, Resolver } from '@nestjs/graphql' -import { VerificationService } from './verification.service' +import { + Args, Context, Mutation, Resolver, +} from '@nestjs/graphql'; + +import { AuthModel } from '@/src/module/auth/account/models/auth.model'; +import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input'; +import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'; +import { GqlContext } from '@/src/shared/types/gql-context.types'; + +import { VerificationService } from './verification.service'; @Resolver('Verification') export class VerificationResolver { @@ -15,6 +19,6 @@ export class VerificationResolver { @Args('data') input: VerificationInput, @UserAgent() userAgent: string, ) { - return this.verificationService.verify(req, input, userAgent) + return this.verificationService.verify(req, input, userAgent); } } diff --git a/backend/src/module/auth/verification/verification.service.ts b/backend/src/module/auth/verification/verification.service.ts index 8fc5e00..cc7c61c 100644 --- a/backend/src/module/auth/verification/verification.service.ts +++ b/backend/src/module/auth/verification/verification.service.ts @@ -1,12 +1,13 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input' -import { MailService } from '@/src/module/libs/mail/mail.service' -import { generateToken } from '@/src/shared/util/generate-token.util' -import { getSessionMetadata } from '@/src/shared/util/session-metadata.util' -import { saveSession } from '@/src/shared/util/session.util' -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' -import { TokenType, User } from '@prisma/generated' -import { Request } from 'express' +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { Request } from 'express'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input'; +import { MailService } from '@/src/module/libs/mail/mail.service'; +import { generateToken } from '@/src/shared/util/generate-token.util'; +import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'; +import { saveSession } from '@/src/shared/util/session.util'; +import { TokenType, User } from '@prisma/generated'; @Injectable() export class VerificationService { @@ -17,19 +18,19 @@ export class VerificationService { } public async verify(req: Request, input: VerificationInput, userAgent: string) { - const { token } = input + const { token } = input; const existingToken = await this.prismaService.token.findUnique({ where: { token, type: TokenType.EMAIL_VERIFY }, - }) + }); if (!existingToken) { - throw new NotFoundException('Токен не найден') + throw new NotFoundException('Токен не найден'); } - const hasExpired = new Date(existingToken.expiresIn) < new Date() + const hasExpired = new Date(existingToken.expiresIn) < new Date(); if (hasExpired) { - throw new BadRequestException('Токен истек') + throw new BadRequestException('Токен истек'); } const user = await this.prismaService.user.update({ @@ -39,16 +40,16 @@ export class VerificationService { data: { isEmailVerified: true, }, - }) + }); await this.prismaService.token.delete({ where: { id: existingToken.id, type: TokenType.EMAIL_VERIFY, }, - }) + }); - return saveSession(req, user, getSessionMetadata(req, userAgent)) + return saveSession(req, user, getSessionMetadata(req, userAgent)); } public async sendVerificationToken(user: User) { @@ -56,10 +57,10 @@ export class VerificationService { this.prismaService, user, TokenType.EMAIL_VERIFY, - ) + ); await this.mailService.sendVerificationToken(user.email, verificationToken.token); - return true + return true; } } diff --git a/backend/src/module/category/category.module.ts b/backend/src/module/category/category.module.ts index 90e4778..36c2717 100644 --- a/backend/src/module/category/category.module.ts +++ b/backend/src/module/category/category.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; -import { CategoryService } from './category.service'; + import { CategoryResolver } from './category.resolver'; +import { CategoryService } from './category.service'; @Module({ providers: [CategoryResolver, CategoryService], diff --git a/backend/src/module/category/category.resolver.ts b/backend/src/module/category/category.resolver.ts index a127994..e7b7137 100644 --- a/backend/src/module/category/category.resolver.ts +++ b/backend/src/module/category/category.resolver.ts @@ -1,6 +1,8 @@ -import { CategoryModel } from '@/src/module/category/models/category.model' -import { Args, Query, Resolver } from '@nestjs/graphql' -import { CategoryService } from './category.service' +import { Args, Query, Resolver } from '@nestjs/graphql'; + +import { CategoryModel } from '@/src/module/category/models/category.model'; + +import { CategoryService } from './category.service'; @Resolver('Category') export class CategoryResolver { @@ -8,16 +10,16 @@ export class CategoryResolver { @Query(() => [CategoryModel], { name: 'findAllCategories' }) public async findAll() { - return this.categoryService.findAll() + return this.categoryService.findAll(); } @Query(() => [CategoryModel], { name: 'findRandomCategories' }) public async findRandom() { - return this.categoryService.findRandom() + return this.categoryService.findRandom(); } @Query(() => CategoryModel, { name: 'findCategoryBySlug' }) public async findByhSlug(@Args('slug') slug: string) { - return this.categoryService.findBySlug(slug) + return this.categoryService.findBySlug(slug); } } diff --git a/backend/src/module/category/category.service.ts b/backend/src/module/category/category.service.ts index 7870003..4703e88 100644 --- a/backend/src/module/category/category.service.ts +++ b/backend/src/module/category/category.service.ts @@ -1,5 +1,6 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { Injectable, NotFoundException } from '@nestjs/common' +import { Injectable, NotFoundException } from '@nestjs/common'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; @Injectable() export class CategoryService { @@ -19,16 +20,16 @@ export class CategoryService { }, }, }, - }) + }); } public async findRandom() { - const total = await this.prismaService.category.count({}) + const total = await this.prismaService.category.count({}); - const randomIndexes = new Set() + const randomIndexes = new Set(); while (randomIndexes.size < 7) { - const randomIndex = Math.floor(Math.random() * total) - randomIndexes.add(randomIndex) + const randomIndex = Math.floor(Math.random() * total); + randomIndexes.add(randomIndex); } const categories = await this.prismaService.category.findMany({ @@ -42,9 +43,9 @@ export class CategoryService { }, }, }, - }) + }); - return Array.from(randomIndexes).map(index => categories[index]) + return Array.from(randomIndexes).map((index) => categories[index]); } public async findBySlug(slug: string) { @@ -60,12 +61,12 @@ export class CategoryService { }, }, }, - }) + }); if (!category) { - throw new NotFoundException('Категория не найдена') + throw new NotFoundException('Категория не найдена'); } - return category + return category; } } diff --git a/backend/src/module/category/index.ts b/backend/src/module/category/index.ts new file mode 100644 index 0000000..9b35159 --- /dev/null +++ b/backend/src/module/category/index.ts @@ -0,0 +1 @@ +export { CategoryModel } from './models/category.model'; diff --git a/backend/src/module/category/models/category.model.ts b/backend/src/module/category/models/category.model.ts index 9918ad6..cecf5fe 100644 --- a/backend/src/module/category/models/category.model.ts +++ b/backend/src/module/category/models/category.model.ts @@ -1,32 +1,32 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql' +import { Field, ID, ObjectType } from '@nestjs/graphql'; -import type { Category } from '@/prisma/generated' +import { StreamModel } from '@/src/module/stream'; -import { StreamModel } from '../../stream/models/stream.model' +import type { Category } from '@/prisma/generated'; @ObjectType() export class CategoryModel implements Category { @Field(() => ID) - public id: string + public id: string; @Field(() => String) - public title: string + public title: string; @Field(() => String) - public slug: string + public slug: string; @Field(() => String, { nullable: true }) - public description: string + public description: string; @Field(() => String) - public thumbnailUrl: string + public thumbnailUrl: string; @Field(() => [StreamModel]) - public streams: StreamModel[] + public streams: StreamModel[]; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } diff --git a/backend/src/module/channel/channel.module.ts b/backend/src/module/channel/channel.module.ts index ad4acfe..f5f2b6e 100644 --- a/backend/src/module/channel/channel.module.ts +++ b/backend/src/module/channel/channel.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common' -import { ChannelService } from './channel.service' -import { ChannelResolver } from './channel.resolver' +import { Module } from '@nestjs/common'; + +import { ChannelResolver } from './channel.resolver'; +import { ChannelService } from './channel.service'; @Module({ providers: [ChannelResolver, ChannelService], diff --git a/backend/src/module/channel/channel.resolver.ts b/backend/src/module/channel/channel.resolver.ts index e0705f2..b3b086d 100644 --- a/backend/src/module/channel/channel.resolver.ts +++ b/backend/src/module/channel/channel.resolver.ts @@ -1,7 +1,9 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { SubscriptionModel } from '@/src/module/sponsorship/subscription/model/subscription.model' -import { Args, Query, Resolver } from '@nestjs/graphql' -import { ChannelService } from './channel.service' +import { Args, Query, Resolver } from '@nestjs/graphql'; + +import { UserModel } from '@/src/module/auth/account/models/user.model'; +import { SubscriptionModel } from '@/src/module/sponsorship/subscription/model/subscription.model'; + +import { ChannelService } from './channel.service'; @Resolver('Channel') export class ChannelResolver { @@ -9,21 +11,21 @@ export class ChannelResolver { @Query(() => [UserModel], { name: 'findRecommendedChannels' }) public async findRecommended() { - return this.channelService.findRecommendedChannel() + return this.channelService.findRecommendedChannel(); } @Query(() => UserModel, { name: 'findChannelByUsername' }) public async findByUsername(@Args('name') name: string) { - return this.channelService.findByUsername(name) + return this.channelService.findByUsername(name); } @Query(() => Number, { name: 'findChannelFollowersCount' }) public async findFollowersCount(@Args('channelId') channelId: string) { - return this.channelService.findFollowersCountByChannel(channelId) + return this.channelService.findFollowersCountByChannel(channelId); } @Query(() => [SubscriptionModel], { name: 'findSponsorsByChannel' }) public async findSponsorsByChannel(@Args('channelId') channelId: string) { - return this.channelService.findSponsorsByChannel(channelId) + return this.channelService.findSponsorsByChannel(channelId); } } diff --git a/backend/src/module/channel/channel.service.ts b/backend/src/module/channel/channel.service.ts index 0784ab6..65ad539 100644 --- a/backend/src/module/channel/channel.service.ts +++ b/backend/src/module/channel/channel.service.ts @@ -1,5 +1,6 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { Injectable, NotFoundException } from '@nestjs/common' +import { Injectable, NotFoundException } from '@nestjs/common'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; @Injectable() export class ChannelService { @@ -14,7 +15,7 @@ export class ChannelService { orderBy: { followings: { _count: 'desc' } }, include: { stream: true }, take: 7, - }) + }); } public async findByUsername(name: string) { @@ -25,28 +26,28 @@ export class ChannelService { stream: { include: { category: true } }, followings: true, }, - }) + }); if (!channel) { - throw new NotFoundException('Канал не найден') + throw new NotFoundException('Канал не найден'); } - return channel + return channel; } public async findFollowersCountByChannel(channelId: string) { return this.prismaService.follow.count({ where: { following: { id: channelId } }, - }) + }); } public async findSponsorsByChannel(channelId: string) { const channel = await this.prismaService.user.findUnique({ where: { id: channelId, isDeactivated: false }, - }) + }); if (!channel) { - throw new NotFoundException('Канал не найден') + throw new NotFoundException('Канал не найден'); } return this.prismaService.sponsorshipSubscription.findMany({ @@ -55,6 +56,6 @@ export class ChannelService { include: { user: true, }, - }) + }); } } diff --git a/backend/src/module/chat/chat.module.ts b/backend/src/module/chat/chat.module.ts index 5fd7478..583b26d 100644 --- a/backend/src/module/chat/chat.module.ts +++ b/backend/src/module/chat/chat.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common' -import { ChatService } from './chat.service' -import { ChatResolver } from './chat.resolver' +import { Module } from '@nestjs/common'; + +import { ChatResolver } from './chat.resolver'; +import { ChatService } from './chat.service'; @Module({ providers: [ChatResolver, ChatService], diff --git a/backend/src/module/chat/chat.resolver.ts b/backend/src/module/chat/chat.resolver.ts index cee7093..c1d4e70 100644 --- a/backend/src/module/chat/chat.resolver.ts +++ b/backend/src/module/chat/chat.resolver.ts @@ -1,24 +1,29 @@ -import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input' -import { SendMessageInput } from '@/src/module/chat/input/send-message.input' -import { StreamModel } from '@/src/module/stream/models/stream.model' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql' -import { ChatMessage, User } from '@prisma/generated' -import { PubSub } from 'graphql-subscriptions' -import { ChatService } from './chat.service' -import { ChatMessageModel } from './models/chat-message.model' +import { + Args, Mutation, Query, Resolver, Subscription, +} from '@nestjs/graphql'; +import { PubSub } from 'graphql-subscriptions'; + +import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input'; +import { SendMessageInput } from '@/src/module/chat/input/send-message.input'; +import { StreamModel } from '@/src/module/stream/models/stream.model'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { ChatMessage, User } from '@prisma/generated'; + +import { ChatService } from './chat.service'; +import { ChatMessageModel } from './models/chat-message.model'; @Resolver('Chat') export class ChatResolver { - public pubSub: PubSub + public pubSub: PubSub; + constructor(private readonly chatService: ChatService) { - this.pubSub = new PubSub() + this.pubSub = new PubSub(); } @Query(() => [ChatMessageModel], { name: 'findMessagesByStream' }) public async findMessagesByStream(@Args('streamId') streamId: string) { - return this.chatService.findMessagesByStream(streamId) + return this.chatService.findMessagesByStream(streamId); } @Authorization() @@ -27,15 +32,15 @@ export class ChatResolver { @Authorized('id') userId: User['id'], @Args('data') input: SendMessageInput, ) { - const message = this.chatService.sendMessage(userId, input) - void this.pubSub.publish('CHAT_MESSAGE_ADDED', { message }) + const message = this.chatService.sendMessage(userId, input); + void this.pubSub.publish('CHAT_MESSAGE_ADDED', { message }); - return message + return message; } @Subscription(() => ChatMessageModel, { name: 'chatMessageAdded', filter: (payload: { message: ChatMessage }, variables: ChatMessageModel) => payload.message.streamId === variables.streamId }) public async chatMessageAdded(@Args('streamId') streamId: string) { - return this.pubSub.asyncIterableIterator('CHAT_MESSAGE_ADDED') + return this.pubSub.asyncIterableIterator('CHAT_MESSAGE_ADDED'); } @Authorization() @@ -44,6 +49,6 @@ export class ChatResolver { @Args('data') input: ChangeChatSettingsInput, @Authorized() user: User, ) { - return this.chatService.changeSettings(user, input) + return this.chatService.changeSettings(user, input); } } diff --git a/backend/src/module/chat/chat.service.ts b/backend/src/module/chat/chat.service.ts index 22c6944..2fcd449 100644 --- a/backend/src/module/chat/chat.service.ts +++ b/backend/src/module/chat/chat.service.ts @@ -1,8 +1,9 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input' -import { SendMessageInput } from '@/src/module/chat/input/send-message.input' -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' -import { Stream, User } from '@prisma/generated' +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input'; +import { SendMessageInput } from '@/src/module/chat/input/send-message.input'; +import { Stream, User } from '@prisma/generated'; @Injectable() export class ChatService { @@ -16,27 +17,27 @@ export class ChatService { where: { streamId }, orderBy: { createdAt: 'desc' }, include: { user: true }, - }) + }); } public async sendMessage(userId: User['id'], input: SendMessageInput) { - const { text, streamId } = input + const { text, streamId } = input; const stream = await this.prismaService.stream.findUnique({ where: { id: streamId }, - }) + }); if (!stream) { - throw new NotFoundException('Стрим не найден') + throw new NotFoundException('Стрим не найден'); } if (!stream.isLive) { - throw new BadRequestException('Стрим не запущен') + throw new BadRequestException('Стрим не запущен'); } const user = await this.prismaService.user.findUnique({ where: { id: userId }, - }) + }); if (!user) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } return this.prismaService.chatMessage.create({ @@ -56,15 +57,15 @@ export class ChatService { include: { stream: true, }, - }) + }); } public async changeSettings(user: User, input: ChangeChatSettingsInput) { - const { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly } = input + const { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly } = input; return this.prismaService.stream.update({ where: { userId: user.id }, data: { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly }, - }) + }); } } diff --git a/backend/src/module/chat/index.ts b/backend/src/module/chat/index.ts new file mode 100644 index 0000000..21c7841 --- /dev/null +++ b/backend/src/module/chat/index.ts @@ -0,0 +1 @@ +export { ChatMessageModel } from './models/chat-message.model'; diff --git a/backend/src/module/chat/input/change-chat-settings.input.ts b/backend/src/module/chat/input/change-chat-settings.input.ts index 9448522..c3e9562 100644 --- a/backend/src/module/chat/input/change-chat-settings.input.ts +++ b/backend/src/module/chat/input/change-chat-settings.input.ts @@ -1,17 +1,17 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsBoolean } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsBoolean } from 'class-validator'; @InputType() export class ChangeChatSettingsInput { @Field(() => Boolean) @IsBoolean() - public isChatEnable: boolean + public isChatEnable: boolean; @Field(() => Boolean) @IsBoolean() - public isChatFollowersOnly: boolean + public isChatFollowersOnly: boolean; @Field(() => Boolean) @IsBoolean() - public isChatPremiumFollowersOnly: boolean + public isChatPremiumFollowersOnly: boolean; } diff --git a/backend/src/module/chat/input/send-message.input.ts b/backend/src/module/chat/input/send-message.input.ts index 0089326..e2f01e6 100644 --- a/backend/src/module/chat/input/send-message.input.ts +++ b/backend/src/module/chat/input/send-message.input.ts @@ -1,16 +1,17 @@ -import { Field, InputType } from '@nestjs/graphql' -import { Stream } from '@prisma/generated' -import { IsNotEmpty, IsString } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString } from 'class-validator'; + +import { Stream } from '@prisma/generated'; @InputType() export class SendMessageInput { @Field(() => String) @IsString() @IsNotEmpty() - public text: string + public text: string; @Field(() => String) @IsString() @IsNotEmpty() - streamId: Stream['id'] + streamId: Stream['id']; } diff --git a/backend/src/module/chat/models/chat-message.model.ts b/backend/src/module/chat/models/chat-message.model.ts index 9f3f6b5..8be48d2 100644 --- a/backend/src/module/chat/models/chat-message.model.ts +++ b/backend/src/module/chat/models/chat-message.model.ts @@ -1,25 +1,26 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { StreamModel } from '@/src/module/stream/models/stream.model' -import { Field, ID, ObjectType } from '@nestjs/graphql' -import { ChatMessage } from '@prisma/generated' +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { UserModel } from '@/src/module/auth/account/models/user.model'; +import { StreamModel } from '@/src/module/stream/models/stream.model'; +import { ChatMessage } from '@prisma/generated'; @ObjectType() export class ChatMessageModel implements ChatMessage { @Field(() => ID) - public id: string + public id: string; @Field(() => String) - text: string + text: string; @Field(() => ID) - streamId: StreamModel['id'] + streamId: StreamModel['id']; @Field(() => ID) - userId: UserModel['id'] + userId: UserModel['id']; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } diff --git a/backend/src/module/cron/cron.module.ts b/backend/src/module/cron/cron.module.ts index 2642f95..dc08e5e 100644 --- a/backend/src/module/cron/cron.module.ts +++ b/backend/src/module/cron/cron.module.ts @@ -1,7 +1,9 @@ -import { NotificationService } from '@/src/module/notification/notification.service' -import { Module } from '@nestjs/common' -import { ScheduleModule } from '@nestjs/schedule' -import { CronService } from './cron.service' +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; + +import { NotificationService } from '@/src/module/notification/notification.service'; + +import { CronService } from './cron.service'; @Module({ imports: [ScheduleModule.forRoot()], diff --git a/backend/src/module/cron/cron.service.ts b/backend/src/module/cron/cron.service.ts index c6d2afc..ea6e861 100644 --- a/backend/src/module/cron/cron.service.ts +++ b/backend/src/module/cron/cron.service.ts @@ -1,10 +1,11 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { MailService } from '@/src/module/libs/mail/mail.service' -import { StorageService } from '@/src/module/libs/storage/storage.service' -import { TelegramService } from '@/src/module/libs/telegram/telegram.service' -import { NotificationService } from '@/src/module/notification/notification.service' -import { Injectable } from '@nestjs/common' -import { Cron, CronExpression } from '@nestjs/schedule' +import { Injectable } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { MailService } from '@/src/module/libs/mail/mail.service'; +import { StorageService } from '@/src/module/libs/storage/storage.service'; +import { TelegramService } from '@/src/module/libs/telegram/telegram.service'; +import { NotificationService } from '@/src/module/notification/notification.service'; @Injectable() export class CronService { @@ -19,8 +20,8 @@ export class CronService { @Cron(CronExpression.EVERY_DAY_AT_NOON) private async deleteDeactivatedAccounts() { - const sevenDayAgo = new Date() - sevenDayAgo.setDate(sevenDayAgo.getDay() - 7) + const sevenDayAgo = new Date(); + sevenDayAgo.setDate(sevenDayAgo.getDay() - 7); const deactivatedAccounts = await this.prismaService.user.findMany({ where: { @@ -32,17 +33,16 @@ export class CronService { include: { notificationSettings: true, }, - }) + }); for (const user of deactivatedAccounts) { - console.log('Deactivate user', user.name, user.email) - await this.mailService.sendAccountDeletion(user.email) + await this.mailService.sendAccountDeletion(user.email); if (user?.telegramId) { - await this.telegramService.sendAccountDeletionToken(user?.telegramId) + await this.telegramService.sendAccountDeletionToken(user?.telegramId); } if (user.avatar) { - await this.storageService.remove(user.avatar) + await this.storageService.remove(user.avatar); } } @@ -53,7 +53,7 @@ export class CronService { lte: sevenDayAgo, }, }, - }) + }); } @Cron('0 0 */4 * *') @@ -61,21 +61,21 @@ export class CronService { const users = await this.prismaService.user.findMany({ where: { isTotpEnabled: false }, include: { notificationSettings: true }, - }) + }); for (const user of users) { if (!user) { - continue + continue; } - await this.mailService.sendEnableTwoFactor(user.email) + await this.mailService.sendEnableTwoFactor(user.email); if (user.notificationSettings?.siteNotifications) { - await this.notificationService.createEnableTwoFactor(user.id) + await this.notificationService.createEnableTwoFactor(user.id); } if (user.notificationSettings?.telegramNotifications && user.telegramId) { - await this.telegramService.sendEnableTwoFactor(user.telegramId) + await this.telegramService.sendEnableTwoFactor(user.telegramId); } } } @@ -84,27 +84,27 @@ export class CronService { public async verifyChannels() { const users = await this.prismaService.user.findMany({ include: { notificationSettings: true }, - }) + }); for (const user of users) { const followersCount = await this.prismaService.follow.count({ where: { followingId: user.id }, - }) + }); if (followersCount > 10 && !user.isVerified) { await this.prismaService.user.update({ where: { id: user.id }, data: { isVerified: true }, - }) + }); - await this.mailService.sendVerifyChannel(user.email) + await this.mailService.sendVerifyChannel(user.email); if (user.notificationSettings?.siteNotifications) { - await this.notificationService.createVerifyChannel(user.id) + await this.notificationService.createVerifyChannel(user.id); } if (user.notificationSettings?.telegramNotifications && user.telegramId) { - await this.telegramService.sendVerifyChannel(user.telegramId) + await this.telegramService.sendVerifyChannel(user.telegramId); } } } @@ -112,8 +112,8 @@ export class CronService { @Cron(CronExpression.EVERY_DAY_AT_1AM) public async deleteOldNotifications() { - const sevenDaysAgo = new Date() - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7) + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); await this.prismaService.notification.deleteMany({ where: { @@ -121,6 +121,6 @@ export class CronService { lte: sevenDaysAgo, }, }, - }) + }); } } diff --git a/backend/src/module/follow/follow.module.ts b/backend/src/module/follow/follow.module.ts index d0cfe23..a63d93a 100644 --- a/backend/src/module/follow/follow.module.ts +++ b/backend/src/module/follow/follow.module.ts @@ -1,7 +1,9 @@ -import { NotificationService } from '@/src/module/notification/notification.service' -import { Module } from '@nestjs/common' -import { FollowService } from './follow.service' -import { FollowResolver } from './follow.resolver' +import { Module } from '@nestjs/common'; + +import { NotificationService } from '@/src/module/notification/notification.service'; + +import { FollowResolver } from './follow.resolver'; +import { FollowService } from './follow.service'; @Module({ providers: [FollowResolver, FollowService, NotificationService], diff --git a/backend/src/module/follow/follow.resolver.ts b/backend/src/module/follow/follow.resolver.ts index d5aaf68..b492ab2 100644 --- a/backend/src/module/follow/follow.resolver.ts +++ b/backend/src/module/follow/follow.resolver.ts @@ -1,9 +1,13 @@ -import { FollowModel } from '@/src/module/follow/model/follow.model' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { FollowService } from './follow.service' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; + +import { FollowModel } from '@/src/module/follow/model/follow.model'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { FollowService } from './follow.service'; @Resolver('Follow') export class FollowResolver { @@ -12,24 +16,24 @@ export class FollowResolver { @Authorization() @Query(() => [FollowModel], { name: 'findMyFollowers' }) public async findMyFollowers(@Authorized() user: User) { - return this.followService.findMyFollowers(user) + return this.followService.findMyFollowers(user); } @Authorization() @Query(() => [FollowModel], { name: 'findMyFollowings' }) public async findMyFollowings(@Authorized() user: User) { - return this.followService.findMyFollowings(user) + return this.followService.findMyFollowings(user); } @Authorization() @Mutation(() => FollowModel, { name: 'followChannel' }) public async follow(@Authorized() user: User, @Args('channelId') channelId: string) { - return this.followService.follow(user, channelId) + return this.followService.follow(user, channelId); } @Authorization() @Mutation(() => FollowModel, { name: 'unfollowChannel' }) public async unfollow(@Authorized() user: User, @Args('channelId') channelId: string) { - return this.followService.unfollow(user, channelId) + return this.followService.unfollow(user, channelId); } } diff --git a/backend/src/module/follow/follow.service.ts b/backend/src/module/follow/follow.service.ts index f2d4094..a582b28 100644 --- a/backend/src/module/follow/follow.service.ts +++ b/backend/src/module/follow/follow.service.ts @@ -1,8 +1,9 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' +import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; import { TelegramService } from '@/src/module/libs/telegram/telegram.service'; -import { NotificationService } from '@/src/module/notification/notification.service' -import { ConflictException, Injectable, NotFoundException } from '@nestjs/common' -import { User } from '@prisma/generated' +import { NotificationService } from '@/src/module/notification/notification.service'; +import { User } from '@prisma/generated'; @Injectable() export class FollowService { @@ -18,7 +19,7 @@ export class FollowService { where: { followingId: user.id }, orderBy: { createdAt: 'desc' }, include: { follower: true, following: true }, - }) + }); } public async findMyFollowings(user: User) { @@ -26,19 +27,19 @@ export class FollowService { where: { followerId: user.id }, orderBy: { createdAt: 'desc' }, include: { follower: true, following: true }, - }) + }); } public async follow(user: User, channelId: string) { const channel = await this.prismaService.user.findUnique({ where: { id: channelId }, - }) + }); if (!channel) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } if (channel.id === user.id) { - throw new ConflictException('Нельзя подписаться на себя') + throw new ConflictException('Нельзя подписаться на себя'); } const existingFollow = await this.prismaService.follow.findFirst({ @@ -46,10 +47,10 @@ export class FollowService { followerId: user.id, followingId: channel.id, }, - }) + }); if (existingFollow) { - throw new ConflictException('Подписка уже существует') + throw new ConflictException('Подписка уже существует'); } const follow = await this.prismaService.follow.create({ @@ -65,27 +66,28 @@ export class FollowService { }, follower: true, }, - }) + }); if (follow.following.notificationSettings?.siteNotifications) { - await this.notificationService.createNewFollowing(follow.following.id, follow.follower) + await this.notificationService.createNewFollowing(follow.following.id, follow.follower); } if (follow.following.notificationSettings?.telegramNotifications && follow.following.telegramId) { - await this.telegramService.sendNewFollowing(follow.following.telegramId, follow.follower) + await this.telegramService.sendNewFollowing(follow.following.telegramId, follow.follower); } - return follow + + return follow; } public async unfollow(user: User, channelId: string) { const channel = await this.prismaService.user.findUnique({ where: { id: channelId }, - }) + }); if (!channel) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } if (channel.id === user.id) { - throw new ConflictException('Нельзя отписаться от себя') + throw new ConflictException('Нельзя отписаться от себя'); } const existingFollow = await this.prismaService.follow.findFirst({ @@ -93,10 +95,10 @@ export class FollowService { followerId: user.id, followingId: channel.id, }, - }) + }); if (!existingFollow) { - throw new ConflictException('Подписки не существует') + throw new ConflictException('Подписки не существует'); } return this.prismaService.follow.delete({ @@ -105,6 +107,6 @@ export class FollowService { following: true, follower: true, }, - }) + }); } } diff --git a/backend/src/module/follow/index.ts b/backend/src/module/follow/index.ts new file mode 100644 index 0000000..dd487ed --- /dev/null +++ b/backend/src/module/follow/index.ts @@ -0,0 +1 @@ +export { FollowModel } from './model/follow.model'; diff --git a/backend/src/module/follow/model/follow.model.ts b/backend/src/module/follow/model/follow.model.ts index 5c8a855..da97a91 100644 --- a/backend/src/module/follow/model/follow.model.ts +++ b/backend/src/module/follow/model/follow.model.ts @@ -1,27 +1,28 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { Field, ID, ObjectType } from '@nestjs/graphql' -import { Follow } from '@prisma/generated' +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { UserModel } from '@/src/module/auth/account/models/user.model'; +import { Follow } from '@prisma/generated'; @ObjectType() export class FollowModel implements Follow { @Field(() => ID) - public id: string + public id: string; @Field(() => UserModel) - public following: UserModel + public following: UserModel; @Field(() => ID) - public followingId: string + public followingId: string; @Field(() => UserModel) - public follower: UserModel + public follower: UserModel; @Field(() => ID) - public followerId: string + public followerId: string; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } diff --git a/backend/src/module/libs/livekit/livekit.module.ts b/backend/src/module/libs/livekit/livekit.module.ts index d8ee024..77d7814 100644 --- a/backend/src/module/libs/livekit/livekit.module.ts +++ b/backend/src/module/libs/livekit/livekit.module.ts @@ -1,10 +1,11 @@ -import { LiveKitService } from '@/src/module/libs/livekit/livekit.service' +import { DynamicModule, Module } from '@nestjs/common'; + +import { LiveKitService } from '@/src/module/libs/livekit/livekit.service'; import { LiveKitOptionSymbol, TypeLiveKitAsyncOptions, TypeLiveKitOptions, -} from '@/src/module/libs/livekit/type/livekit.type' -import { DynamicModule, Module } from '@nestjs/common' +} from '@/src/module/libs/livekit/type/livekit.type'; @Module({}) export class LiveKitModule { @@ -20,7 +21,7 @@ export class LiveKitModule { ], exports: [LiveKitService], global: true, - } + }; } public static registerAsync(options: TypeLiveKitAsyncOptions): DynamicModule { @@ -37,6 +38,6 @@ export class LiveKitModule { ], exports: [LiveKitService], global: true, - } + }; } } diff --git a/backend/src/module/libs/livekit/livekit.service.ts b/backend/src/module/libs/livekit/livekit.service.ts index 818e0a7..997b966 100644 --- a/backend/src/module/libs/livekit/livekit.service.ts +++ b/backend/src/module/libs/livekit/livekit.service.ts @@ -1,44 +1,47 @@ -import { LiveKitOptionSymbol, TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type' -import { Inject, Injectable } from '@nestjs/common' -import { IngressClient, RoomServiceClient, WebhookReceiver } from 'livekit-server-sdk' +import { Inject, Injectable } from '@nestjs/common'; +import { IngressClient, RoomServiceClient, WebhookReceiver } from 'livekit-server-sdk'; + +import { LiveKitOptionSymbol, TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type'; @Injectable() export class LiveKitService { - private roomService: RoomServiceClient - private ingressClient: IngressClient - private webhookReceiver: WebhookReceiver + private readonly roomService: RoomServiceClient; + + private readonly ingressClient: IngressClient; + + private readonly webhookReceiver: WebhookReceiver; constructor( @Inject(LiveKitOptionSymbol) private readonly options: TypeLiveKitOptions, ) { - this.roomService = new RoomServiceClient(options.apiUrl, options.apiKey, options.apiSecret) - this.ingressClient = new IngressClient(options.apiUrl) - this.webhookReceiver = new WebhookReceiver(options.apiKey, options.apiSecret) + this.roomService = new RoomServiceClient(options.apiUrl, options.apiKey, options.apiSecret); + this.ingressClient = new IngressClient(options.apiUrl); + this.webhookReceiver = new WebhookReceiver(options.apiKey, options.apiSecret); } public get ingress(): IngressClient { - return this.createProxy(this.ingressClient) + return this.createProxy(this.ingressClient); } public get room(): RoomServiceClient { - return this.createProxy(this.roomService) + return this.createProxy(this.roomService); } public get webhook(): WebhookReceiver { - return this.createProxy(this.webhookReceiver) + return this.createProxy(this.webhookReceiver); } private createProxy(target: Target) { return new Proxy(target, { get: (obj, prop) => { - const value = obj[prop as Prop] + const value = obj[prop as Prop]; if (typeof value === 'function') { - return value.bind(obj) + return value.bind(obj); } - return value + return value; }, - }) + }); } } diff --git a/backend/src/module/libs/livekit/type/livekit.type.ts b/backend/src/module/libs/livekit/type/livekit.type.ts index 81b9b0a..70a9ea3 100644 --- a/backend/src/module/libs/livekit/type/livekit.type.ts +++ b/backend/src/module/libs/livekit/type/livekit.type.ts @@ -1,12 +1,11 @@ -import { FactoryProvider, ModuleMetadata } from '@nestjs/common' +import type { FactoryProvider, ModuleMetadata } from '@nestjs/common'; -export const LiveKitOptionSymbol = Symbol('LivekitOptionSymbol') +export const LiveKitOptionSymbol = Symbol('LivekitOptionSymbol'); export type TypeLiveKitOptions = { - apiUrl: string - apiKey: string - apiSecret: string -} + apiUrl: string; + apiKey: string; + apiSecret: string; +}; -export type TypeLiveKitAsyncOptions = Pick - & Pick, 'useFactory' | 'inject'> +export type TypeLiveKitAsyncOptions = Pick, 'inject' | 'useFactory'> & Pick; diff --git a/backend/src/module/libs/mail/mail.module.ts b/backend/src/module/libs/mail/mail.module.ts index 0c26d25..cdaec53 100644 --- a/backend/src/module/libs/mail/mail.module.ts +++ b/backend/src/module/libs/mail/mail.module.ts @@ -1,8 +1,10 @@ -import { getMailConfig } from '@/src/core/config/mailer.config' -import { MailerModule } from '@nestjs-modules/mailer' -import { Global, Module } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' -import { MailService } from './mail.service' +import { Global, Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { MailerModule } from '@nestjs-modules/mailer'; + +import { getMailConfig } from '@/src/core/config/mailer.config'; + +import { MailService } from './mail.service'; @Global() @Module({ diff --git a/backend/src/module/libs/mail/mail.service.ts b/backend/src/module/libs/mail/mail.service.ts index 18bd40d..7cbb907 100644 --- a/backend/src/module/libs/mail/mail.service.ts +++ b/backend/src/module/libs/mail/mail.service.ts @@ -1,16 +1,15 @@ -import { SessionInfo } from '@/src/shared/types/session-metadata.types' -import { Token } from '@prisma/generated' -import { ProcessEnv } from '@/src/shared/types/env' -import { MailerService } from '@nestjs-modules/mailer' -import { Injectable } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { render } from '@react-email/components' -import { EnableTwoFactorTemplate } from './templates' -import { VerifyChannelTemplate } from './templates' -import { VerificationTemplate } from './templates' -import { PasswordRecoveryTemplate } from './templates' -import { DeactivateTemplate } from './templates' -import { AccountDeletionTemplate } from './templates' +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { MailerService } from '@nestjs-modules/mailer'; +import { render } from '@react-email/components'; + +import { ProcessEnv } from '@/src/shared/types/env'; +import { SessionInfo } from '@/src/shared/types/session-metadata.types'; +import { Token } from '@prisma/generated'; + +import { + EnableTwoFactorTemplate, VerifyChannelTemplate, VerificationTemplate, PasswordRecoveryTemplate, DeactivateTemplate, AccountDeletionTemplate, +} from './templates'; @Injectable() export class MailService { @@ -20,50 +19,50 @@ export class MailService { ) {} public async sendVerificationToken(email: string, token: Token['token']) { - const domain = this.configService.getOrThrow('ALLOWED_ORIGIN') - const html = await render(VerificationTemplate({ domain, token })) + const domain = this.configService.getOrThrow('ALLOWED_ORIGIN'); + const html = await render(VerificationTemplate({ domain, token })); - void this.sendMail(email, 'Верификация аккаунта', html) + void this.sendMail(email, 'Верификация аккаунта', html); } public async sendPasswordResetToken(email: string, token: Token['token'], metadata: SessionInfo) { - const domain = this.configService.getOrThrow('ALLOWED_ORIGIN') - const html = await render(PasswordRecoveryTemplate({ domain, token, metadata })) + const domain = this.configService.getOrThrow('ALLOWED_ORIGIN'); + const html = await render(PasswordRecoveryTemplate({ domain, token, metadata })); - void this.sendMail(email, 'Сброс пароля', html) + void this.sendMail(email, 'Сброс пароля', html); } public async sendDeactivateToken(email: string, token: Token['token'], metadata: SessionInfo) { - const html = await render(DeactivateTemplate({ token, metadata })) + const html = await render(DeactivateTemplate({ token, metadata })); - void this.sendMail(email, 'Деактивация аккаунта', html) + void this.sendMail(email, 'Деактивация аккаунта', html); } public async sendAccountDeletion(email: string) { - const domain = this.configService.getOrThrow('ALLOWED_ORIGIN') - const html = await render(AccountDeletionTemplate({ domain })) + const domain = this.configService.getOrThrow('ALLOWED_ORIGIN'); + const html = await render(AccountDeletionTemplate({ domain })); - void this.sendMail(email, 'Удаление аккаунта', html) + void this.sendMail(email, 'Удаление аккаунта', html); } public async sendEnableTwoFactor(email: string) { - const domain = this.configService.getOrThrow('ALLOWED_ORIGIN') - const html = await render(EnableTwoFactorTemplate({ domain })) + const domain = this.configService.getOrThrow('ALLOWED_ORIGIN'); + const html = await render(EnableTwoFactorTemplate({ domain })); - void this.sendMail(email, 'Обеспечьте свою безопасность', html) + void this.sendMail(email, 'Обеспечьте свою безопасность', html); } public async sendVerifyChannel(email: string) { - const html = await render(VerifyChannelTemplate()) + const html = await render(VerifyChannelTemplate()); - void this.sendMail(email, 'Ваш канал верифицирован', html) + void this.sendMail(email, 'Ваш канал верифицирован', html); } - private sendMail(email: string, subject: string, html: string) { + private async sendMail(email: string, subject: string, html: string) { return this.mailerService.sendMail({ to: email, subject, html, - }) + }); } } diff --git a/backend/src/module/libs/mail/templates/account-deletion.template.tsx b/backend/src/module/libs/mail/templates/account-deletion.template.tsx index 14cdfe2..e8dbfd4 100644 --- a/backend/src/module/libs/mail/templates/account-deletion.template.tsx +++ b/backend/src/module/libs/mail/templates/account-deletion.template.tsx @@ -8,26 +8,29 @@ import { Section, Tailwind, Text, -} from '@react-email/components' -import * as React from 'react' +} from '@react-email/components'; +import * as React from 'react'; -interface AccountDeletionTemplateProps { - domain: string -} +type AccountDeletionTemplateProps = { + domain: string; +}; -export function AccountDeletionTemplate({ domain }: AccountDeletionTemplateProps) { - const registerLink = `${domain}/account/create` +export const AccountDeletionTemplate = ({ domain }: AccountDeletionTemplateProps) => { + const registerLink = `${domain}/account/create`; return ( + Аккаунт удалён +
Ваш аккаунт был полностью удалён + Ваш аккаунт был полностью стерт из базы данных TeaStream. Все ваши данные и информация были удалены безвозвратно. @@ -37,12 +40,14 @@ export function AccountDeletionTemplate({ domain }: AccountDeletionTemplateProps Вы больше не будете получать уведомления в Telegram и на почту. + Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке: + Зарегистрироваться на Teastream @@ -56,5 +61,5 @@ export function AccountDeletionTemplate({ domain }: AccountDeletionTemplateProps - ) -} + ); +}; diff --git a/backend/src/module/libs/mail/templates/deactivate.template.tsx b/backend/src/module/libs/mail/templates/deactivate.template.tsx index 45b0ef6..aafc8e1 100644 --- a/backend/src/module/libs/mail/templates/deactivate.template.tsx +++ b/backend/src/module/libs/mail/templates/deactivate.template.tsx @@ -1,90 +1,104 @@ -import type { SessionInfo } from '@/src/shared/types/session-metadata.types' -import { Body, Head, Heading, Link, Preview, Section, Tailwind, Text } from '@react-email/components' -import { Html } from '@react-email/html' -import * as React from 'react' +import { + Body, Head, Heading, Link, Preview, Section, Tailwind, Text, +} from '@react-email/components'; +import { Html } from '@react-email/html'; +import * as React from 'react'; -interface DeactivateTemplateProps { - token: string - metadata: SessionInfo -} +import type { SessionInfo } from '@/src/shared/types/session-metadata.types'; -export function DeactivateTemplate({ token, metadata }: DeactivateTemplateProps) { - return ( - - - Деактивация аккаунта - - -
- - Запрос на деактивацию аккаунта - - - Вы инициировали процесс деактивации вашего аккаунта на платформе - {' '} - TeaStream - . - -
+type DeactivateTemplateProps = { + token: string; + metadata: SessionInfo; +}; -
- - Код подтверждения: - - - {token} - - - Этот код действителен в течение 5 минут. - -
+export const DeactivateTemplate = ({ token, metadata }: DeactivateTemplateProps) => ( + + -
- Деактивация аккаунта + + + +
+ + Запрос на деактивацию аккаунта + + + + Вы инициировали процесс деактивации вашего аккаунта на платформе + {' '} + + TeaStream + . + +
+ +
+ + Код подтверждения: + + + + {token} + + + + Этот код действителен в течение 5 минут. + +
+ +
+ + Информация о запросе: + + +
    +
  • + 🌍 Расположение: + {metadata.location.country} + , + + {metadata.location.city} +
  • + +
  • + 📱 Операционная система: + {metadata.device.os} +
  • + +
  • + 🌐 Браузер: + {metadata.device.browser} +
  • + +
  • + 💻 IP-адрес: + {metadata.ip} +
  • +
+ + + Если вы не инициировали этот запрос, пожалуйста, игнорируйте это сообщение. + +
+ +
+ + Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу + {' '} + + - Информация о запросе: - -
    -
  • - 🌍 Расположение: - {metadata.location.country} - , - {metadata.location.city} -
  • -
  • - 📱 Операционная система: - {metadata.device.os} -
  • -
  • - 🌐 Браузер: - {metadata.device.browser} -
  • -
  • - 💻 IP-адрес: - {metadata.ip} -
  • -
- - Если вы не инициировали этот запрос, пожалуйста, игнорируйте это сообщение. - -
- -
- - Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу - {' '} - - help@teastream.ru - - . - -
- -
- - ) -} + help@teastream.ru + + . + +
+ +
+ +); diff --git a/backend/src/module/libs/mail/templates/enable-two-factor.template.tsx b/backend/src/module/libs/mail/templates/enable-two-factor.template.tsx index 1433240..0cb4e3c 100644 --- a/backend/src/module/libs/mail/templates/enable-two-factor.template.tsx +++ b/backend/src/module/libs/mail/templates/enable-two-factor.template.tsx @@ -8,26 +8,29 @@ import { Section, Tailwind, Text, -} from '@react-email/components' -import * as React from 'react' +} from '@react-email/components'; +import * as React from 'react'; -interface EnableTwoFactorTemplateProps { - domain: string -} +type EnableTwoFactorTemplateProps = { + domain: string; +}; -export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps) { - const settingsLink = `${domain}/dashboard/settings` +export const EnableTwoFactorTemplate = ({ domain }: EnableTwoFactorTemplateProps) => { + const settingsLink = `${domain}/dashboard/settings`; return ( + Обеспечьте свою безопасность +
Защитите свой аккаунт с двухфакторной аутентификацией + Включите двухфакторную аутентификацию, чтобы повысить безопасность вашего аккаунта. @@ -37,12 +40,14 @@ export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps Почему это важно? + Двухфакторная аутентификация добавляет дополнительный уровень защиты, требуя код, который известен только вам. + Перейти в настройки аккаунта @@ -52,9 +57,10 @@ export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps Если у вас возникли вопросы, обращайтесь в службу поддержки по адресу {' '} + help@teastream.ru @@ -64,5 +70,5 @@ export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps - ) -} + ); +}; diff --git a/backend/src/module/libs/mail/templates/index.ts b/backend/src/module/libs/mail/templates/index.ts index 58a81f5..cbd7a3e 100644 --- a/backend/src/module/libs/mail/templates/index.ts +++ b/backend/src/module/libs/mail/templates/index.ts @@ -1,6 +1,6 @@ -export { default as VerificationTemplate } from './verification.template' -export { AccountDeletionTemplate } from './account-deletion.template' -export { DeactivateTemplate } from './deactivate.template' -export { EnableTwoFactorTemplate } from './enable-two-factor.template' -export { default as PasswordRecoveryTemplate } from './password-recovery.template' -export { VerifyChannelTemplate } from './verify-channel.template' +export { default as VerificationTemplate } from './verification.template'; +export { AccountDeletionTemplate } from './account-deletion.template'; +export { DeactivateTemplate } from './deactivate.template'; +export { EnableTwoFactorTemplate } from './enable-two-factor.template'; +export { default as PasswordRecoveryTemplate } from './password-recovery.template'; +export { VerifyChannelTemplate } from './verify-channel.template'; diff --git a/backend/src/module/libs/mail/templates/password-recovery.template.tsx b/backend/src/module/libs/mail/templates/password-recovery.template.tsx index 3a6500f..29fc0d6 100644 --- a/backend/src/module/libs/mail/templates/password-recovery.template.tsx +++ b/backend/src/module/libs/mail/templates/password-recovery.template.tsx @@ -1,35 +1,43 @@ -import * as React from 'react' -import { Body, Head, Heading, Preview, Section, Tailwind, Text, Link } from '@react-email/components' -import { SessionInfo } from '@/src/shared/types/session-metadata.types' -import { Html } from '@react-email/html' +import { + Body, Head, Heading, Preview, Section, Tailwind, Text, Link, +} from '@react-email/components'; +import { Html } from '@react-email/html'; +import * as React from 'react'; + +import type { SessionInfo } from '@/src/shared/types/session-metadata.types'; type PasswordRecoveryTemplateProps = { - domain: string - token: string - metadata: SessionInfo -} + domain: string; + token: string; + metadata: SessionInfo; +}; const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => { - const { domain, metadata, token } = props - const resetLink = `${domain}/account/recovery/${token}` + const { domain, metadata, token } = props; + const resetLink = `${domain}/account/recovery/${token}`; return ( + Сброс пароля +
Сброс пароля + Вы запросили сброс пароля для вашей учетной записи. + Чтобы создать новый пароль, нажмите на ссылку ниже: - + + Сбросить пароль
@@ -40,26 +48,32 @@ const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => { > Информация о запросе: +
  • 🌍 Расположение: {metadata.location.country} , + {metadata.location.city}
  • +
  • 📱 Операционная система: {metadata.device.os}
  • +
  • 🌐 Браузер: {metadata.device.browser}
  • +
  • 💻 IP-адрес: {metadata.ip}
+ Если вы не инициировали этот запрос, пожалуйста, игнорируйте это сообщение. @@ -69,9 +83,10 @@ const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => { Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу {' '} + help@teastream.ru @@ -81,7 +96,7 @@ const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => {
- ) -} + ); +}; -export default PasswordRecoveryTemplate +export default PasswordRecoveryTemplate; diff --git a/backend/src/module/libs/mail/templates/verification.template.tsx b/backend/src/module/libs/mail/templates/verification.template.tsx index 696d353..e5fe773 100644 --- a/backend/src/module/libs/mail/templates/verification.template.tsx +++ b/backend/src/module/libs/mail/templates/verification.template.tsx @@ -1,15 +1,17 @@ -import * as React from 'react' -import { Body, Head, Heading, Link, Preview, Section, Tailwind, Text } from '@react-email/components' -import { Html } from '@react-email/html' +import { + Body, Head, Heading, Link, Preview, Section, Tailwind, Text, +} from '@react-email/components'; +import { Html } from '@react-email/html'; +import * as React from 'react'; type VerificationTemplateProps = { - domain: string - token: string -} + domain: string; + token: string; +}; const VerificationTemplate = (props: VerificationTemplateProps) => { - const { domain, token } = props + const { domain, token } = props; - const verificationLink = `${domain}/account/verify?token=${token}` + const verificationLink = `${domain}/account/verify?token=${token}`; return ( @@ -31,7 +33,7 @@ const VerificationTemplate = (props: VerificationTemplateProps) => { Чтобы подтвердить свой адрес электронной почты, перейдите по следующей ссылке
- + Подтвердить почту
@@ -40,9 +42,10 @@ const VerificationTemplate = (props: VerificationTemplateProps) => { Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу {' '} + help@teastream.ru @@ -53,7 +56,7 @@ const VerificationTemplate = (props: VerificationTemplateProps) => {
- ) -} + ); +}; -export default VerificationTemplate +export default VerificationTemplate; diff --git a/backend/src/module/libs/mail/templates/verify-channel.template.tsx b/backend/src/module/libs/mail/templates/verify-channel.template.tsx index 27b4311..683bcb2 100644 --- a/backend/src/module/libs/mail/templates/verify-channel.template.tsx +++ b/backend/src/module/libs/mail/templates/verify-channel.template.tsx @@ -8,49 +8,52 @@ import { Section, Tailwind, Text, -} from '@react-email/components' -import * as React from 'react' +} from '@react-email/components'; +import * as React from 'react'; -export function VerifyChannelTemplate() { - return ( - - - Ваш канал верифицирован - - -
- - Поздравляем! Ваш канал верифицирован - - - Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок. - -
+export const VerifyChannelTemplate = () => ( + + -
- - Что это значит? - - - Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей. - -
+ Ваш канал верифицирован -
- - Если у вас есть вопросы, напишите нам на - {' '} - - help@teastream.ru - - . - -
- -
- - ) -} + + +
+ + Поздравляем! Ваш канал верифицирован + + + + Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок. + +
+ +
+ + Что это значит? + + + + Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей. + +
+ +
+ + Если у вас есть вопросы, напишите нам на + {' '} + + + help@teastream.ru + + . + +
+ +
+ +); diff --git a/backend/src/module/libs/storage/storage.module.ts b/backend/src/module/libs/storage/storage.module.ts index 380adb5..f7503cf 100644 --- a/backend/src/module/libs/storage/storage.module.ts +++ b/backend/src/module/libs/storage/storage.module.ts @@ -1,5 +1,6 @@ -import { Global, Module } from '@nestjs/common' -import { StorageService } from './storage.service' +import { Global, Module } from '@nestjs/common'; + +import { StorageService } from './storage.service'; @Global() @Module({ diff --git a/backend/src/module/libs/storage/storage.service.ts b/backend/src/module/libs/storage/storage.service.ts index 6ca06c1..456c9ad 100644 --- a/backend/src/module/libs/storage/storage.service.ts +++ b/backend/src/module/libs/storage/storage.service.ts @@ -4,15 +4,17 @@ import { PutObjectCommand, PutObjectCommandInput, S3Client, -} from '@aws-sdk/client-s3' -import { BadRequestException, Injectable } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' +} from '@aws-sdk/client-s3'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + import { ProcessEnv } from '../../../shared/types/env'; @Injectable() export class StorageService { - private readonly client: S3Client - private readonly bucket: string + private readonly client: S3Client; + + private readonly bucket: string; constructor( private readonly configService: ConfigService, @@ -24,9 +26,9 @@ export class StorageService { accessKeyId: this.configService.getOrThrow('S3_ACCESS_KEY_ID'), secretAccessKey: this.configService.getOrThrow('S3_SECRET_KEY_ID'), }, - }) + }); - this.bucket = this.configService.getOrThrow('S3_BUCKET_NAME') + this.bucket = this.configService.getOrThrow('S3_BUCKET_NAME'); } public async upload(buffer: Buffer, key: string, mimetype: string) { @@ -35,28 +37,25 @@ export class StorageService { Key: String(key), Body: buffer, ContentType: mimetype, - } + }; try { - await this.client.send(new PutObjectCommand(command)) - } - catch (e) { - throw new BadRequestException('Ошибка при загрузке файла') + await this.client.send(new PutObjectCommand(command)); + } catch { + throw new BadRequestException('Ошибка при загрузке файла'); } } public async remove(key: string) { const command: DeleteObjectCommandInput = { Bucket: this.bucket, - Key: String(key), - } + Key: key, + }; try { - await this.client.send(new DeleteObjectCommand(command)) - } - catch (e) { - console.log('e', e); - throw new BadRequestException('Ошибка при удалении файла') + await this.client.send(new DeleteObjectCommand(command)); + } catch { + throw new BadRequestException('Ошибка при удалении файла'); } } } diff --git a/backend/src/module/libs/stripe/stripe.module.ts b/backend/src/module/libs/stripe/stripe.module.ts index f615684..68dd22b 100644 --- a/backend/src/module/libs/stripe/stripe.module.ts +++ b/backend/src/module/libs/stripe/stripe.module.ts @@ -1,6 +1,7 @@ -import { DynamicModule, Module } from '@nestjs/common' -import { StripeOptionSymbol, TypeStripeAsyncOptions, TypeStripeOptions } from './types/stripe.type' -import { StripeService } from './stripe.service' +import { DynamicModule, Module } from '@nestjs/common'; + +import { StripeService } from './stripe.service'; +import { StripeOptionSymbol, TypeStripeAsyncOptions, TypeStripeOptions } from './types/stripe.type'; @Module({}) export class StripeModule { @@ -16,7 +17,7 @@ export class StripeModule { ], exports: [StripeService], global: true, - } + }; } public static registerAsync(options: TypeStripeAsyncOptions): DynamicModule { @@ -33,6 +34,6 @@ export class StripeModule { ], exports: [StripeService], global: true, - } + }; } } diff --git a/backend/src/module/libs/stripe/stripe.service.ts b/backend/src/module/libs/stripe/stripe.service.ts index e82c53b..0c78874 100644 --- a/backend/src/module/libs/stripe/stripe.service.ts +++ b/backend/src/module/libs/stripe/stripe.service.ts @@ -1,6 +1,7 @@ -import { StripeOptionSymbol, TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type' -import { Inject, Injectable } from '@nestjs/common' -import Stripe from 'stripe' +import { Inject, Injectable } from '@nestjs/common'; +import Stripe from 'stripe'; + +import { StripeOptionSymbol, TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type'; @Injectable() export class StripeService extends Stripe { @@ -8,6 +9,6 @@ export class StripeService extends Stripe { @Inject(StripeOptionSymbol) private readonly options: TypeStripeOptions, ) { - super(options.apiKey, options.config) + super(options.apiKey, options.config); } } diff --git a/backend/src/module/libs/stripe/types/stripe.type.ts b/backend/src/module/libs/stripe/types/stripe.type.ts index a03e4e8..1a38a1d 100644 --- a/backend/src/module/libs/stripe/types/stripe.type.ts +++ b/backend/src/module/libs/stripe/types/stripe.type.ts @@ -1,12 +1,11 @@ -import { FactoryProvider, ModuleMetadata } from '@nestjs/common' -import Stripe from 'stripe' +import type { FactoryProvider, ModuleMetadata } from '@nestjs/common'; +import type Stripe from 'stripe'; -export const StripeOptionSymbol = Symbol('StripeOptionSymbol') +export const StripeOptionSymbol = Symbol('StripeOptionSymbol'); export type TypeStripeOptions = { - apiKey: string - config?: Stripe.StripeConfig -} + apiKey: string; + config?: Stripe.StripeConfig; +}; -export type TypeStripeAsyncOptions = Pick - & Pick, 'useFactory' | 'inject'> +export type TypeStripeAsyncOptions = Pick, 'inject' | 'useFactory'> & Pick; diff --git a/backend/src/module/libs/telegram/telegram.button.ts b/backend/src/module/libs/telegram/telegram.button.ts index d0091e5..d444de4 100644 --- a/backend/src/module/libs/telegram/telegram.button.ts +++ b/backend/src/module/libs/telegram/telegram.button.ts @@ -1,4 +1,4 @@ -import { Markup } from 'telegraf' +import { Markup } from 'telegraf'; export const BUTTONS = { authSuccess: Markup.inlineKeyboard([ @@ -14,4 +14,4 @@ export const BUTTONS = { 'https://teastream.ru/dashboard/settings', ), ]), -} +}; diff --git a/backend/src/module/libs/telegram/telegram.message.ts b/backend/src/module/libs/telegram/telegram.message.ts index 082267d..5d1d428 100644 --- a/backend/src/module/libs/telegram/telegram.message.ts +++ b/backend/src/module/libs/telegram/telegram.message.ts @@ -1,5 +1,5 @@ -import { SessionInfo } from '@/src/shared/types/session-metadata.types' -import type { SponsorshipPlan, User } from '@prisma/generated' +import type { SessionInfo } from '@/src/shared/types/session-metadata.types'; +import type { SponsorshipPlan, User } from '@prisma/generated'; export const MESSAGES = { welcome: @@ -14,68 +14,61 @@ export const MESSAGES = { + `👥 Количество подписчиков: ${followersCount}\n` + `📝 О себе: ${user.bio ?? 'Не указано'}\n\n` + '🔧 Нажмите на кнопку ниже, чтобы перейти к настройкам профиля.', - follows: (user: User) => - `📺 ${user.name}`, - resetPassword: (token: string, metadata: SessionInfo) => - `🔒 Сброс пароля\n\n` - + `Вы запросили сброс пароля для вашей учетной записи на платформе TeaStream.\n\n` - + `Чтобы создать новый пароль, пожалуйста, перейдите по следующей ссылке:\n\n` + follows: (user: User) => `📺 ${user.name}`, + resetPassword: (token: string, metadata: SessionInfo) => '🔒 Сброс пароля\n\n' + + 'Вы запросили сброс пароля для вашей учетной записи на платформе TeaStream.\n\n' + + 'Чтобы создать новый пароль, пожалуйста, перейдите по следующей ссылке:\n\n' + `Сбросить пароль\n\n` + `📅 Дата запроса: ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n` - + `🖥️ Информация о запросе:\n\n` + + '🖥️ Информация о запросе:\n\n' + `🌍 Расположение: ${metadata.location.country}, ${metadata.location.city}\n` + `📱 Операционная система: ${metadata.device.os}\n` + `🌐 Браузер: ${metadata.device.browser}\n` + `💻 IP-адрес: ${metadata.ip}\n\n` - + `Если вы не делали этот запрос, просто проигнорируйте это сообщение.\n\n` - + `Спасибо за использование TeaStream! 🚀`, - deactivate: (token: string, metadata: SessionInfo) => - `⚠️ Запрос на деактивацию аккаунта\n\n` - + `Вы инициировали процесс деактивации вашего аккаунта на платформе Teastream.\n\n` - + `Для завершения операции, пожалуйста, подтвердите свой запрос, введя следующий код подтверждения:\n\n` + + 'Если вы не делали этот запрос, просто проигнорируйте это сообщение.\n\n' + + 'Спасибо за использование TeaStream! 🚀', + deactivate: (token: string, metadata: SessionInfo) => '⚠️ Запрос на деактивацию аккаунта\n\n' + + 'Вы инициировали процесс деактивации вашего аккаунта на платформе Teastream.\n\n' + + 'Для завершения операции, пожалуйста, подтвердите свой запрос, введя следующий код подтверждения:\n\n' + `Код подтверждения: ${token}\n\n` + `📅 Дата запроса: ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n` - + `🖥️ Информация о запросе:\n\n` + + '🖥️ Информация о запросе:\n\n' + `• 🌍 Расположение: ${metadata.location.country}, ${metadata.location.city}\n` + `• 📱 Операционная система: ${metadata.device.os}\n` + `• 🌐 Браузер: ${metadata.device.browser}\n` + `• 💻 IP-адрес: ${metadata.ip}\n\n` - + `Что произойдет после деактивации?\n\n` - + `1. Вы автоматически выйдете из системы и потеряете доступ к аккаунту.\n` - + `2. Если вы не отмените деактивацию в течение 7 дней, ваш аккаунт будет безвозвратно удален со всей вашей информацией, данными и подписками.\n\n` - + `⏳ Обратите внимание: Если в течение 7 дней вы передумаете, вы можете обратиться в нашу поддержку для восстановления доступа к вашему аккаунту до момента его полного удаления.\n\n` - + `После удаления аккаунта восстановить его будет невозможно, и все данные будут потеряны без возможности восстановления.\n\n` - + `Если вы передумали, просто проигнорируйте это сообщение. Ваш аккаунт останется активным.\n\n` - + `Спасибо, что пользуетесь TeaStream! Мы всегда рады видеть вас на нашей платформе и надеемся, что вы останетесь с нами. 🚀\n\n` - + `С уважением,\n` - + `Команда TeaStream`, + + 'Что произойдет после деактивации?\n\n' + + '1. Вы автоматически выйдете из системы и потеряете доступ к аккаунту.\n' + + '2. Если вы не отмените деактивацию в течение 7 дней, ваш аккаунт будет безвозвратно удален со всей вашей информацией, данными и подписками.\n\n' + + '⏳ Обратите внимание: Если в течение 7 дней вы передумаете, вы можете обратиться в нашу поддержку для восстановления доступа к вашему аккаунту до момента его полного удаления.\n\n' + + 'После удаления аккаунта восстановить его будет невозможно, и все данные будут потеряны без возможности восстановления.\n\n' + + 'Если вы передумали, просто проигнорируйте это сообщение. Ваш аккаунт останется активным.\n\n' + + 'Спасибо, что пользуетесь TeaStream! Мы всегда рады видеть вас на нашей платформе и надеемся, что вы останетесь с нами. 🚀\n\n' + + 'С уважением,\n' + + 'Команда TeaStream', accountDeleted: - `⚠️ Ваш аккаунт был полностью удалён.\n\n` - + `Ваш аккаунт был полностью стерт из базы данных Teastream. Все ваши данные и информация были удалены безвозвратно. ❌\n\n` - + `🔒 Вы больше не будете получать уведомления в Telegram и на почту.\n\n` - + `Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке:\n` - + `Зарегистрироваться на Teastream\n\n` - + `Спасибо, что были с нами! Мы всегда будем рады видеть вас на платформе. 🚀\n\n` - + `С уважением,\n` - + `Команда TeaStream`, - streamStart: (channel: User) => - `📡 На канале ${channel.displayName} началась трансляция!\n\n` + '⚠️ Ваш аккаунт был полностью удалён.\n\n' + + 'Ваш аккаунт был полностью стерт из базы данных Teastream. Все ваши данные и информация были удалены безвозвратно. ❌\n\n' + + '🔒 Вы больше не будете получать уведомления в Telegram и на почту.\n\n' + + 'Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке:\n' + + 'Зарегистрироваться на Teastream\n\n' + + 'Спасибо, что были с нами! Мы всегда будем рады видеть вас на платформе. 🚀\n\n' + + 'С уважением,\n' + + 'Команда TeaStream', + streamStart: (channel: User) => `📡 На канале ${channel.displayName} началась трансляция!\n\n` + `Смотрите здесь: Перейти к трансляции`, - newFollowing: (follower: User, followersCount: number) => - - `У вас новый подписчик!\n\nЭто пользователь ${follower.displayName}\n\nИтоговое количество подписчиков на вашем канале: ${followersCount}`, + newFollowing: (follower: User, followersCount: number) => `У вас новый подписчик!\n\nЭто пользователь ${follower.displayName}\n\nИтоговое количество подписчиков на вашем канале: ${followersCount}`, enableTwoFactor: - `🔐 Обеспечьте свою безопасность!\n\n` - + `Включите двухфакторную аутентификацию в настройках аккаунта.`, + '🔐 Обеспечьте свою безопасность!\n\n' + + 'Включите двухфакторную аутентификацию в настройках аккаунта.', verifyChannel: - `🎉 Поздравляем! Ваш канал верифицирован\n\n` - + `Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.\n\n` - + `Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.\n\n` - + `Спасибо, что вы с нами и продолжаете развивать свой канал вместе с TeaStream!`, - newSponsorship: (plan: SponsorshipPlan, sponsor: User) => - `🎉 Новое спонсор!\n\n` + '🎉 Поздравляем! Ваш канал верифицирован\n\n' + + 'Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.\n\n' + + 'Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.\n\n' + + 'Спасибо, что вы с нами и продолжаете развивать свой канал вместе с TeaStream!', + newSponsorship: (plan: SponsorshipPlan, sponsor: User) => '🎉 Новое спонсор!\n\n' + `Вы получили новое спонсорство на план ${plan.title}.\n` + `💰 Сумма: ${plan.price} ₽\n` + `👤 Спонсор: ${sponsor.displayName}\n` + `📅 Дата оформления: ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}`, -} +}; diff --git a/backend/src/module/libs/telegram/telegram.module.ts b/backend/src/module/libs/telegram/telegram.module.ts index ed73a50..7d211b8 100644 --- a/backend/src/module/libs/telegram/telegram.module.ts +++ b/backend/src/module/libs/telegram/telegram.module.ts @@ -1,8 +1,10 @@ -import { getTelegrafOptions } from '@/src/core/config/telegraf.config' -import { Global, Module } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' -import { TelegrafModule } from 'nestjs-telegraf' -import { TelegramService } from './telegram.service' +import { Global, Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TelegrafModule } from 'nestjs-telegraf'; + +import { getTelegrafOptions } from '@/src/core/config/telegraf.config'; + +import { TelegramService } from './telegram.service'; @Global() @Module({ diff --git a/backend/src/module/libs/telegram/telegram.service.ts b/backend/src/module/libs/telegram/telegram.service.ts index f9f775d..c8a8873 100644 --- a/backend/src/module/libs/telegram/telegram.service.ts +++ b/backend/src/module/libs/telegram/telegram.service.ts @@ -1,37 +1,37 @@ -import { SessionInfo } from '@/src/shared/types/session-metadata.types' -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' +import { Injectable, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Action, Command, Ctx, Start, Update, -} from 'nestjs-telegraf' -import { Context, Telegraf } from 'telegraf' +} from 'nestjs-telegraf'; +import { Context, Telegraf } from 'telegraf'; -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { BUTTONS } from '@/src/module/libs/telegram/telegram.button' -import { MESSAGES } from '@/src/module/libs/telegram/telegram.message' -import { SponsorshipPlan, TokenType, User } from '@prisma/generated' +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { BUTTONS } from '@/src/module/libs/telegram/telegram.button'; +import { MESSAGES } from '@/src/module/libs/telegram/telegram.message'; +import { SessionInfo } from '@/src/shared/types/session-metadata.types'; +import { SponsorshipPlan, TokenType, User } from '@prisma/generated'; -import { ProcessEnv } from '../../../shared/types/env' +import { ProcessEnv } from '../../../shared/types/env'; @Update() @Injectable() export class TelegramService extends Telegraf { - private readonly _token: string + private readonly _token: string; constructor( private readonly prismaService: PrismaService, private readonly configService: ConfigService, ) { - super(configService.getOrThrow('TELEGRAM_BOT_TOKEN')) - this._token = configService.getOrThrow('TELEGRAM_BOT_TOKEN') + super(configService.getOrThrow('TELEGRAM_BOT_TOKEN')); + this._token = configService.getOrThrow('TELEGRAM_BOT_TOKEN'); } @Start() public async onStart(@Ctx() ctx: Context) { - const chatId = ctx.chat?.id.toString() + const chatId = ctx.chat?.id.toString(); // @ts-ignore - const token = ctx.message.text.split(' ')[1] as string + const token = ctx.message.text.split(' ')[1] as string; if (token) { const authToken = await this.prismaService.token.findUnique({ @@ -39,124 +39,130 @@ export class TelegramService extends Telegraf { token, type: TokenType.TELEGRAM_AUTH, }, - }) + }); if (!authToken?.userId) { - await ctx.reply('Токен не найден') - return + await ctx.reply('Токен не найден'); + + return; } - const hasExpired = new Date(authToken.expiresIn) < new Date() + const hasExpired = new Date(authToken.expiresIn) < new Date(); if (hasExpired) { - await ctx.reply(MESSAGES.invalidToken) - return + await ctx.reply(MESSAGES.invalidToken); + + return; } - await this.connectTelegram(authToken.userId, chatId!) + await this.connectTelegram(authToken.userId, chatId!); await this.prismaService.token.delete({ where: { id: authToken.id }, - }) + }); - await ctx.replyWithHTML(MESSAGES.authSuccess, BUTTONS.authSuccess) - return + await ctx.replyWithHTML(MESSAGES.authSuccess, BUTTONS.authSuccess); + + return; } - const user = await this.findUserByChatId(chatId!) + const user = await this.findUserByChatId(chatId!); if (user) { - return await this.onMe(ctx) + await this.onMe(ctx); + + return; } - await ctx.replyWithHTML(MESSAGES.welcome, BUTTONS.profile) + await ctx.replyWithHTML(MESSAGES.welcome, BUTTONS.profile); } @Command('me') @Action('me') public async onMe(@Ctx() ctx: Context) { - const chatId = ctx.chat?.id.toString() + const chatId = ctx.chat?.id.toString(); if (typeof chatId === 'undefined') { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } - const user = await this.findUserByChatId(chatId) + const user = await this.findUserByChatId(chatId); if (!user) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } const followersCount = await this.prismaService.follow.count({ where: { followingId: user.id }, - }) + }); - await ctx.replyWithHTML(MESSAGES.profile(user, followersCount), BUTTONS.profile) + await ctx.replyWithHTML(MESSAGES.profile(user, followersCount), BUTTONS.profile); } @Command('follows') @Action('follows') public async onFollow(@Ctx() ctx: Context) { - const chatId = ctx.chat?.id + const chatId = ctx.chat?.id; if (typeof chatId === 'undefined') { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } - const user = await this.findUserByChatId(chatId.toString()) + const user = await this.findUserByChatId(chatId.toString()); if (!user) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } const follows = await this.prismaService.follow.findMany({ where: { followerId: user.id }, include: { following: true }, - }) + }); if (follows.length > 0) { - const followList = follows.map(follow => MESSAGES.follows(follow.following)).join('\n') - const message = `Каналы, на которые Вы подписаны\n\n${followList}` - await ctx.replyWithHTML(message) - return + const followList = follows.map((follow) => MESSAGES.follows(follow.following)).join('\n'); + const message = `Каналы, на которые Вы подписаны\n\n${followList}`; + await ctx.replyWithHTML(message); + + return; } - await ctx.replyWithHTML('❌ У Вас нет подписок') + await ctx.replyWithHTML('❌ У Вас нет подписок'); } public async sendPasswordResetToken(chatId: string, token: string, metadata: SessionInfo) { - await this.telegram.sendMessage(chatId, MESSAGES.resetPassword(token, metadata), { parse_mode: 'HTML' }) + await this.telegram.sendMessage(chatId, MESSAGES.resetPassword(token, metadata), { parse_mode: 'HTML' }); } public async sendDeactivateToken(chatId: string, token: string, metadata: SessionInfo) { - await this.telegram.sendMessage(chatId, MESSAGES.deactivate(token, metadata), { parse_mode: 'HTML' }) + await this.telegram.sendMessage(chatId, MESSAGES.deactivate(token, metadata), { parse_mode: 'HTML' }); } public async sendAccountDeletionToken(chatId: string) { - await this.telegram.sendMessage(chatId, MESSAGES.accountDeleted, { parse_mode: 'HTML' }) + await this.telegram.sendMessage(chatId, MESSAGES.accountDeleted, { parse_mode: 'HTML' }); } public async sendStreamStart(chatId: string, channel: User) { - await this.telegram.sendMessage(chatId, MESSAGES.streamStart(channel), { parse_mode: 'HTML' }) + await this.telegram.sendMessage(chatId, MESSAGES.streamStart(channel), { parse_mode: 'HTML' }); } public async sendNewSponsorship(chatId: string, plan: SponsorshipPlan, sponsor: User) { - await this.telegram.sendMessage(chatId, MESSAGES.newSponsorship(plan, sponsor), { parse_mode: 'HTML' }) + await this.telegram.sendMessage(chatId, MESSAGES.newSponsorship(plan, sponsor), { parse_mode: 'HTML' }); } public async sendNewFollowing(chatId: string, follower: User) { - const user = await this.findUserByChatId(chatId) + const user = await this.findUserByChatId(chatId); if (!user) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } - await this.telegram.sendMessage(chatId, MESSAGES.newFollowing(follower, user.followings.length), { parse_mode: 'HTML' }) + await this.telegram.sendMessage(chatId, MESSAGES.newFollowing(follower, user.followings.length), { parse_mode: 'HTML' }); } public async sendEnableTwoFactor(chatId: string) { - return this.telegram.sendMessage(chatId, MESSAGES.enableTwoFactor, { parse_mode: 'HTML' }) + return this.telegram.sendMessage(chatId, MESSAGES.enableTwoFactor, { parse_mode: 'HTML' }); } public async sendVerifyChannel(chatId: string) { - return this.telegram.sendMessage(chatId, MESSAGES.verifyChannel, { parse_mode: 'HTML' }) + return this.telegram.sendMessage(chatId, MESSAGES.verifyChannel, { parse_mode: 'HTML' }); } private async connectTelegram(userId: string, chatId: string) { return this.prismaService.user.update({ where: { id: userId }, data: { telegramId: chatId }, - }) + }); } private async findUserByChatId(chatId: string) { @@ -166,6 +172,6 @@ export class TelegramService extends Telegraf { followings: true, followers: true, }, - }) + }); } } diff --git a/backend/src/module/notification/index.ts b/backend/src/module/notification/index.ts new file mode 100644 index 0000000..1c2c0fc --- /dev/null +++ b/backend/src/module/notification/index.ts @@ -0,0 +1,2 @@ +export { NotificationSettingsModel, ChangeNotificationsSettingsResponse } from './models/notification-settings.model'; +export { NotificationModel } from './models/notification.model'; diff --git a/backend/src/module/notification/inputs/change-notification-settings.input.ts b/backend/src/module/notification/inputs/change-notification-settings.input.ts index 3ccb0e6..fba759d 100644 --- a/backend/src/module/notification/inputs/change-notification-settings.input.ts +++ b/backend/src/module/notification/inputs/change-notification-settings.input.ts @@ -1,13 +1,13 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsBoolean } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsBoolean } from 'class-validator'; @InputType() export class ChangeNotificationSettingsInput { @Field(() => Boolean) @IsBoolean() - public siteNotifications: boolean + public siteNotifications: boolean; @Field(() => Boolean) @IsBoolean() - public telegramNotifications: boolean + public telegramNotifications: boolean; } diff --git a/backend/src/module/notification/models/notification-settings.model.ts b/backend/src/module/notification/models/notification-settings.model.ts index 74b16da..f3b6086 100644 --- a/backend/src/module/notification/models/notification-settings.model.ts +++ b/backend/src/module/notification/models/notification-settings.model.ts @@ -1,37 +1,38 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { Field, ObjectType } from '@nestjs/graphql' +import { Field, ObjectType } from '@nestjs/graphql'; -import type { NotificationSettings } from '@/prisma/generated' +import { UserModel } from '@/src/module/auth/account/models/user.model'; + +import type { NotificationSettings } from '@/prisma/generated'; @ObjectType() export class NotificationSettingsModel implements NotificationSettings { @Field(() => String) - public id: string + public id: string; @Field(() => Boolean) - public siteNotifications: boolean + public siteNotifications: boolean; @Field(() => Boolean) - public telegramNotifications: boolean + public telegramNotifications: boolean; @Field(() => UserModel) - public user: UserModel + public user: UserModel; @Field(() => String) - public userId: string + public userId: string; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } @ObjectType() export class ChangeNotificationsSettingsResponse { @Field(() => NotificationSettingsModel) - public notificationSettings: NotificationSettingsModel + public notificationSettings: NotificationSettingsModel; @Field(() => String, { nullable: true }) - public telegramAuthToken?: string + public telegramAuthToken?: string; } diff --git a/backend/src/module/notification/models/notification.model.ts b/backend/src/module/notification/models/notification.model.ts index ab5fae0..790a4df 100644 --- a/backend/src/module/notification/models/notification.model.ts +++ b/backend/src/module/notification/models/notification.model.ts @@ -1,36 +1,35 @@ -import { Field, ObjectType, registerEnumType } from '@nestjs/graphql' +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; -import { type Notification, NotificationType } from '@/prisma/generated' - -import { UserModel } from '../../auth/account/models/user.model' +import { type Notification, NotificationType } from '@/prisma/generated'; +import { UserModel } from '@/src/module/auth'; registerEnumType(NotificationType, { name: 'NotificationType', -}) +}); @ObjectType() export class NotificationModel implements Notification { @Field(() => String) - public id: string + public id: string; @Field(() => String) - public text: string + public text: string; @Field(() => NotificationType) - public type: NotificationType + public type: NotificationType; @Field(() => Boolean) - public isRead: boolean + public isRead: boolean; @Field(() => UserModel) - public user: UserModel + public user: UserModel; @Field(() => String) - public userId: string + public userId: string; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } diff --git a/backend/src/module/notification/notification.module.ts b/backend/src/module/notification/notification.module.ts index 94b9aa0..7a198e6 100644 --- a/backend/src/module/notification/notification.module.ts +++ b/backend/src/module/notification/notification.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common' -import { NotificationService } from './notification.service' -import { NotificationResolver } from './notification.resolver' +import { Module } from '@nestjs/common'; + +import { NotificationResolver } from './notification.resolver'; +import { NotificationService } from './notification.service'; @Module({ providers: [NotificationResolver, NotificationService], diff --git a/backend/src/module/notification/notification.resolver.ts b/backend/src/module/notification/notification.resolver.ts index ca03a0c..6fe6a40 100644 --- a/backend/src/module/notification/notification.resolver.ts +++ b/backend/src/module/notification/notification.resolver.ts @@ -1,11 +1,15 @@ -import { ChangeNotificationSettingsInput } from '@/src/module/notification/inputs/change-notification-settings.input' -import { ChangeNotificationsSettingsResponse } from '@/src/module/notification/models/notification-settings.model' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { NotificationService } from './notification.service' -import { NotificationModel } from './models/notification.model' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; + +import { ChangeNotificationSettingsInput } from '@/src/module/notification/inputs/change-notification-settings.input'; +import { ChangeNotificationsSettingsResponse } from '@/src/module/notification/models/notification-settings.model'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { NotificationModel } from './models/notification.model'; +import { NotificationService } from './notification.service'; @Resolver('Notification') export class NotificationResolver { @@ -13,14 +17,14 @@ export class NotificationResolver { @Authorization() @Query(() => Number, { name: 'findUnreadNotificationsCount' }) - public findUnreadCount(@Authorized() user: User) { - return this.notificationService.findUnreadCount(user) + public async findUnreadCount(@Authorized() user: User) { + return this.notificationService.findUnreadCount(user); } @Authorization() @Query(() => [NotificationModel], { name: 'findNotificationByUser' }) - public findByUser(@Authorized() user: User) { - return this.notificationService.findByUser(user) + public async findByUser(@Authorized() user: User) { + return this.notificationService.findByUser(user); } @Authorization() @@ -29,6 +33,6 @@ export class NotificationResolver { @Authorized() user: User, @Args('data') input: ChangeNotificationSettingsInput, ) { - return this.notificationService.changeSettings(user, input) + return this.notificationService.changeSettings(user, input); } } diff --git a/backend/src/module/notification/notification.service.ts b/backend/src/module/notification/notification.service.ts index 532a8d9..3153d82 100644 --- a/backend/src/module/notification/notification.service.ts +++ b/backend/src/module/notification/notification.service.ts @@ -1,10 +1,12 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { ChangeNotificationSettingsInput } from '@/src/module/notification/inputs/change-notification-settings.input' -import { generateToken } from '@/src/shared/util/generate-token.util' -import { Injectable } from '@nestjs/common' -import { $Enums, SponsorshipPlan, User } from '@prisma/generated' -import TokenType = $Enums.TokenType -import NotificationType = $Enums.NotificationType +import { Injectable } from '@nestjs/common'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { ChangeNotificationSettingsInput } from '@/src/module/notification/inputs/change-notification-settings.input'; +import { generateToken } from '@/src/shared/util/generate-token.util'; +import { $Enums, SponsorshipPlan, User } from '@prisma/generated'; + +import TokenType = $Enums.TokenType; +import NotificationType = $Enums.NotificationType; @Injectable() export class NotificationService { @@ -19,7 +21,7 @@ export class NotificationService { isRead: false, userId: user.id, }, - }) + }); } public async findByUser(user: User) { @@ -29,16 +31,16 @@ export class NotificationService { userId: user.id, }, data: { isRead: true }, - }) + }); return this.prismaService.notification.findMany({ where: { userId: user.id }, orderBy: { createdAt: 'desc' }, - }) + }); } public async changeSettings(user: User, input: ChangeNotificationSettingsInput) { - const { siteNotifications, telegramNotifications } = input + const { siteNotifications, telegramNotifications } = input; const notificationSetting = await this.prismaService.notificationSettings.upsert({ where: { userId: user.id }, @@ -49,38 +51,38 @@ export class NotificationService { }, update: { siteNotifications, telegramNotifications }, include: { user: true }, - }) + }); if (notificationSetting.telegramNotifications && !notificationSetting.user.telegramId) { const telegramAuthToken = await generateToken( this.prismaService, user, TokenType.TELEGRAM_AUTH, - ) + ); return { notificationSetting, telegramAuthToken: telegramAuthToken.token, - } + }; } if (!notificationSetting.telegramNotifications && notificationSetting.user.telegramId) { await this.prismaService.user.update({ where: { id: user.id }, data: { telegramId: null }, - }) + }); - return { notificationSetting } + return { notificationSetting }; } - return { notificationSetting } + return { notificationSetting }; } public async createStreamStart(userId: string, channel: User) { return this.prismaService.notification.create({ data: { text: `Не пропустите! -

Присоединяйтесь к стриму на канале ${channel.displayName}.

`, +

Присоединяйтесь к стриму на канале ${channel.displayName}.

`, type: NotificationType.STREAM_START, user: { connect: { @@ -89,14 +91,14 @@ export class NotificationService { }, }, - }) + }); } public async createNewFollowing(userId: string, follower: User) { return this.prismaService.notification.create({ data: { text: `У вас новый подписчик! -

Это пользователь ${follower.displayName}.

`, +

Это пользователь ${follower.displayName}.

`, type: NotificationType.NEW_FOLLOWER, user: { connect: { @@ -104,7 +106,7 @@ export class NotificationService { }, }, }, - }) + }); } public async createNewSponsorship(userId: string, plan: SponsorshipPlan, sponsor: User) { @@ -119,28 +121,28 @@ export class NotificationService { }, }, }, - }) + }); } public async createEnableTwoFactor(userId: string) { return this.prismaService.notification.create({ data: { text: `Обеспечьте свою безопасность! -

Включите двухфакторную аутентификацию в настройках вашего аккаунта, чтобы повысить уровень защиты.

`, +

Включите двухфакторную аутентификацию в настройках вашего аккаунта, чтобы повысить уровень защиты.

`, type: NotificationType.ENABLE_TWO_FACTOR, userId, }, - }) + }); } public async createVerifyChannel(userId: string) { return this.prismaService.notification.create({ data: { text: `Поздравляем! -

Ваш канал верифицирован, и теперь рядом с вашим каналом будет галочка.

`, +

Ваш канал верифицирован, и теперь рядом с вашим каналом будет галочка.

`, type: NotificationType.VERIFIED_CHANNEL, userId, }, - }) + }); } } diff --git a/backend/src/module/sponsorship/index.ts b/backend/src/module/sponsorship/index.ts new file mode 100644 index 0000000..c91d0a6 --- /dev/null +++ b/backend/src/module/sponsorship/index.ts @@ -0,0 +1,4 @@ +export { PlanModel } from './plan/models/plan.model'; +export { SubscriptionModel } from './subscription/model/subscription.model'; +export { MakePaymentModel } from './transaction/model/make-payment.model'; +export { TransactionModel } from './transaction/model/transaction.model'; diff --git a/backend/src/module/sponsorship/plan/inputs/create-plan.input.ts b/backend/src/module/sponsorship/plan/inputs/create-plan.input.ts index 89b1208..953cae3 100644 --- a/backend/src/module/sponsorship/plan/inputs/create-plan.input.ts +++ b/backend/src/module/sponsorship/plan/inputs/create-plan.input.ts @@ -1,20 +1,22 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { + IsNotEmpty, IsNumber, IsOptional, IsString, +} from 'class-validator'; @InputType() export class CreatePlanInput { @Field(() => String) @IsString() @IsNotEmpty() - title: string + title: string; @Field(() => String, { nullable: true }) @IsString() @IsOptional() - description?: string + description?: string; @Field(() => Number) @IsNumber() @IsNotEmpty() - price: number + price: number; } diff --git a/backend/src/module/sponsorship/plan/models/plan.model.ts b/backend/src/module/sponsorship/plan/models/plan.model.ts index 01785e5..898ca0a 100644 --- a/backend/src/module/sponsorship/plan/models/plan.model.ts +++ b/backend/src/module/sponsorship/plan/models/plan.model.ts @@ -1,36 +1,37 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { Field, ID, ObjectType } from '@nestjs/graphql' -import { SponsorshipPlan } from '@prisma/generated' +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { UserModel } from '@/src/module/auth/account/models/user.model'; +import { SponsorshipPlan } from '@prisma/generated'; @ObjectType() export class PlanModel implements SponsorshipPlan { @Field(() => ID) - id: string + id: string; @Field(() => String) - title: string + title: string; @Field(() => String, { nullable: true }) - description: string + description: string; @Field(() => Number) - price: number + price: number; @Field(() => UserModel) - channel: UserModel + channel: UserModel; @Field(() => ID) - channelId: string + channelId: string; @Field(() => ID) - stripeProductId: string + stripeProductId: string; @Field(() => ID) - stripePlanId: string + stripePlanId: string; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } diff --git a/backend/src/module/sponsorship/plan/plan.module.ts b/backend/src/module/sponsorship/plan/plan.module.ts index 0d1060c..4b5bb4a 100644 --- a/backend/src/module/sponsorship/plan/plan.module.ts +++ b/backend/src/module/sponsorship/plan/plan.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; -import { PlanService } from './plan.service'; + import { PlanResolver } from './plan.resolver'; +import { PlanService } from './plan.service'; @Module({ providers: [PlanResolver, PlanService], diff --git a/backend/src/module/sponsorship/plan/plan.resolver.ts b/backend/src/module/sponsorship/plan/plan.resolver.ts index d78d5af..f2b39aa 100644 --- a/backend/src/module/sponsorship/plan/plan.resolver.ts +++ b/backend/src/module/sponsorship/plan/plan.resolver.ts @@ -1,10 +1,14 @@ -import { CreatePlanInput } from '@/src/module/sponsorship/plan/inputs/create-plan.input' -import { PlanModel } from '@/src/module/sponsorship/plan/models/plan.model' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { PlanService } from './plan.service' -import { Authorization } from '@/src/shared/decorators/auth.decorator' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; + +import { CreatePlanInput } from '@/src/module/sponsorship/plan/inputs/create-plan.input'; +import { PlanModel } from '@/src/module/sponsorship/plan/models/plan.model'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { PlanService } from './plan.service'; @Resolver('Plan') export class PlanResolver { @@ -12,24 +16,24 @@ export class PlanResolver { @Authorization() @Query(() => [PlanModel], { name: 'findMySponsorshipPlans' }) - public findMyPlans(@Authorized() user: User) { - return this.planService.findMyPlans(user) + public async findMyPlans(@Authorized() user: User) { + return this.planService.findMyPlans(user); } @Authorization() @Mutation(() => PlanModel, { name: 'createSponsorshipPlan' }) - public createPlan( + public async createPlan( @Authorized() user: User, @Args('data') input: CreatePlanInput, ) { - return this.planService.create(user, input) + return this.planService.create(user, input); } @Authorization() @Mutation(() => PlanModel, { name: 'removeSponsorshipPlan' }) - public removePlan( + public async removePlan( @Args('planId') planId: string, ) { - return this.planService.remove(planId) + return this.planService.remove(planId); } } diff --git a/backend/src/module/sponsorship/plan/plan.service.ts b/backend/src/module/sponsorship/plan/plan.service.ts index 246addd..6d4f50d 100644 --- a/backend/src/module/sponsorship/plan/plan.service.ts +++ b/backend/src/module/sponsorship/plan/plan.service.ts @@ -1,8 +1,9 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { StripeService } from '@/src/module/libs/stripe/stripe.service' -import { CreatePlanInput } from '@/src/module/sponsorship/plan/inputs/create-plan.input' -import { Injectable, NotFoundException } from '@nestjs/common' -import { User } from '@prisma/generated' +import { Injectable, NotFoundException } from '@nestjs/common'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { StripeService } from '@/src/module/libs/stripe/stripe.service'; +import { CreatePlanInput } from '@/src/module/sponsorship/plan/inputs/create-plan.input'; +import { User } from '@prisma/generated'; @Injectable() export class PlanService { @@ -15,22 +16,22 @@ export class PlanService { public async findMyPlans(user: User) { return this.prismaService.sponsorshipPlan.findMany({ where: { channelId: user.id }, - }) + }); } public async create(user: User, input: CreatePlanInput) { - const { description, price, title } = input + const { description, price, title } = input; const channel = await this.prismaService.user.findUnique({ where: { id: user.id }, - }) + }); if (!channel) { - throw new NotFoundException('Канал не найден') + throw new NotFoundException('Канал не найден'); } if (!channel.isVerified) { - throw new NotFoundException('Создание планов доступно только для верифицированных каналов') + throw new NotFoundException('Создание планов доступно только для верифицированных каналов'); } const stripePlan = await this.stripeService.plans.create({ @@ -40,7 +41,7 @@ export class PlanService { product: { name: title, }, - }) + }); return this.prismaService.sponsorshipPlan.create({ data: { @@ -57,23 +58,23 @@ export class PlanService { }, }, }, - }) + }); } public async remove(planId: string) { const plan = await this.prismaService.sponsorshipPlan.findUnique({ where: { id: planId }, - }) + }); if (!plan) { - throw new NotFoundException('План не найден') + throw new NotFoundException('План не найден'); } - await this.stripeService.plans.del(plan.stripePlanId) - await this.stripeService.products.del(plan.stripeProductId) + await this.stripeService.plans.del(plan.stripePlanId); + await this.stripeService.products.del(plan.stripeProductId); return this.prismaService.sponsorshipPlan.delete({ where: { id: plan.id }, - }) + }); } } diff --git a/backend/src/module/sponsorship/subscription/model/subscription.model.ts b/backend/src/module/sponsorship/subscription/model/subscription.model.ts index e8bdd5c..437485a 100644 --- a/backend/src/module/sponsorship/subscription/model/subscription.model.ts +++ b/backend/src/module/sponsorship/subscription/model/subscription.model.ts @@ -1,39 +1,39 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { SponsorshipSubscription } from '@/prisma/generated'; import { UserModel } from '@/src/module/auth/account/models/user.model'; -import { Field, ID, ObjectType } from '@nestjs/graphql' -import { SponsorshipSubscription } from '@/prisma/generated' - -import { PlanModel } from '../../plan/models/plan.model' +import { PlanModel } from '../../plan/models/plan.model'; @ObjectType() export class SubscriptionModel implements SponsorshipSubscription { @Field(() => ID) - public id: string + public id: string; @Field(() => Date) - public expiresAt: Date + public expiresAt: Date; @Field(() => PlanModel) - public plan: PlanModel + public plan: PlanModel; @Field(() => String) - public planId: string + public planId: string; @Field(() => UserModel) - public user: UserModel + public user: UserModel; @Field(() => String) - public userId: string + public userId: string; @Field(() => UserModel) - public channel: UserModel + public channel: UserModel; @Field(() => String) - public channelId: string + public channelId: string; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } diff --git a/backend/src/module/sponsorship/subscription/subscription.module.ts b/backend/src/module/sponsorship/subscription/subscription.module.ts index 3dcf40e..d0e466c 100644 --- a/backend/src/module/sponsorship/subscription/subscription.module.ts +++ b/backend/src/module/sponsorship/subscription/subscription.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; -import { SubscriptionService } from './subscription.service'; + import { SubscriptionResolver } from './subscription.resolver'; +import { SubscriptionService } from './subscription.service'; @Module({ providers: [SubscriptionResolver, SubscriptionService], diff --git a/backend/src/module/sponsorship/subscription/subscription.resolver.ts b/backend/src/module/sponsorship/subscription/subscription.resolver.ts index e6bffbf..aa02a03 100644 --- a/backend/src/module/sponsorship/subscription/subscription.resolver.ts +++ b/backend/src/module/sponsorship/subscription/subscription.resolver.ts @@ -1,9 +1,11 @@ +import { Query, Resolver } from '@nestjs/graphql'; + import { SubscriptionModel } from '@/src/module/sponsorship/subscription/model/subscription.model'; -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Query, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { SubscriptionService } from './subscription.service' -import { Authorization } from '@/src/shared/decorators/auth.decorator' +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { SubscriptionService } from './subscription.service'; @Resolver('Subscription') export class SubscriptionResolver { @@ -12,6 +14,6 @@ export class SubscriptionResolver { @Authorization() @Query(() => [SubscriptionModel]) public async findMySponsors(@Authorized() user: User) { - return this.subscriptionService.findMySponsors(user) + return this.subscriptionService.findMySponsors(user); } } diff --git a/backend/src/module/sponsorship/subscription/subscription.service.ts b/backend/src/module/sponsorship/subscription/subscription.service.ts index a0494f3..a760a87 100644 --- a/backend/src/module/sponsorship/subscription/subscription.service.ts +++ b/backend/src/module/sponsorship/subscription/subscription.service.ts @@ -1,6 +1,7 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { Injectable } from '@nestjs/common' -import { User } from '@prisma/generated' +import { Injectable } from '@nestjs/common'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { User } from '@prisma/generated'; @Injectable() export class SubscriptionService { @@ -16,8 +17,8 @@ export class SubscriptionService { include: { plan: true, user: true, - channel: true + channel: true, }, - }) + }); } } diff --git a/backend/src/module/sponsorship/transaction/model/make-payment.model.ts b/backend/src/module/sponsorship/transaction/model/make-payment.model.ts index 58ddc49..92ae745 100644 --- a/backend/src/module/sponsorship/transaction/model/make-payment.model.ts +++ b/backend/src/module/sponsorship/transaction/model/make-payment.model.ts @@ -1,7 +1,7 @@ -import { Field, ObjectType } from '@nestjs/graphql' +import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() export class MakePaymentModel { @Field(() => String) - url: string + url: string; } diff --git a/backend/src/module/sponsorship/transaction/model/transaction.model.ts b/backend/src/module/sponsorship/transaction/model/transaction.model.ts index 8dba6d9..3e53a02 100644 --- a/backend/src/module/sponsorship/transaction/model/transaction.model.ts +++ b/backend/src/module/sponsorship/transaction/model/transaction.model.ts @@ -1,37 +1,40 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql' -import { Transaction, TransactionStatus } from '@prisma/generated' +import { + Field, ID, ObjectType, registerEnumType, +} from '@nestjs/graphql'; + +import { UserModel } from '@/src/module/auth/account/models/user.model'; +import { Transaction, TransactionStatus } from '@prisma/generated'; registerEnumType(TransactionStatus, { name: 'TransactionStatus', -}) +}); @ObjectType() export class TransactionModel implements Transaction { @Field(() => ID) - public id: string + public id: string; @Field(() => ID) - stripeSubscriptionId: string + stripeSubscriptionId: string; @Field(() => TransactionStatus) - status: TransactionStatus + status: TransactionStatus; @Field(() => String) - currency: string + currency: string; @Field(() => Number) - amount: number + amount: number; @Field(() => UserModel) - user: UserModel + user: UserModel; @Field(() => ID) - userId: string + userId: string; @Field(() => Date) - public createdAt: Date + public createdAt: Date; @Field(() => Date) - public updatedAt: Date + public updatedAt: Date; } diff --git a/backend/src/module/sponsorship/transaction/transaction.module.ts b/backend/src/module/sponsorship/transaction/transaction.module.ts index 32ac4a7..f4d59eb 100644 --- a/backend/src/module/sponsorship/transaction/transaction.module.ts +++ b/backend/src/module/sponsorship/transaction/transaction.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; -import { TransactionService } from './transaction.service'; + import { TransactionResolver } from './transaction.resolver'; +import { TransactionService } from './transaction.service'; @Module({ providers: [TransactionResolver, TransactionService], diff --git a/backend/src/module/sponsorship/transaction/transaction.resolver.ts b/backend/src/module/sponsorship/transaction/transaction.resolver.ts index b0ea20e..3c54833 100644 --- a/backend/src/module/sponsorship/transaction/transaction.resolver.ts +++ b/backend/src/module/sponsorship/transaction/transaction.resolver.ts @@ -1,10 +1,14 @@ -import { MakePaymentModel } from '@/src/module/sponsorship/transaction/model/make-payment.model' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { TransactionService } from './transaction.service' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { TransactionModel } from './model/transaction.model' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; + +import { MakePaymentModel } from '@/src/module/sponsorship/transaction/model/make-payment.model'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { TransactionModel } from './model/transaction.model'; +import { TransactionService } from './transaction.service'; @Resolver('Transaction') export class TransactionResolver { @@ -13,7 +17,7 @@ export class TransactionResolver { @Authorization() @Query(() => [TransactionModel], { name: 'findMyTransactions' }) public async findMyTransactions(@Authorized() user: User) { - return this.transactionService.findMyTransactions(user) + return this.transactionService.findMyTransactions(user); } @Authorization() @@ -22,6 +26,6 @@ export class TransactionResolver { @Authorized() user: User, @Args('planId') planId: string, ) { - return this.transactionService.makePayment(user, planId) + return this.transactionService.makePayment(user, planId); } } diff --git a/backend/src/module/sponsorship/transaction/transaction.service.ts b/backend/src/module/sponsorship/transaction/transaction.service.ts index 3feefed..6d74a1e 100644 --- a/backend/src/module/sponsorship/transaction/transaction.service.ts +++ b/backend/src/module/sponsorship/transaction/transaction.service.ts @@ -1,9 +1,10 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { StripeService } from '@/src/module/libs/stripe/stripe.service' -import { ProcessEnv } from '@/src/shared/types/env' -import { ConflictException, Injectable, NotFoundException } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { User } from '@prisma/generated' +import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { StripeService } from '@/src/module/libs/stripe/stripe.service'; +import { ProcessEnv } from '@/src/shared/types/env'; +import { User } from '@prisma/generated'; @Injectable() export class TransactionService { @@ -17,21 +18,21 @@ export class TransactionService { public async findMyTransactions(user: User) { return this.prismaService.transaction.findMany({ where: { userId: user.id }, - }) + }); } public async makePayment(user: User, planId: string) { const plan = await this.prismaService.sponsorshipPlan.findUnique({ where: { id: planId }, include: { channel: true }, - }) + }); if (!plan) { - throw new NotFoundException('Не найден тарифный план') + throw new NotFoundException('Не найден тарифный план'); } if (user.id === plan.channel?.id) { - throw new ConflictException('Нельзя оформить подписку на себя') + throw new ConflictException('Нельзя оформить подписку на себя'); } const existingSubscription = await this.prismaService.sponsorshipSubscription.findFirst({ @@ -39,16 +40,16 @@ export class TransactionService { userId: user.id, channelId: plan.channel?.id, }, - }) + }); if (existingSubscription) { - throw new ConflictException('Вы уже оформили спонсорство на этот канал') + throw new ConflictException('Вы уже оформили спонсорство на этот канал'); } const customer = await this.stripeService.customers.create({ name: user.name, email: user.email, - }) + }); const session = await this.stripeService.checkout.sessions.create({ // @ts-expect-error TODO @@ -78,7 +79,7 @@ export class TransactionService { userId: user.id, channelId: plan.channel?.id, }, - }) + }); await this.prismaService.transaction.create({ data: { @@ -89,8 +90,8 @@ export class TransactionService { connect: { id: user.id }, }, }, - }) + }); - return { url: session.url } + return { url: session.url }; } } diff --git a/backend/src/module/stream/index.ts b/backend/src/module/stream/index.ts new file mode 100644 index 0000000..3f4b30b --- /dev/null +++ b/backend/src/module/stream/index.ts @@ -0,0 +1,2 @@ +export { GenerateTokenModel } from './models/generate-token.model'; +export { StreamModel } from './models/stream.model'; diff --git a/backend/src/module/stream/ingress/ingress.module.ts b/backend/src/module/stream/ingress/ingress.module.ts index f40c36d..5ef2d85 100644 --- a/backend/src/module/stream/ingress/ingress.module.ts +++ b/backend/src/module/stream/ingress/ingress.module.ts @@ -1,6 +1,7 @@ -import { Module } from '@nestjs/common' -import { IngressService } from './ingress.service' -import { IngressResolver } from './ingress.resolver' +import { Module } from '@nestjs/common'; + +import { IngressResolver } from './ingress.resolver'; +import { IngressService } from './ingress.service'; @Module({ providers: [IngressResolver, IngressService], diff --git a/backend/src/module/stream/ingress/ingress.resolver.ts b/backend/src/module/stream/ingress/ingress.resolver.ts index 8a2e8bd..9d651ba 100644 --- a/backend/src/module/stream/ingress/ingress.resolver.ts +++ b/backend/src/module/stream/ingress/ingress.resolver.ts @@ -1,9 +1,11 @@ -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { Args, Mutation, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { IngressInput } from 'livekit-server-sdk' -import { IngressService } from './ingress.service' +import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { IngressInput } from 'livekit-server-sdk'; + +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { User } from '@prisma/generated'; + +import { IngressService } from './ingress.service'; @Resolver('Ingress') export class IngressResolver { @@ -15,6 +17,6 @@ export class IngressResolver { @Authorized() user: User, @Args('ingressType') ingressType: IngressInput, ) { - return this.ingressService.create(user, ingressType) + return this.ingressService.create(user, ingressType); } } diff --git a/backend/src/module/stream/ingress/ingress.service.ts b/backend/src/module/stream/ingress/ingress.service.ts index fd0c39a..1792838 100644 --- a/backend/src/module/stream/ingress/ingress.service.ts +++ b/backend/src/module/stream/ingress/ingress.service.ts @@ -1,13 +1,14 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { LiveKitService } from '@/src/module/libs/livekit/livekit.service' -import { BadRequestException, Injectable } from '@nestjs/common' -import { User } from '@prisma/generated' +import { BadRequestException, Injectable } from '@nestjs/common'; import { CreateIngressOptions, IngressAudioEncodingPreset, IngressInput, IngressVideoEncodingPreset, -} from 'livekit-server-sdk' +} from 'livekit-server-sdk'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { LiveKitService } from '@/src/module/libs/livekit/livekit.service'; +import { User } from '@prisma/generated'; @Injectable() export class IngressService { @@ -18,33 +19,32 @@ export class IngressService { } public async create(user: User, ingressType: IngressInput) { - await this.resetIngresses(user) + await this.resetIngresses(user); const options: CreateIngressOptions = { name: user.name, roomName: user.id, participantName: user.name, participantIdentity: user.id, - } + }; if (ingressType === IngressInput.WHIP_INPUT) { - options.bypassTranscoding = true - } - else { + options.bypassTranscoding = true; + } else { options.video = { source: 1, preset: IngressVideoEncodingPreset.H264_1080P_30FPS_3_LAYERS, - } + }; options.audio = { source: 2, preset: IngressAudioEncodingPreset.OPUS_STEREO_96KBPS, - } + }; } - const ingress = await this.liveKitService.ingress.createIngress(ingressType, options) + const ingress = await this.liveKitService.ingress.createIngress(ingressType, options); - if (!ingress || !ingress.url || !ingress.streamKey) { - throw new BadRequestException('Не удалось создать входной поток') + if (!ingress?.url || !ingress.streamKey) { + throw new BadRequestException('Не удалось создать входной поток'); } await this.prismaService.stream.update({ @@ -54,25 +54,25 @@ export class IngressService { serverUrl: ingress.url, key: ingress.streamKey, }, - }) + }); - return true + return true; } private async resetIngresses(user: User) { const ingresses = await this.liveKitService.ingress.listIngress({ roomName: user.id, - }) + }); - const rooms = await this.liveKitService.room.listRooms([user.id]) + const rooms = await this.liveKitService.room.listRooms([user.id]); for (const room of rooms) { - await this.liveKitService.room.deleteRoom(room.name) + await this.liveKitService.room.deleteRoom(room.name); } for (const ingress of ingresses) { if (ingress.ingressId) { - await this.liveKitService.ingress.deleteIngress(ingress.ingressId) + await this.liveKitService.ingress.deleteIngress(ingress.ingressId); } } } diff --git a/backend/src/module/stream/inputs/change-stream-info.input.ts b/backend/src/module/stream/inputs/change-stream-info.input.ts index df5ad56..0c26b53 100644 --- a/backend/src/module/stream/inputs/change-stream-info.input.ts +++ b/backend/src/module/stream/inputs/change-stream-info.input.ts @@ -1,15 +1,15 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsString } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString } from 'class-validator'; @InputType() export class ChangeStreamInfoInput { @Field(() => String) @IsString() @IsNotEmpty() - title: string + title: string; @Field(() => String) @IsString() @IsNotEmpty() - categoryId: string + categoryId: string; } diff --git a/backend/src/module/stream/inputs/filter.input.ts b/backend/src/module/stream/inputs/filter.input.ts index a1addf6..cd99937 100644 --- a/backend/src/module/stream/inputs/filter.input.ts +++ b/backend/src/module/stream/inputs/filter.input.ts @@ -1,20 +1,20 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNumber, IsOptional, IsString } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; @InputType() export class FilterInput { @Field(() => Number, { nullable: true }) @IsNumber() @IsOptional() - public take?: number + public take?: number; @Field(() => Number, { nullable: true }) @IsNumber() @IsOptional() - public skip?: number + public skip?: number; @Field(() => String, { nullable: true }) @IsString() @IsOptional() - public searchTerm?: string + public searchTerm?: string; } diff --git a/backend/src/module/stream/inputs/generate-stream-token.input.ts b/backend/src/module/stream/inputs/generate-stream-token.input.ts index 6c05df9..94703ac 100644 --- a/backend/src/module/stream/inputs/generate-stream-token.input.ts +++ b/backend/src/module/stream/inputs/generate-stream-token.input.ts @@ -1,15 +1,15 @@ -import { Field, InputType } from '@nestjs/graphql' -import { IsNotEmpty, IsString } from 'class-validator' +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString } from 'class-validator'; @InputType() export class GenerateStreamTokenInput { @Field(() => String) @IsString() @IsNotEmpty() - public userId: string + public userId: string; @Field(() => String) @IsString() @IsNotEmpty() - public channelId: string + public channelId: string; } diff --git a/backend/src/module/stream/models/generate-token.model.ts b/backend/src/module/stream/models/generate-token.model.ts index adc829b..58aafe9 100644 --- a/backend/src/module/stream/models/generate-token.model.ts +++ b/backend/src/module/stream/models/generate-token.model.ts @@ -1,7 +1,7 @@ -import { Field, ObjectType } from '@nestjs/graphql' +import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() export class GenerateTokenModel { @Field(() => String) - public token: string + public token: string; } diff --git a/backend/src/module/stream/models/stream.model.ts b/backend/src/module/stream/models/stream.model.ts index ce66147..b562a9d 100644 --- a/backend/src/module/stream/models/stream.model.ts +++ b/backend/src/module/stream/models/stream.model.ts @@ -1,59 +1,60 @@ -import { UserModel } from '@/src/module/auth/account/models/user.model' -import { CategoryModel } from '@/src/module/category/models/category.model' -import { ChatMessageModel } from '@/src/module/chat/models/chat-message.model'; -import { Field, ID, ObjectType } from '@nestjs/graphql' -import { Stream } from '@prisma/generated' +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { UserModel } from '@/src/module/auth'; +import { CategoryModel } from '@/src/module/category'; +import { ChatMessageModel } from '@/src/module/chat'; +import { Stream } from '@prisma/generated'; @ObjectType() export class StreamModel implements Stream { @Field(() => ID) - id: string + id: string; @Field(() => ID) - userId: UserModel['id'] + userId: UserModel['id']; @Field(() => UserModel) - user: UserModel + user: UserModel; @Field(() => ID) - categoryId: CategoryModel['id'] + categoryId: CategoryModel['id']; @Field(() => CategoryModel) - category: CategoryModel + category: CategoryModel; @Field(() => String) - title: string + title: string; @Field(() => String, { nullable: true }) - key: string + key: string; @Field(() => String, { nullable: true }) - thumbnailUrl: string + thumbnailUrl: string; @Field(() => String, { nullable: true }) - serverUrl: string + serverUrl: string; @Field(() => Boolean) - isLive: boolean + isLive: boolean; @Field(() => String, { nullable: true }) - ingressId: string + ingressId: string; @Field(() => Boolean) - isChatEnable: boolean + isChatEnable: boolean; @Field(() => Boolean) - isChatPremiumFollowersOnly: boolean + isChatPremiumFollowersOnly: boolean; @Field(() => Boolean) - isChatFollowersOnly: boolean + isChatFollowersOnly: boolean; @Field(() => [ChatMessageModel]) - chatMessages: ChatMessageModel[] + chatMessages: ChatMessageModel[]; @Field(() => Date) - updatedAt: Date + updatedAt: Date; @Field(() => Date) - createdAt: Date + createdAt: Date; } diff --git a/backend/src/module/stream/stream.module.ts b/backend/src/module/stream/stream.module.ts index 978942f..d66c5e2 100644 --- a/backend/src/module/stream/stream.module.ts +++ b/backend/src/module/stream/stream.module.ts @@ -1,7 +1,8 @@ -import { Module } from '@nestjs/common' -import { StreamService } from './stream.service' -import { StreamResolver } from './stream.resolver' -import { IngressModule } from './ingress/ingress.module' +import { Module } from '@nestjs/common'; + +import { IngressModule } from './ingress/ingress.module'; +import { StreamResolver } from './stream.resolver'; +import { StreamService } from './stream.service'; @Module({ providers: [StreamResolver, StreamService], diff --git a/backend/src/module/stream/stream.resolver.ts b/backend/src/module/stream/stream.resolver.ts index 30cd00e..e079a58 100644 --- a/backend/src/module/stream/stream.resolver.ts +++ b/backend/src/module/stream/stream.resolver.ts @@ -1,16 +1,20 @@ -import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input' -import { FilterInput } from '@/src/module/stream/inputs/filter.input' -import { GenerateStreamTokenInput } from '@/src/module/stream/inputs/generate-stream-token.input' -import { GenerateTokenModel } from '@/src/module/stream/models/generate-token.model' -import { StreamModel } from '@/src/module/stream/models/stream.model' -import { Authorization } from '@/src/shared/decorators/auth.decorator' -import { Authorized } from '@/src/shared/decorators/authorized.decorator' -import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe' -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import * as Upload from 'graphql-upload/Upload.js' -import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js' -import { StreamService } from './stream.service' +import { + Args, Mutation, Query, Resolver, +} from '@nestjs/graphql'; +import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js'; +import * as Upload from 'graphql-upload/Upload.js'; + +import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input'; +import { FilterInput } from '@/src/module/stream/inputs/filter.input'; +import { GenerateStreamTokenInput } from '@/src/module/stream/inputs/generate-stream-token.input'; +import { GenerateTokenModel } from '@/src/module/stream/models/generate-token.model'; +import { StreamModel } from '@/src/module/stream/models/stream.model'; +import { Authorization } from '@/src/shared/decorators/auth.decorator'; +import { Authorized } from '@/src/shared/decorators/authorized.decorator'; +import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe'; +import { User } from '@prisma/generated'; + +import { StreamService } from './stream.service'; @Resolver('Stream') export class StreamResolver { @@ -20,12 +24,12 @@ export class StreamResolver { public async findAll( @Args('filters') input: FilterInput, ) { - return this.streamService.findAll(input) + return this.streamService.findAll(input); } @Query(() => [StreamModel], { name: 'findRandomStreams' }) public async findRandomStreams() { - return this.streamService.findRandom() + return this.streamService.findRandom(); } @Authorization() @@ -34,7 +38,7 @@ export class StreamResolver { @Authorized() user: User, @Args('data') input: ChangeStreamInfoInput, ) { - return this.streamService.changeInfo(user, input) + return this.streamService.changeInfo(user, input); } @Authorization() @@ -43,17 +47,17 @@ export class StreamResolver { @Authorized() user: User, @Args('thumbnail', { type: () => GraphQLUpload }, FileValidationPipe) file: Upload, ) { - return this.streamService.changeThumbnail(user, file) + return this.streamService.changeThumbnail(user, file); } @Authorization() @Mutation(() => Boolean, { name: 'removeStreamThumbnail' }) public async removeAvatar(@Authorized() user: User) { - return this.streamService.removeThumbnail(user) + return this.streamService.removeThumbnail(user); } @Mutation(() => GenerateTokenModel, { name: 'generateStreamToken' }) public async generateStreamToken(@Args('data') input: GenerateStreamTokenInput) { - return this.streamService.generateToken(input) + return this.streamService.generateToken(input); } } diff --git a/backend/src/module/stream/stream.service.ts b/backend/src/module/stream/stream.service.ts index 36a6f1e..e51f295 100644 --- a/backend/src/module/stream/stream.service.ts +++ b/backend/src/module/stream/stream.service.ts @@ -1,14 +1,16 @@ -import { Prisma, User } from '@/prisma/generated' -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { StorageService } from '@/src/module/libs/storage/storage.service' -import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input' -import { FilterInput } from '@/src/module/stream/inputs/filter.input' -import { GenerateStreamTokenInput } from '@/src/module/stream/inputs/generate-stream-token.input' -import { Injectable, NotFoundException } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import * as Upload from 'graphql-upload/Upload' -import { AccessToken } from 'livekit-server-sdk' -import sharp from 'sharp' +import { Injectable, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as Upload from 'graphql-upload/Upload'; +import { AccessToken } from 'livekit-server-sdk'; +import sharp from 'sharp'; + +import { Prisma, User } from '@/prisma/generated'; +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { StorageService } from '@/src/module/libs/storage/storage.service'; +import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input'; +import { FilterInput } from '@/src/module/stream/inputs/filter.input'; +import { GenerateStreamTokenInput } from '@/src/module/stream/inputs/generate-stream-token.input'; + import { ProcessEnv } from '../../shared/types/env'; @Injectable() @@ -21,10 +23,10 @@ export class StreamService { } public async findAll(input: FilterInput = {}) { - const { take, skip, searchTerm } = input + const { take, skip, searchTerm } = input; const whereClause = searchTerm ? this.findBySearchTermFilter(searchTerm) - : undefined + : undefined; return this.prismaService.stream.findMany({ take: take ?? 12, @@ -37,7 +39,7 @@ export class StreamService { orderBy: { createdAt: 'desc', }, - }) + }); } public async findRandom() { @@ -47,12 +49,12 @@ export class StreamService { isDeactivated: false, }, }, - }) + }); - const randomIndexes = new Set() + const randomIndexes = new Set(); while (randomIndexes.size < 2) { - const randomIndex = Math.floor(Math.random() * total) - randomIndexes.add(randomIndex) + const randomIndex = Math.floor(Math.random() * total); + randomIndexes.add(randomIndex); } const streams = await this.prismaService.stream.findMany({ @@ -64,9 +66,9 @@ export class StreamService { take: total, skip: 0, include: { user: true, category: true }, - }) + }); - return Array.from(randomIndexes).map(index => streams[index]) + return Array.from(randomIndexes).map((index) => streams[index]); } public findBySearchTermFilter(searchTerm: NonNullable): Prisma.StreamWhereInput { @@ -87,11 +89,12 @@ export class StreamService { }, }, ], - } + }; } public async changeInfo(user: User, input: ChangeStreamInfoInput) { - const { categoryId, title } = input + const { categoryId, title } = input; + return this.prismaService.stream.update({ where: { userId: user.id }, data: { @@ -102,51 +105,51 @@ export class StreamService { }, }, }, - }) + }); } public async changeThumbnail(user: User, file: Upload) { - const stream = await this.findStreamByUser(user) + const stream = await this.findStreamByUser(user); if (stream.thumbnailUrl) { - await this.storageService.remove(stream.thumbnailUrl) + await this.storageService.remove(stream.thumbnailUrl); } - const chunks: Buffer[] = [] + const chunks: Buffer[] = []; for await (const chunk of file.createReadStream()) { - chunks.push(chunk) + chunks.push(chunk); } - const buffer = Buffer.concat(chunks) - const fileName = `/streams/${user.name}.webp` + const buffer = Buffer.concat(chunks); + const fileName = `/streams/${user.name}.webp`; const processedBuffer = await sharp(buffer, { animated: file.filename.endsWith('.gif') }) .resize(1280, 720) .webp() - .toBuffer() + .toBuffer(); - await this.storageService.upload(processedBuffer, fileName, 'image/webp') + await this.storageService.upload(processedBuffer, fileName, 'image/webp'); return this.prismaService.stream.update({ where: { id: stream.id }, data: { thumbnailUrl: fileName }, - }) + }); } public async removeThumbnail(user: User) { - const stream = await this.findStreamByUser(user) + const stream = await this.findStreamByUser(user); if (!stream.thumbnailUrl) { - return true + return true; } - await this.storageService.remove(stream.thumbnailUrl) + await this.storageService.remove(stream.thumbnailUrl); return this.prismaService.stream.update({ where: { id: stream.id }, data: { thumbnailUrl: null }, - }) + }); } private async findStreamByUser(user: User) { @@ -154,46 +157,45 @@ export class StreamService { where: { userId: user.id, }, - }) + }); if (!stream) { - throw new NotFoundException('Не найден такой стрим') + throw new NotFoundException('Не найден такой стрим'); } - return stream + return stream; } public async generateToken(input: GenerateStreamTokenInput) { - const { channelId, userId } = input + const { channelId, userId } = input; - let self: { id: string, username: string } + let self: { id: string; username: string }; const user = await this.prismaService.user.findUnique({ where: { id: userId }, - }) + }); if (user) { self = { id: user.id, username: user.name, - } - } - else { + }; + } else { self = { id: userId, username: `Зритель ${Math.floor(Math.random() * 100000)}`, - } + }; } const channel = await this.prismaService.user.findUnique({ where: { id: channelId }, - }) + }); if (!channel) { - throw new NotFoundException('Канал не найден') + throw new NotFoundException('Канал не найден'); } - const isHost = self.id === channel.id + const isHost = self.id === channel.id; const token = new AccessToken( this.configService.getOrThrow('LIVEKIT_API_KEY'), @@ -202,16 +204,16 @@ export class StreamService { identity: isHost ? `Host -${self.id}` : self.id.toString(), name: self.username, }, - ) + ); token.addGrant({ room: channel.id, roomJoin: true, canPublish: false, - }) + }); return { token: token.toJwt(), - } + }; } } diff --git a/backend/src/module/webhook/webhook.controller.ts b/backend/src/module/webhook/webhook.controller.ts index 7f07a44..7bc0014 100644 --- a/backend/src/module/webhook/webhook.controller.ts +++ b/backend/src/module/webhook/webhook.controller.ts @@ -1,5 +1,8 @@ -import { Controller, HttpCode, HttpStatus, Post, UnauthorizedException, Headers, Body, RawBody } from '@nestjs/common' -import { WebhookService } from './webhook.service' +import { + Controller, HttpCode, HttpStatus, Post, UnauthorizedException, Headers, Body, RawBody, +} from '@nestjs/common'; + +import { WebhookService } from './webhook.service'; @Controller('webhook') export class WebhookController { @@ -7,15 +10,15 @@ export class WebhookController { @Post('livekit') @HttpCode(HttpStatus.OK) - public receiveWebhookLiveKit( + public async receiveWebhookLiveKit( @Body() body: string, @Headers('Authorization') authorization: string, ) { if (!authorization) { - throw new UnauthorizedException('Отстутсвует авторизация') + throw new UnauthorizedException('Отстутсвует авторизация'); } - return this.webhookService.receiveWebhookLiveKit(body, authorization) + return this.webhookService.receiveWebhookLiveKit(body, authorization); } @Post('stripe') @@ -25,10 +28,10 @@ export class WebhookController { @Headers('stripe-signature') sig: string, ) { if (!sig) { - throw new UnauthorizedException('Отсутствует подпись Stripe в заголовке') + throw new UnauthorizedException('Отсутствует подпись Stripe в заголовке'); } - const event = await this.webhookService.constructStripeEvent(rawBody, sig) - await this.webhookService.receiveWebhookStripe(event) + const event = this.webhookService.constructStripeEvent(rawBody, sig); + await this.webhookService.receiveWebhookStripe(event); } } diff --git a/backend/src/module/webhook/webhook.module.ts b/backend/src/module/webhook/webhook.module.ts index 672e7d0..e12a5d2 100644 --- a/backend/src/module/webhook/webhook.module.ts +++ b/backend/src/module/webhook/webhook.module.ts @@ -1,8 +1,10 @@ +import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; + import { NotificationService } from '@/src/module/notification/notification.service'; -import { RawBodyMiddleware } from '@/src/shared/middlewares/raw-body.middleware' -import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common' -import { WebhookController } from './webhook.controller' -import { WebhookService } from './webhook.service' +import { RawBodyMiddleware } from '@/src/shared/middlewares/raw-body.middleware'; + +import { WebhookController } from './webhook.controller'; +import { WebhookService } from './webhook.service'; @Module({ controllers: [WebhookController], @@ -12,6 +14,6 @@ export class WebhookModule { public configure(consumer: MiddlewareConsumer) { consumer .apply(RawBodyMiddleware) - .forRoutes({ path: 'webhook/livekit', method: RequestMethod.POST }) + .forRoutes({ path: 'webhook/livekit', method: RequestMethod.POST }); } } diff --git a/backend/src/module/webhook/webhook.service.ts b/backend/src/module/webhook/webhook.service.ts index 545d098..403b64a 100644 --- a/backend/src/module/webhook/webhook.service.ts +++ b/backend/src/module/webhook/webhook.service.ts @@ -1,13 +1,14 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { LiveKitService } from '@/src/module/libs/livekit/livekit.service' -import { StripeService } from '@/src/module/libs/stripe/stripe.service' -import { TelegramService } from '@/src/module/libs/telegram/telegram.service' -import { NotificationService } from '@/src/module/notification/notification.service' +import { BadRequestException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Stripe from 'stripe'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { LiveKitService } from '@/src/module/libs/livekit/livekit.service'; +import { StripeService } from '@/src/module/libs/stripe/stripe.service'; +import { TelegramService } from '@/src/module/libs/telegram/telegram.service'; +import { NotificationService } from '@/src/module/notification/notification.service'; import { ProcessEnv } from '@/src/shared/types/env'; -import { BadRequestException, Injectable } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { TransactionStatus } from '@prisma/generated' -import Stripe from 'stripe' +import { TransactionStatus } from '@prisma/generated'; @Injectable() export class WebhookService { @@ -22,14 +23,14 @@ export class WebhookService { } public async receiveWebhookLiveKit(body: string, authorization: string) { - const event = this.liveKitService.webhook.receive(body, authorization, true) + const event = this.liveKitService.webhook.receive(body, authorization, true); if (event.event === 'ingress_started' && event.ingressInfo?.ingressId) { const stream = await this.prismaService.stream.update({ where: { ingressId: event.ingressInfo.ingressId }, data: { isLive: true }, include: { user: true }, - }) + }); const followers = await this.prismaService.follow.findMany({ where: { @@ -43,15 +44,15 @@ export class WebhookService { }, }, }, - }) + }); for (const follow of followers) { - const follower = follow.follower + const { follower } = follow; if (follower.notificationSettings?.siteNotifications) { - await this.notificationService.createStreamStart(follower.id, stream.user!) + await this.notificationService.createStreamStart(follower.id, stream.user!); } if (follower.notificationSettings?.telegramNotifications && follower.telegramId) { - await this.telegramService.sendStreamStart(follower.telegramId, stream.user!) + await this.telegramService.sendStreamStart(follower.telegramId, stream.user!); } } } @@ -60,28 +61,28 @@ export class WebhookService { const stream = await this.prismaService.stream.update({ where: { ingressId: event.ingressInfo.ingressId }, data: { isLive: false }, - }) + }); await this.prismaService.chatMessage.deleteMany({ where: { streamId: stream.id }, - }) + }); } } public async receiveWebhookStripe(event: Stripe.Event) { - const session = event.data.object as Stripe.Checkout.Session + const session = event.data.object as Stripe.Checkout.Session; if (!session) { - throw new BadRequestException('Не найдена сессия') + throw new BadRequestException('Не найдена сессия'); } if (event.type === 'checkout.session.completed') { - const planId = session.metadata!.planId - const userId = session.metadata!.userId - const channelId = session.metadata!.channelId + const { planId } = (session.metadata!); + const { userId } = (session.metadata!); + const { channelId } = (session.metadata!); - const expiresAt = new Date() - expiresAt.setDate(expiresAt.getDate() + 30) + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 30); const sponsorshipSubscription = await this.prismaService.sponsorshipSubscription.create({ data: { @@ -99,7 +100,7 @@ export class WebhookService { }, }, }, - }) + }); await this.prismaService.transaction.updateMany({ where: { @@ -109,14 +110,14 @@ export class WebhookService { data: { status: TransactionStatus.SUCCESS, }, - }) + }); if (sponsorshipSubscription.channel?.notificationSettings?.siteNotifications) { await this.notificationService.createNewSponsorship( sponsorshipSubscription.channel.id, sponsorshipSubscription.plan!, sponsorshipSubscription.user!, - ) + ); } if (sponsorshipSubscription.channel?.notificationSettings?.telegramNotifications && sponsorshipSubscription.channel.telegramId) { @@ -124,7 +125,7 @@ export class WebhookService { sponsorshipSubscription.channel.telegramId, sponsorshipSubscription.plan!, sponsorshipSubscription.user!, - ) + ); } } @@ -132,14 +133,14 @@ export class WebhookService { await this.prismaService.transaction.updateMany({ where: { stripeSubscriptionId: session.id }, data: { status: TransactionStatus.EXPIRED }, - }) + }); } if (event.type === 'checkout.session.async_payment_failed') { await this.prismaService.transaction.updateMany({ where: { stripeSubscriptionId: session.id }, data: { status: TransactionStatus.FAILED }, - }) + }); } } @@ -148,6 +149,6 @@ export class WebhookService { payload, signature, this.configService.getOrThrow('STRIPE_WEBHOOK_SECRET'), - ) + ); } } diff --git a/backend/src/shared/decorators/auth.decorator.ts b/backend/src/shared/decorators/auth.decorator.ts index d6b7d99..85f0e6f 100644 --- a/backend/src/shared/decorators/auth.decorator.ts +++ b/backend/src/shared/decorators/auth.decorator.ts @@ -1,6 +1,7 @@ -import { GqlAuthGuard } from '@/src/shared/guards/gql-auth.guard' -import { applyDecorators, UseGuards } from '@nestjs/common' +import { applyDecorators, UseGuards } from '@nestjs/common'; + +import { GqlAuthGuard } from '@/src/shared/guards/gql-auth.guard'; export function Authorization() { - return applyDecorators(UseGuards(GqlAuthGuard)) + return applyDecorators(UseGuards(GqlAuthGuard)); } diff --git a/backend/src/shared/decorators/authorized.decorator.ts b/backend/src/shared/decorators/authorized.decorator.ts index 9ea244e..652ec7b 100644 --- a/backend/src/shared/decorators/authorized.decorator.ts +++ b/backend/src/shared/decorators/authorized.decorator.ts @@ -1,20 +1,21 @@ -import { GqlContext } from '@/src/shared/types/gql-context.types' -import { createParamDecorator, NotFoundException } from '@nestjs/common' -import { GqlExecutionContext } from '@nestjs/graphql' -import { User } from '@prisma/generated' -import { Request } from 'express' +import { createParamDecorator, NotFoundException } from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import type { GqlContext } from '@/src/shared/types/gql-context.types'; +import type { User } from '@prisma/generated'; +import type { Request } from 'express'; export const Authorized = createParamDecorator( - (data: keyof User, ctx) => { + (data: keyof User | undefined, ctx) => { const req: Request = ctx.getType() === 'http' ? ctx.switchToHttp().getRequest() - : GqlExecutionContext.create(ctx).getContext().req + : GqlExecutionContext.create(ctx).getContext().req; - const user = req.user + const { user } = req; if (!user) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } - return data ? user[data] : user + return data ? user[data] : user; }, -) +); diff --git a/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts b/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts index 1bf78f4..b8ad367 100644 --- a/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts +++ b/backend/src/shared/decorators/is-password-matching-constraint.decorator.ts @@ -1,14 +1,16 @@ -import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input' -import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator' +import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; + +import { NewPasswordInput } from '@/src/module/auth'; @ValidatorConstraint({ name: 'IsPasswordMatching', async: false }) export class IsPasswordMatchingConstraintDecorator implements ValidatorConstraintInterface { public validate(passwordRepeat: string, args: ValidationArguments): boolean { - const values = args.object as NewPasswordInput - return values.password === passwordRepeat + const values = args.object as NewPasswordInput; + + return values.password === passwordRepeat; } public defaultMessage(): string { - return 'Пароли не совпадают' + return 'Пароли не совпадают'; } } diff --git a/backend/src/shared/decorators/user-agent.decorator.ts b/backend/src/shared/decorators/user-agent.decorator.ts index 3f89fc9..ef40729 100644 --- a/backend/src/shared/decorators/user-agent.decorator.ts +++ b/backend/src/shared/decorators/user-agent.decorator.ts @@ -1,14 +1,15 @@ -import { GqlContext } from '@/src/shared/types/gql-context.types' -import { createParamDecorator } from '@nestjs/common' -import { GqlExecutionContext } from '@nestjs/graphql' -import { Request } from 'express' +import { createParamDecorator } from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import type { GqlContext } from '@/src/shared/types/gql-context.types'; +import type { Request } from 'express'; export const UserAgent = createParamDecorator( (data: unknown, ctx) => { const req: Request = ctx.getType() === 'http' ? ctx.switchToHttp().getRequest() - : GqlExecutionContext.create(ctx).getContext().req + : GqlExecutionContext.create(ctx).getContext().req; - return req.headers['user-agent'] + return req.headers['user-agent']; }, -) +); diff --git a/backend/src/shared/guards/gql-auth.guard.ts b/backend/src/shared/guards/gql-auth.guard.ts index 9060a1d..6e62010 100644 --- a/backend/src/shared/guards/gql-auth.guard.ts +++ b/backend/src/shared/guards/gql-auth.guard.ts @@ -1,7 +1,10 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { GqlContext } from '@/src/shared/types/gql-context.types' -import { CanActivate, ExecutionContext, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common' -import { GqlExecutionContext } from '@nestjs/graphql' +import { + CanActivate, ExecutionContext, Injectable, NotFoundException, UnauthorizedException, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import { PrismaService } from '@/src/core/prisma/prisma.service'; +import { GqlContext } from '@/src/shared/types/gql-context.types'; @Injectable() export class GqlAuthGuard implements CanActivate { @@ -11,23 +14,24 @@ export class GqlAuthGuard implements CanActivate { } async canActivate(context: ExecutionContext): Promise { - const ctx = GqlExecutionContext.create(context) - const { req } = ctx.getContext() + const ctx = GqlExecutionContext.create(context); + const { req } = ctx.getContext(); if (typeof req.session.userId === 'undefined') { - throw new UnauthorizedException('Пользователь не авторизован') + throw new UnauthorizedException('Пользователь не авторизован'); } const user = await this.prismaService.user.findUnique({ where: { id: req.session.userId, }, - }) + }); if (!user) { - throw new NotFoundException('Пользователь не найден') + throw new NotFoundException('Пользователь не найден'); } - req.user = user - return true + req.user = user; + + return true; } } diff --git a/backend/src/shared/middlewares/raw-body.middleware.ts b/backend/src/shared/middlewares/raw-body.middleware.ts index 041c9c0..f4d5cdb 100644 --- a/backend/src/shared/middlewares/raw-body.middleware.ts +++ b/backend/src/shared/middlewares/raw-body.middleware.ts @@ -1,21 +1,27 @@ -import { BadRequestException, Injectable, NestMiddleware } from '@nestjs/common' -import { NextFunction, Request, Response } from 'express' -import * as getRawBody from 'raw-body' +import { BadRequestException, Injectable, NestMiddleware } from '@nestjs/common'; +import { NextFunction } from 'express'; +import * as getRawBody from 'raw-body'; + +import type { Request, Response } from 'express'; @Injectable() export class RawBodyMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { if (!req.readable) { - return next(new BadRequestException('Неправильный запрос')) + next(new BadRequestException('Неправильный запрос')); + + return; } getRawBody(req, { encoding: 'utf-8' }) .then((rawBody) => { - req.body = rawBody - next() - }) - .catch((error: string) => { - throw new BadRequestException('Ошибка при получении', error) + req.body = rawBody; + next(); }) + .catch((error: unknown) => { + if (typeof error === 'string') { + throw new BadRequestException('Ошибка при получении', error); + } + }); } } diff --git a/backend/src/shared/pipes/file-validation.pipe.ts b/backend/src/shared/pipes/file-validation.pipe.ts index 414f06c..b256aa7 100644 --- a/backend/src/shared/pipes/file-validation.pipe.ts +++ b/backend/src/shared/pipes/file-validation.pipe.ts @@ -1,29 +1,33 @@ -import { validateFileFormat, validateFileSize } from '@/src/shared/util/file.utils' -import { type ReadStream } from 'node:fs' -import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common' -import * as FileUpload from 'graphql-upload/Upload.js' +import { + ArgumentMetadata, BadRequestException, Injectable, PipeTransform, +} from '@nestjs/common'; +import * as FileUpload from 'graphql-upload/Upload.js'; + +import { validateFileFormat, validateFileSize } from '@/src/shared/util/file.utils'; + +import type { ReadStream } from 'node:fs'; @Injectable() export class FileValidationPipe implements PipeTransform { async transform(value: FileUpload, metadata: ArgumentMetadata): FileUpload { if (!value.filename) { - throw new BadRequestException('Файл не загружен') + throw new BadRequestException('Файл не загружен'); } - const { filename, createReadStream } = value - const fileStream = createReadStream() as ReadStream - const allowedFormats = ['jpg', 'jpeg', 'png', 'webp', 'gif'] - const allowedFileSizeInByte = 5 * 1024 * 1024 - const isFileFormatValidate = validateFileFormat(filename, allowedFormats) + const { filename, createReadStream } = value; + const fileStream = createReadStream() as ReadStream; + const allowedFormats = ['jpg', 'jpeg', 'png', 'webp', 'gif']; + const allowedFileSizeInByte = 5 * 1024 * 1024; + const isFileFormatValidate = validateFileFormat(filename, allowedFormats); if (!isFileFormatValidate) { - throw new BadRequestException('Неподдерживаемый формат файла') + throw new BadRequestException('Неподдерживаемый формат файла'); } - const isFileSizeValid = await validateFileSize(fileStream, allowedFileSizeInByte) + const isFileSizeValid = await validateFileSize(fileStream, allowedFileSizeInByte); if (!isFileSizeValid) { - throw new BadRequestException('Превышен максимальный размер файла') + throw new BadRequestException('Превышен максимальный размер файла'); } - return value + return value; } } diff --git a/backend/src/shared/types/env.d.ts b/backend/src/shared/types/env.d.ts index 69b864d..83215eb 100644 --- a/backend/src/shared/types/env.d.ts +++ b/backend/src/shared/types/env.d.ts @@ -1,52 +1,52 @@ -import { StringValue } from '../util/ms.util' +import type { StringValue } from '../util/ms.util'; -export interface ProcessEnv { - NODE_ENV: 'development' | 'production' +export type ProcessEnv = { + NODE_ENV: 'development' | 'production'; - APPLICATION_PORT: number - APPLICATION_URL: string - ALLOWED_ORIGIN: string + APPLICATION_PORT: number; + APPLICATION_URL: string; + ALLOWED_ORIGIN: string; - COOKIE_SECRET: string - SESSION_SECRET: string - SESSION_NAME: string - SESSION_DOMAIN: string - SESSION_MAX_AGE: StringValue - SESSION_HTTP_ONLY: boolean - SESSION_SECURE: boolean - SESSION_FOLDER: string + COOKIE_SECRET: string; + SESSION_SECRET: string; + SESSION_NAME: string; + SESSION_DOMAIN: string; + SESSION_MAX_AGE: StringValue; + SESSION_HTTP_ONLY: boolean; + SESSION_SECURE: boolean; + SESSION_FOLDER: string; - GRAPHQL_PREFIX: string + GRAPHQL_PREFIX: string; - POSTGRES_USER: string - POSTGRES_PASSWORD: string - POSTGRES_HOST: string - POSTGRES_PORT: number - POSTGRES_DATABASE: string - POSTGRES_URI: string + POSTGRES_USER: string; + POSTGRES_PASSWORD: string; + POSTGRES_HOST: string; + POSTGRES_PORT: number; + POSTGRES_DATABASE: string; + POSTGRES_URI: string; - REDIS_USER: string - REDIS_PASSWORD: string - REDIS_HOST: string - REDIS_PORT: number - REDIS_URI: string + REDIS_USER: string; + REDIS_PASSWORD: string; + REDIS_HOST: string; + REDIS_PORT: number; + REDIS_URI: string; - MAIL_HOST: string - MAIL_PORT: number - MAIL_LOGIN: string - MAIL_PASSWORD: string + MAIL_HOST: string; + MAIL_PORT: number; + MAIL_LOGIN: string; + MAIL_PASSWORD: string; - S3_ENDPOINT: string - S3_REGION: string - S3_ACCESS_KEY_ID: string - S3_SECRET_KEY_ID: string - S3_BUCKET_NAME: string + S3_ENDPOINT: string; + S3_REGION: string; + S3_ACCESS_KEY_ID: string; + S3_SECRET_KEY_ID: string; + S3_BUCKET_NAME: string; - LIVEKIT_URL: string - LIVEKIT_API_KEY: string - LIVEKIT_API_SECRET: string + LIVEKIT_URL: string; + LIVEKIT_API_KEY: string; + LIVEKIT_API_SECRET: string; - TELEGRAM_BOT_TOKEN: string - STRIPE_SECRET_KEY: string - STRIPE_WEBHOOK_SECRET: string -} + TELEGRAM_BOT_TOKEN: string; + STRIPE_SECRET_KEY: string; + STRIPE_WEBHOOK_SECRET: string; +}; diff --git a/backend/src/shared/types/express-request.d.ts b/backend/src/shared/types/express-request.d.ts index 5f6a38c..79689d9 100644 --- a/backend/src/shared/types/express-request.d.ts +++ b/backend/src/shared/types/express-request.d.ts @@ -1,9 +1,10 @@ -import { User } from '@prisma/generated' +import type { User } from '@prisma/generated'; declare global { namespace Express { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Request { - user?: User + user?: User; } } } diff --git a/backend/src/shared/types/express-session.d.ts b/backend/src/shared/types/express-session.d.ts index 18ed0d3..5bb8989 100644 --- a/backend/src/shared/types/express-session.d.ts +++ b/backend/src/shared/types/express-session.d.ts @@ -1,10 +1,11 @@ -import 'express-session' -import { SessionInfo } from './session-metadata.types' +import 'express-session'; +import type { SessionInfo } from './session-metadata.types'; declare module 'express-session' { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface SessionData { - userId?: string - createdAt?: Date | string - metadata: SessionInfo + userId?: string; + createdAt?: Date | string; + metadata: SessionInfo; } } diff --git a/backend/src/shared/types/gql-context.types.ts b/backend/src/shared/types/gql-context.types.ts index e538b13..9a6c39a 100644 --- a/backend/src/shared/types/gql-context.types.ts +++ b/backend/src/shared/types/gql-context.types.ts @@ -1,6 +1,6 @@ -import { Request, Response } from 'express' +import type { Request, Response } from 'express'; -export interface GqlContext { - req: Request - res: Response -} +export type GqlContext = { + req: Request; + res: Response; +}; diff --git a/backend/src/shared/types/session-metadata.types.ts b/backend/src/shared/types/session-metadata.types.ts index 82b33ee..e8142e6 100644 --- a/backend/src/shared/types/session-metadata.types.ts +++ b/backend/src/shared/types/session-metadata.types.ts @@ -1,18 +1,18 @@ -export interface LocationInfo { - country: string - city: string - longitude: number - latitude: number -} +export type LocationInfo = { + country: string; + city: string; + longitude: number; + latitude: number; +}; -export interface DeviceInfo { - browser: string - os: string - type: string -} +export type DeviceInfo = { + browser: string; + os: string; + type: string; +}; -export interface SessionInfo { - location: LocationInfo - device: DeviceInfo - ip: string -} +export type SessionInfo = { + location: LocationInfo; + device: DeviceInfo; + ip: string; +}; diff --git a/backend/src/shared/util/file.utils.ts b/backend/src/shared/util/file.utils.ts index 8872cb0..6e9322f 100644 --- a/backend/src/shared/util/file.utils.ts +++ b/backend/src/shared/util/file.utils.ts @@ -1,13 +1,13 @@ -import { ReadStream } from 'fs' +import type { ReadStream } from 'node:fs'; export function validateFileFormat( fileName: string, allowedFormats: string[], ) { - const fileParts = fileName.split('.') - const extension = fileParts[fileParts.length - 1] + const fileParts = fileName.split('.'); + const extension = fileParts[fileParts.length - 1]; - return allowedFormats.includes(extension) + return allowedFormats.includes(extension); } export async function validateFileSize( @@ -15,11 +15,11 @@ export async function validateFileSize( allowedFileSizeInBytes: number, ) { return new Promise((res, rej) => { - let fileSizeInBytes = 0 + let fileSizeInBytes = 0; fileStream - .on('data', (data: Buffer) => { fileSizeInBytes = data.byteLength }) - .on('end', () => { res(fileSizeInBytes <= allowedFileSizeInBytes) }) - .on('error', (error) => { rej(error) }) - }) + .on('data', (data: Buffer) => { fileSizeInBytes = data.byteLength; }) + .on('end', () => { res(fileSizeInBytes <= allowedFileSizeInBytes); }) + .on('error', (error) => { rej(error); }); + }); } diff --git a/backend/src/shared/util/generate-token.util.ts b/backend/src/shared/util/generate-token.util.ts index 1a1b602..335d4b3 100644 --- a/backend/src/shared/util/generate-token.util.ts +++ b/backend/src/shared/util/generate-token.util.ts @@ -1,23 +1,27 @@ -import { PrismaService } from '@/src/core/prisma/prisma.service' -import { $Enums, User } from '@prisma/generated' -import TokenType = $Enums.TokenType -import { v4 as uuidv4 } from 'uuid' +import { v4 as uuidv4 } from 'uuid'; -export async function generateToken(prismaService: PrismaService, user: User, type: TokenType, isUUID: boolean = true) { +import { $Enums } from '@prisma/generated'; + +import TokenType = $Enums.TokenType; + +import type { PrismaService } from '@/src/core/prisma/prisma.service'; +import type { User } from '@prisma/generated'; + +export async function generateToken(prismaService: PrismaService, user: User, type: TokenType, isUUID = true) { const token = isUUID ? uuidv4() - : Math.floor(Math.random() * (1_000_000 - 100_000) + 100_000).toString() + : Math.floor(Math.random() * (1_000_000 - 100_000) + 100_000).toString(); - const expiresIn = new Date(new Date().getTime() + 15 * 60 * 1000) + const expiresIn = new Date(new Date().getTime() + 15 * 60 * 1000); const existingToken = await prismaService.token.findFirst({ where: { type, userId: user.id, }, - }) + }); if (existingToken) { - await prismaService.token.delete({ where: { id: existingToken.id } }) + await prismaService.token.delete({ where: { id: existingToken.id } }); } return prismaService.token.create({ @@ -38,5 +42,5 @@ export async function generateToken(prismaService: PrismaService, user: User, ty }, }, }, - }) + }); } diff --git a/backend/src/shared/util/is-dev.util.ts b/backend/src/shared/util/is-dev.util.ts index 6c3751e..adc607e 100644 --- a/backend/src/shared/util/is-dev.util.ts +++ b/backend/src/shared/util/is-dev.util.ts @@ -1,12 +1,14 @@ -import { ConfigService } from '@nestjs/config' -import * as dotenv from 'dotenv' -import * as process from 'node:process' -import { ProcessEnv } from '../types/env'; +import * as process from 'node:process'; -dotenv.config() +import * as dotenv from 'dotenv'; + +import type { ProcessEnv } from '../types/env'; +import type { ConfigService } from '@nestjs/config'; + +dotenv.config(); export function isDev(configService: ConfigService) { - return configService.getOrThrow('NODE_ENV') === 'development' + return configService.getOrThrow('NODE_ENV') === 'development'; } -export const IS_DEV = process.env.NODE_ENV === 'development' +export const IS_DEV = process.env.NODE_ENV === 'development'; diff --git a/backend/src/shared/util/ms.util.ts b/backend/src/shared/util/ms.util.ts index 7eb0f45..05aa304 100644 --- a/backend/src/shared/util/ms.util.ts +++ b/backend/src/shared/util/ms.util.ts @@ -1,68 +1,33 @@ -const s = 1000 -const m = s * 60 -const h = m * 60 -const d = h * 24 -const w = d * 7 -const y = d * 365.25 +const s = 1000; +const m = s * 60; +const h = m * 60; +const d = h * 24; +const w = d * 7; +const y = d * 365.25; -type Unit - = | 'Years' - | 'Year' - | 'Yrs' - | 'Yr' - | 'Y' - | 'Weeks' - | 'Week' - | 'W' - | 'Days' - | 'Day' - | 'D' - | 'Hours' - | 'Hour' - | 'Hrs' - | 'Hr' - | 'H' - | 'Minutes' - | 'Minute' - | 'Mins' - | 'Min' - | 'M' - | 'Seconds' - | 'Second' - | 'Secs' - | 'Sec' - | 's' - | 'Milliseconds' - | 'Millisecond' - | 'Msecs' - | 'Msec' - | 'Ms' +type Unit = 'D' | 'Day' | 'Days' | 'H' | 'Hour' | 'Hours' | 'Hr' | 'Hrs' | 'M' | 'Millisecond' | 'Milliseconds' | 'Min' | 'Mins' | 'Minute' | 'Minutes' | 'Ms' | 'Msec' | 'Msecs' | 's' | 'Sec' | 'Second' | 'Seconds' | 'Secs' | 'W' | 'Week' | 'Weeks' | 'Y' | 'Year' | 'Years' | 'Yr' | 'Yrs'; -type UnitAnyCase = Unit | Uppercase | Lowercase +type UnitAnyCase = Lowercase | Unit | Uppercase; -export type StringValue - = | `${number}` - | `${number}${UnitAnyCase}` - | `${number} ${UnitAnyCase}` +export type StringValue = `${number} ${UnitAnyCase}` | `${number}` | `${number}${UnitAnyCase}`; export function ms(str: StringValue): number { if (typeof str !== 'string' || str.length === 0 || str.length > 100) { throw new Error( 'Value provided to ms() must be a string with length between 1 and 99.', - ) + ); } - const match - = /^(?-?(?:\d+)?\.?\d+) *(?milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec( - str, - ) + const match = (/^(?-?(?:\d+)?\.?\d+) *(?milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i).exec( + str, + ); - const groups = match?.groups as { value: string, type?: string } | undefined + const groups = match?.groups as { value: string; type?: string } | undefined; if (!groups) { - return NaN + return NaN; } - const n = parseFloat(groups.value) - const type = (groups.type || 'ms').toLowerCase() as Lowercase + const n = parseFloat(groups.value); + const type = (groups.type || 'ms').toLowerCase() as Lowercase; switch (type) { case 'years': @@ -70,43 +35,43 @@ export function ms(str: StringValue): number { case 'yrs': case 'yr': case 'y': - return n * y + return n * y; case 'weeks': case 'week': case 'w': - return n * w + return n * w; case 'days': case 'day': case 'd': - return n * d + return n * d; case 'hours': case 'hour': case 'hrs': case 'hr': case 'h': - return n * h + return n * h; case 'minutes': case 'minute': case 'mins': case 'min': case 'm': - return n * m + return n * m; case 'seconds': case 'second': case 'secs': case 'sec': case 's': - return n * s + return n * s; case 'milliseconds': case 'millisecond': case 'msecs': case 'msec': case 'ms': - return n + return n; default: throw new Error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Ошибка: единица времени ${type} была распознана, но не существует соответствующего случая. Пожалуйста, проверьте введенные данные.`, - ) + ); } } diff --git a/backend/src/shared/util/parse-boolean.util.ts b/backend/src/shared/util/parse-boolean.util.ts index d174b84..fe78dce 100644 --- a/backend/src/shared/util/parse-boolean.util.ts +++ b/backend/src/shared/util/parse-boolean.util.ts @@ -1,19 +1,19 @@ export function parseBoolean(value: string): boolean { if (typeof value === 'boolean') { - return value + return value; } if (typeof value === 'string') { - const lowerValue = value.trim().toLowerCase() + const lowerValue = value.trim().toLowerCase(); if (lowerValue === 'true') { - return true + return true; } if (lowerValue === 'false') { - return false + return false; } } throw new Error( `Не удалось преобразовать значение "${value}" в логическое значение.`, - ) + ); } diff --git a/backend/src/shared/util/session-metadata.util.ts b/backend/src/shared/util/session-metadata.util.ts index 2297f4e..65f1c2a 100644 --- a/backend/src/shared/util/session-metadata.util.ts +++ b/backend/src/shared/util/session-metadata.util.ts @@ -1,30 +1,30 @@ -import { SessionInfo } from '@/src/shared/types/session-metadata.types' -import { IS_DEV } from '@/src/shared/util/is-dev.util' -import { Request } from 'express' -import { lookup } from 'geoip-lite' -// eslint-disable-next-line @typescript-eslint/no-require-imports -import DeviceDetector = require('device-detector-js') -import * as countries from 'i18n-iso-countries' +import DeviceDetector = require('device-detector-js'); +import { lookup } from 'geoip-lite'; +import * as countries from 'i18n-iso-countries'; -// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-unsafe-argument -countries.registerLocale(require('i18n-iso-countries/langs/en.json')) +import { IS_DEV } from '@/src/shared/util/is-dev.util'; + +import type { SessionInfo } from '@/src/shared/types/session-metadata.types'; +import type { Request } from 'express'; + +countries.registerLocale(require('i18n-iso-countries/langs/en.json')); export function getSessionMetadata(req: Request, userAgent: string): SessionInfo { const ip = IS_DEV ? '174.136.85.11' : ( - Array.isArray(req.headers['cf-connecting-ip']) - ? req.headers['cf-connecting-ip'][0] - : req.headers['cf-connecting-ip'] - ) - || ( - typeof req.headers['x-forwarded-for'] === 'string' - ? req.headers['x-forwarded-for'].split(',')[0] - : req.ip - ) as string + Array.isArray(req.headers['cf-connecting-ip']) + ? req.headers['cf-connecting-ip'][0] + : req.headers['cf-connecting-ip'] + ) + || ( + typeof req.headers['x-forwarded-for'] === 'string' + ? req.headers['x-forwarded-for'].split(',')[0] + : req.ip + ) as string; - const location = lookup(ip) - const device = new DeviceDetector().parse(userAgent) + const location = lookup(ip); + const device = new DeviceDetector().parse(userAgent); return { ip, @@ -39,5 +39,5 @@ export function getSessionMetadata(req: Request, userAgent: string): SessionInfo longitude: location?.ll[0] ?? 0, latitude: location?.ll[1] ?? 0, }, - } + }; } diff --git a/backend/src/shared/util/session.util.ts b/backend/src/shared/util/session.util.ts index c3bb683..15a914e 100644 --- a/backend/src/shared/util/session.util.ts +++ b/backend/src/shared/util/session.util.ts @@ -1,35 +1,36 @@ -import { SessionInfo } from '@/src/shared/types/session-metadata.types' -import { InternalServerErrorException } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { User } from '@prisma/generated' -import { Request } from 'express' -import { ProcessEnv } from '../types/env' +import { InternalServerErrorException } from '@nestjs/common'; -export function saveSession(req: Request, user: User, metadata: SessionInfo) { +import type { ProcessEnv } from '../types/env'; +import type { SessionInfo } from '@/src/shared/types/session-metadata.types'; +import type { ConfigService } from '@nestjs/config'; +import type { User } from '@prisma/generated'; +import type { Request } from 'express'; + +export async function saveSession(req: Request, user: User, metadata: SessionInfo) { return new Promise((resolve, reject) => { - req.session.createdAt = new Date() - req.session.userId = user.id - req.session.metadata = metadata + req.session.createdAt = new Date(); + req.session.userId = user.id; + req.session.metadata = metadata; req.session.save((error) => { if (error) { - reject(new InternalServerErrorException('Не удалось сохранить сессию')) + reject(new InternalServerErrorException('Не удалось сохранить сессию')); } - resolve({ user }) - }) - }) + resolve({ user }); + }); + }); } -export function destroySession(req: Request, configService: ConfigService) { +export async function destroySession(req: Request, configService: ConfigService) { return new Promise((resolve, reject) => { - req.session.destroy((error) => { + req.session.destroy((error: unknown) => { if (error) { - reject(new InternalServerErrorException('Не удалось завершить сессию')) + reject(new InternalServerErrorException('Не удалось завершить сессию')); } - req.res?.clearCookie(configService.getOrThrow('SESSION_NAME')) - resolve(true) - }) - }) + req.res?.clearCookie(configService.getOrThrow('SESSION_NAME')); + resolve(true); + }); + }); } diff --git a/backend/yarn.lock b/backend/yarn.lock index 6e675b0..c6b34ad 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1213,7 +1213,7 @@ dependencies: "@types/json-schema" "^7.0.15" -"@eslint/eslintrc@3.3.1", "@eslint/eslintrc@^3.2.0", "@eslint/eslintrc@^3.3.1": +"@eslint/eslintrc@3.3.1", "@eslint/eslintrc@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964" integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== @@ -1228,7 +1228,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.29.0", "@eslint/js@^9.18.0": +"@eslint/js@9.29.0": version "9.29.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.29.0.tgz#dc6fd117c19825f8430867a662531da36320fe56" integrity sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ== @@ -2991,18 +2991,6 @@ estraverse "^5.3.0" picomatch "^4.0.3" -"@stylistic/eslint-plugin@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-5.0.0.tgz#587a2d0ca80e3395ad16d8044a62d40119e1b4a7" - integrity sha512-nVV2FSzeTJ3oFKw+3t9gQYQcrgbopgCASSY27QOtkhEGgSfdQQjDmzZd41NeT1myQ8Wc6l+pZllST9qIu4NKzg== - dependencies: - "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/types" "^8.34.1" - eslint-visitor-keys "^4.2.1" - espree "^10.4.0" - estraverse "^5.3.0" - picomatch "^4.0.2" - "@swc/cli@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@swc/cli/-/cli-0.6.0.tgz#fe986a436797c9d3850938366dbd660c9ba1101f" @@ -3510,21 +3498,6 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz#56cf35b89383eaf2bdcf602f5bbdac6dbb11e51b" - integrity sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.34.1" - "@typescript-eslint/type-utils" "8.34.1" - "@typescript-eslint/utils" "8.34.1" - "@typescript-eslint/visitor-keys" "8.34.1" - graphemer "^1.4.0" - ignore "^7.0.0" - natural-compare "^1.4.0" - ts-api-utils "^2.1.0" - "@typescript-eslint/eslint-plugin@8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70" @@ -3540,17 +3513,6 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.34.1.tgz#f102357ab3a02d5b8aa789655905662cc5093067" - integrity sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA== - dependencies: - "@typescript-eslint/scope-manager" "8.34.1" - "@typescript-eslint/types" "8.34.1" - "@typescript-eslint/typescript-estree" "8.34.1" - "@typescript-eslint/visitor-keys" "8.34.1" - debug "^4.3.4" - "@typescript-eslint/parser@8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804" @@ -3562,15 +3524,6 @@ "@typescript-eslint/visitor-keys" "8.37.0" debug "^4.3.4" -"@typescript-eslint/project-service@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.34.1.tgz#20501f8b87202c45f5e70a5b24dcdcb8fe12d460" - integrity sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA== - dependencies: - "@typescript-eslint/tsconfig-utils" "^8.34.1" - "@typescript-eslint/types" "^8.34.1" - debug "^4.3.4" - "@typescript-eslint/project-service@8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe" @@ -3580,14 +3533,6 @@ "@typescript-eslint/types" "^8.37.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz#727ea43441f4d23d5c73d34195427d85042e5117" - integrity sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA== - dependencies: - "@typescript-eslint/types" "8.34.1" - "@typescript-eslint/visitor-keys" "8.34.1" - "@typescript-eslint/scope-manager@8.37.0", "@typescript-eslint/scope-manager@^8.15.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4" @@ -3596,26 +3541,11 @@ "@typescript-eslint/types" "8.37.0" "@typescript-eslint/visitor-keys" "8.37.0" -"@typescript-eslint/tsconfig-utils@8.34.1", "@typescript-eslint/tsconfig-utils@^8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz#d6abb1b1e9f1f1c83ac92051c8fbf2dbc4dc9f5e" - integrity sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg== - "@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053" integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg== -"@typescript-eslint/type-utils@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz#df860d8edefbfe142473ea4defb7408edb0c379e" - integrity sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g== - dependencies: - "@typescript-eslint/typescript-estree" "8.34.1" - "@typescript-eslint/utils" "8.34.1" - debug "^4.3.4" - ts-api-utils "^2.1.0" - "@typescript-eslint/type-utils@8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507" @@ -3627,32 +3557,11 @@ debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.34.1", "@typescript-eslint/types@^8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.34.1.tgz#565a46a251580dae674dac5aafa8eb14b8322a35" - integrity sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA== - "@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff" integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ== -"@typescript-eslint/typescript-estree@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz#befdb042a6bc44fdad27429b2d3b679c80daad71" - integrity sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA== - dependencies: - "@typescript-eslint/project-service" "8.34.1" - "@typescript-eslint/tsconfig-utils" "8.34.1" - "@typescript-eslint/types" "8.34.1" - "@typescript-eslint/visitor-keys" "8.34.1" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^2.1.0" - "@typescript-eslint/typescript-estree@8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc" @@ -3669,16 +3578,6 @@ semver "^7.6.0" ts-api-utils "^2.1.0" -"@typescript-eslint/utils@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.34.1.tgz#f98c9b0c5cae407e34f5131cac0f3a74347a398e" - integrity sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ== - dependencies: - "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/scope-manager" "8.34.1" - "@typescript-eslint/types" "8.34.1" - "@typescript-eslint/typescript-estree" "8.34.1" - "@typescript-eslint/utils@8.37.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.15.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335" @@ -3689,14 +3588,6 @@ "@typescript-eslint/types" "8.37.0" "@typescript-eslint/typescript-estree" "8.37.0" -"@typescript-eslint/visitor-keys@8.34.1": - version "8.34.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz#28a1987ea3542ccafb92aa792726a304b39531cf" - integrity sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw== - dependencies: - "@typescript-eslint/types" "8.34.1" - eslint-visitor-keys "^4.2.1" - "@typescript-eslint/visitor-keys@8.37.0": version "8.37.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455" @@ -11043,15 +10934,6 @@ typescript-eslint@8.37.0: "@typescript-eslint/typescript-estree" "8.37.0" "@typescript-eslint/utils" "8.37.0" -typescript-eslint@^8.20.0: - version "8.34.1" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.34.1.tgz#4bab64b298531b9f6f3ff59b41a7161321ef8cd6" - integrity sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow== - dependencies: - "@typescript-eslint/eslint-plugin" "8.34.1" - "@typescript-eslint/parser" "8.34.1" - "@typescript-eslint/utils" "8.34.1" - typescript@5.8.3, typescript@^5.7.3: version "5.8.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" @@ -11181,6 +11063,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"