add eslint config
This commit is contained in:
parent
d1c5ac7e19
commit
c4bba76b50
@ -1,113 +1,108 @@
|
|||||||
// @ts-check
|
import config from 'eslint-config-ksv741';
|
||||||
import eslint from '@eslint/js';
|
|
||||||
import globals from 'globals';
|
|
||||||
import tseslint from 'typescript-eslint';
|
|
||||||
import stylistic from '@stylistic/eslint-plugin'
|
|
||||||
|
|
||||||
export default tseslint.config(
|
export default [
|
||||||
|
...config,
|
||||||
{
|
{
|
||||||
ignores: [
|
name: 'my-rules',
|
||||||
'node_modules',
|
files: [
|
||||||
'dist'
|
'**/*.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: {
|
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-explicit-any': 'off',
|
||||||
'@typescript-eslint/no-floating-promises': 'warn',
|
'@typescript-eslint/member-ordering': 'off',
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
'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,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ];
|
|
||||||
|
|||||||
@ -66,15 +66,13 @@
|
|||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"stripe": "^18.3.0",
|
"stripe": "^18.3.0",
|
||||||
"telegraf": "^4.16.3"
|
"telegraf": "^4.16.3",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
|
||||||
"@eslint/js": "^9.18.0",
|
|
||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@stylistic/eslint-plugin": "^5.0.0",
|
|
||||||
"@swc/cli": "^0.6.0",
|
"@swc/cli": "^0.6.0",
|
||||||
"@swc/core": "^1.10.7",
|
"@swc/core": "^1.10.7",
|
||||||
"@types/cookie-parser": "^1.4.9",
|
"@types/cookie-parser": "^1.4.9",
|
||||||
@ -96,8 +94,7 @@
|
|||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3"
|
||||||
"typescript-eslint": "^8.20.0"
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { isDev } from '@/src/shared/util/is-dev.util'
|
import * as path from 'node:path';
|
||||||
import { ApolloDriverConfig } from '@nestjs/apollo'
|
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { isDev } from '@/src/shared/util/is-dev.util';
|
||||||
import { Request, Response } from 'express'
|
|
||||||
import * as path from 'node:path'
|
import type { ProcessEnv } from '../../shared/types/env';
|
||||||
import { 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 = {
|
type ContextType = {
|
||||||
req: Request
|
req: Request;
|
||||||
res: Response
|
res: Response;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function getGraphQLConfig(configService: ConfigService<ProcessEnv>): ApolloDriverConfig {
|
export function getGraphQLConfig(configService: ConfigService<ProcessEnv>): ApolloDriverConfig {
|
||||||
return {
|
return {
|
||||||
@ -18,5 +20,5 @@ export function getGraphQLConfig(configService: ConfigService<ProcessEnv>): Apol
|
|||||||
sortSchema: true,
|
sortSchema: true,
|
||||||
context: ({ req, res }: ContextType) => ({ req, res }),
|
context: ({ req, res }: ContextType) => ({ req, res }),
|
||||||
installSubscriptionHandlers: true,
|
installSubscriptionHandlers: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type'
|
import type { ProcessEnv } from '../../shared/types/env';
|
||||||
import { ConfigService } from '@nestjs/config'
|
import type { TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type';
|
||||||
import { ProcessEnv } from '../../shared/types/env'
|
import type { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
export function getLiveKitConfig(configService: ConfigService<ProcessEnv>): TypeLiveKitOptions {
|
export function getLiveKitConfig(configService: ConfigService<ProcessEnv>): TypeLiveKitOptions {
|
||||||
return {
|
return {
|
||||||
apiSecret: configService.getOrThrow('LIVEKIT_API_SECRET'),
|
apiSecret: configService.getOrThrow('LIVEKIT_API_SECRET'),
|
||||||
apiKey: configService.getOrThrow('LIVEKIT_API_KEY'),
|
apiKey: configService.getOrThrow('LIVEKIT_API_KEY'),
|
||||||
apiUrl: configService.getOrThrow('LIVEKIT_URL'),
|
apiUrl: configService.getOrThrow('LIVEKIT_URL'),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type'
|
import type { TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type';
|
||||||
import { ProcessEnv } from '@/src/shared/types/env'
|
import type { ProcessEnv } from '@/src/shared/types/env';
|
||||||
import { ConfigService } from '@nestjs/config'
|
import type { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
export function getStripeConfig(configService: ConfigService<ProcessEnv>): TypeStripeOptions {
|
export function getStripeConfig(configService: ConfigService<ProcessEnv>): TypeStripeOptions {
|
||||||
return {
|
return {
|
||||||
@ -8,5 +8,5 @@ export function getStripeConfig(configService: ConfigService<ProcessEnv>): TypeS
|
|||||||
apiVersion: '2025-06-30.basil',
|
apiVersion: '2025-06-30.basil',
|
||||||
},
|
},
|
||||||
apiKey: configService.getOrThrow('STRIPE_SECRET_KEY'),
|
apiKey: configService.getOrThrow('STRIPE_SECRET_KEY'),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { ConfigService } from '@nestjs/config'
|
import type { ProcessEnv } from '../../shared/types/env';
|
||||||
import { TelegrafModuleOptions } from 'nestjs-telegraf'
|
import type { ConfigService } from '@nestjs/config';
|
||||||
import { ProcessEnv } from '../../shared/types/env';
|
import type { TelegrafModuleOptions } from 'nestjs-telegraf';
|
||||||
|
|
||||||
export function getTelegrafOptions(configService: ConfigService<ProcessEnv>): TelegrafModuleOptions {
|
export function getTelegrafOptions(configService: ConfigService<ProcessEnv>): TelegrafModuleOptions {
|
||||||
return {
|
return {
|
||||||
token: configService.getOrThrow('TELEGRAM_BOT_TOKEN'),
|
token: configService.getOrThrow('TELEGRAM_BOT_TOKEN'),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +1,39 @@
|
|||||||
import { getGraphQLConfig } from '@/src/core/config/graphql.config'
|
import { ApolloDriver } from '@nestjs/apollo';
|
||||||
import { getLiveKitConfig } from '@/src/core/config/livekit.config'
|
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 { getStripeConfig } from '@/src/core/config/stripe.config';
|
||||||
import { AccountModule } from '@/src/module/auth/account/account.module'
|
import { AccountModule } from '@/src/module/auth/account/account.module';
|
||||||
import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module'
|
import { DeactivateModule } from '@/src/module/auth/deactivate/deactivate.module';
|
||||||
import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module'
|
import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module';
|
||||||
import { ProfileModule } from '@/src/module/auth/profile/profile.module'
|
import { ProfileModule } from '@/src/module/auth/profile/profile.module';
|
||||||
import { SessionModule } from '@/src/module/auth/session/session.module'
|
import { SessionModule } from '@/src/module/auth/session/session.module';
|
||||||
import { TotpModule } from '@/src/module/auth/totp/totp.module'
|
import { TotpModule } from '@/src/module/auth/totp/totp.module';
|
||||||
import { VerificationModule } from '@/src/module/auth/verification/verification.module'
|
import { VerificationModule } from '@/src/module/auth/verification/verification.module';
|
||||||
import { CategoryModule } from '@/src/module/category/category.module'
|
import { CategoryModule } from '@/src/module/category/category.module';
|
||||||
import { ChannelModule } from '@/src/module/channel/channel.module'
|
import { ChannelModule } from '@/src/module/channel/channel.module';
|
||||||
import { ChatModule } from '@/src/module/chat/chat.module'
|
import { ChatModule } from '@/src/module/chat/chat.module';
|
||||||
import { CronModule } from '@/src/module/cron/cron.module'
|
import { CronModule } from '@/src/module/cron/cron.module';
|
||||||
import { FollowModule } from '@/src/module/follow/follow.module'
|
import { FollowModule } from '@/src/module/follow/follow.module';
|
||||||
import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module'
|
import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module';
|
||||||
import { MailModule } from '@/src/module/libs/mail/mail.module'
|
import { MailModule } from '@/src/module/libs/mail/mail.module';
|
||||||
import { StorageModule } from '@/src/module/libs/storage/storage.module'
|
import { StorageModule } from '@/src/module/libs/storage/storage.module';
|
||||||
import { StripeModule } from '@/src/module/libs/stripe/stripe.module';
|
import { StripeModule } from '@/src/module/libs/stripe/stripe.module';
|
||||||
import { TelegramModule } from '@/src/module/libs/telegram/telegram.module'
|
import { TelegramModule } from '@/src/module/libs/telegram/telegram.module';
|
||||||
import { NotificationModule } from '@/src/module/notification/notification.module'
|
import { NotificationModule } from '@/src/module/notification/notification.module';
|
||||||
import { PlanModule } from '@/src/module/sponsorship/plan/plan.module';
|
import { PlanModule } from '@/src/module/sponsorship/plan/plan.module';
|
||||||
import { SubscriptionModule } from '@/src/module/sponsorship/subscription/subscription.module';
|
import { SubscriptionModule } from '@/src/module/sponsorship/subscription/subscription.module';
|
||||||
import { TransactionModule } from '@/src/module/sponsorship/transaction/transaction.module';
|
import { TransactionModule } from '@/src/module/sponsorship/transaction/transaction.module';
|
||||||
import { IngressModule } from '@/src/module/stream/ingress/ingress.module'
|
import { IngressModule } from '@/src/module/stream/ingress/ingress.module';
|
||||||
import { StreamModule } from '@/src/module/stream/stream.module'
|
import { StreamModule } from '@/src/module/stream/stream.module';
|
||||||
import { WebhookModule } from '@/src/module/webhook/webhook.module'
|
import { WebhookModule } from '@/src/module/webhook/webhook.module';
|
||||||
import { IS_DEV } from '@/src/shared/util/is-dev.util'
|
import { IS_DEV } from '@/src/shared/util/is-dev.util';
|
||||||
import { ApolloDriver } from '@nestjs/apollo'
|
|
||||||
import { Module } from '@nestjs/common'
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config'
|
import { RedisModule } from './redis/redis.module';
|
||||||
import { GraphQLModule } from '@nestjs/graphql'
|
|
||||||
import { PrismaModule } from './prisma/prisma.module'
|
|
||||||
import { RedisModule } from './redis/redis.module'
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@ -125,4 +125,4 @@ export const CATEGORIES = [
|
|||||||
'Погрузитесь в захватывающий мир спорта, где страсть, соревнование и дух командной игры сливаются воедино! Эта категория предназначена для всех поклонников активного образа жизни и спортивных событий, где вы сможете следить за увлекательными матчами, обсуждать стратегии команд и делиться своим опытом с другими фанатами. Присоединяйтесь к стримам, следите за последними новостями и результатами, а также находите единомышленников для совместных тренировок и обсуждений. Откройте для себя удивительный мир спорта, где каждое соревнование — это возможность проявить свои силы и стремление к победе!',
|
'Погрузитесь в захватывающий мир спорта, где страсть, соревнование и дух командной игры сливаются воедино! Эта категория предназначена для всех поклонников активного образа жизни и спортивных событий, где вы сможете следить за увлекательными матчами, обсуждать стратегии команд и делиться своим опытом с другими фанатами. Присоединяйтесь к стримам, следите за последними новостями и результатами, а также находите единомышленников для совместных тренировок и обсуждений. Откройте для себя удивительный мир спорта, где каждое соревнование — это возможность проявить свои силы и стремление к победе!',
|
||||||
thumbnailUrl: '/categories/sport.webp',
|
thumbnailUrl: '/categories/sport.webp',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export const STREAMS = {
|
export const STREAMS = {
|
||||||
'minecraft': [
|
minecraft: [
|
||||||
'Проходим Minecraft: выживание с нуля!',
|
'Проходим Minecraft: выживание с нуля!',
|
||||||
'Строим эпические постройки в Minecraft',
|
'Строим эпические постройки в Minecraft',
|
||||||
'Тайны Майнкрафт: приключения начинаются!',
|
'Тайны Майнкрафт: приключения начинаются!',
|
||||||
@ -23,7 +23,7 @@ export const STREAMS = {
|
|||||||
'Обзор новых машин и тюнинга в GTA Online',
|
'Обзор новых машин и тюнинга в GTA Online',
|
||||||
'Создаем свой бизнес в Лос-Сантосе',
|
'Создаем свой бизнес в Лос-Сантосе',
|
||||||
],
|
],
|
||||||
'rust': [
|
rust: [
|
||||||
'Рейдим базы в Rust',
|
'Рейдим базы в Rust',
|
||||||
'Выживание в пустоши: Rust',
|
'Выживание в пустоши: Rust',
|
||||||
'Новые механики Rust: тестируем!',
|
'Новые механики Rust: тестируем!',
|
||||||
@ -71,7 +71,7 @@ export const STREAMS = {
|
|||||||
'Становимся охотниками за головами в RDR2',
|
'Становимся охотниками за головами в RDR2',
|
||||||
'Обзор легендарных зверей и трофеев',
|
'Обзор легендарных зверей и трофеев',
|
||||||
],
|
],
|
||||||
'learning': [
|
learning: [
|
||||||
'Изучаем основы фотографии',
|
'Изучаем основы фотографии',
|
||||||
'Как стать мастером ораторского искусства',
|
'Как стать мастером ораторского искусства',
|
||||||
'Погружаемся в искусство рисования',
|
'Погружаемся в искусство рисования',
|
||||||
@ -83,7 +83,7 @@ export const STREAMS = {
|
|||||||
'Изучаем основы кулинарии: готовим вместе',
|
'Изучаем основы кулинарии: готовим вместе',
|
||||||
'Учим основы финансовой грамотности',
|
'Учим основы финансовой грамотности',
|
||||||
],
|
],
|
||||||
'fortnite': [
|
fortnite: [
|
||||||
'Стрим по Fortnite: королевская битва!',
|
'Стрим по Fortnite: королевская битва!',
|
||||||
'Секреты строительства в Fortnite',
|
'Секреты строительства в Fortnite',
|
||||||
'Лучшие тактики для победы в Fortnite',
|
'Лучшие тактики для победы в Fortnite',
|
||||||
@ -107,7 +107,7 @@ export const STREAMS = {
|
|||||||
'Киберспортивные команды: следим за матчами',
|
'Киберспортивные команды: следим за матчами',
|
||||||
'Участвуем в тренировочных матчах CS:GO',
|
'Участвуем в тренировочных матчах CS:GO',
|
||||||
],
|
],
|
||||||
'programming': [
|
programming: [
|
||||||
'Программируем на JavaScript: от простого к сложному',
|
'Программируем на JavaScript: от простого к сложному',
|
||||||
'Разработка игр на Python: шаг за шагом',
|
'Разработка игр на Python: шаг за шагом',
|
||||||
'Создание веб-приложений с React: практическое руководство',
|
'Создание веб-приложений с React: практическое руководство',
|
||||||
@ -155,7 +155,7 @@ export const STREAMS = {
|
|||||||
'Стримим клановые битвы в Clash Royale',
|
'Стримим клановые битвы в Clash Royale',
|
||||||
'Побеждаем в дуэлях и соревнованиях',
|
'Побеждаем в дуэлях и соревнованиях',
|
||||||
],
|
],
|
||||||
'music': [
|
music: [
|
||||||
'Музыкальные новинки: обсуждаем хиты!',
|
'Музыкальные новинки: обсуждаем хиты!',
|
||||||
'Слушаем и обсуждаем любимые альбомы',
|
'Слушаем и обсуждаем любимые альбомы',
|
||||||
'Обсуждаем музыкальные жанры: что слушать?',
|
'Обсуждаем музыкальные жанры: что слушать?',
|
||||||
@ -203,7 +203,7 @@ export const STREAMS = {
|
|||||||
'Проходим ранговые игры в League of Legends',
|
'Проходим ранговые игры в League of Legends',
|
||||||
'Обзор новых скинов и событий в League of Legends',
|
'Обзор новых скинов и событий в League of Legends',
|
||||||
],
|
],
|
||||||
'sport': [
|
sport: [
|
||||||
'Обсуждаем спортивные события: последние новости!',
|
'Обсуждаем спортивные события: последние новости!',
|
||||||
'Спортивные тренировки: секреты успеха',
|
'Спортивные тренировки: секреты успеха',
|
||||||
'Лучшие моменты в мире спорта',
|
'Лучшие моменты в мире спорта',
|
||||||
@ -215,4 +215,4 @@ export const STREAMS = {
|
|||||||
'Реакции на последние спортивные трансляции',
|
'Реакции на последние спортивные трансляции',
|
||||||
'Участвуем в спортивных турнирах и соревнованиях',
|
'Участвуем в спортивных турнирах и соревнованиях',
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
|
|||||||
@ -101,4 +101,4 @@ export const USERNAMES = [
|
|||||||
'tristan',
|
'tristan',
|
||||||
'ulysses',
|
'ulysses',
|
||||||
'violet',
|
'violet',
|
||||||
]
|
];
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Global, Module } from '@nestjs/common'
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { PrismaService } from './prisma.service'
|
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { BadRequestException, Logger } from '@nestjs/common'
|
import { BadRequestException, Logger } from '@nestjs/common';
|
||||||
import { hash } from 'argon2'
|
import { hash } from 'argon2';
|
||||||
import { Prisma, PrismaClient } from '../../../prisma/generated'
|
|
||||||
|
|
||||||
import { CATEGORIES } from './data/categories.data'
|
import { Prisma, PrismaClient } from '@/prisma/generated';
|
||||||
import { USERNAMES } from './data/users.data'
|
|
||||||
import { STREAMS } from './data/streams.data'
|
import { CATEGORIES } from './data/categories.data';
|
||||||
|
import { STREAMS } from './data/streams.data';
|
||||||
|
import { USERNAMES } from './data/users.data';
|
||||||
|
|
||||||
const prisma = new PrismaClient({
|
const prisma = new PrismaClient({
|
||||||
transactionOptions: {
|
transactionOptions: {
|
||||||
@ -12,25 +13,25 @@ const prisma = new PrismaClient({
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
Logger.log('Начало заполнения базы данных')
|
Logger.log('Начало заполнения базы данных');
|
||||||
|
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
prisma.user.deleteMany({}),
|
prisma.user.deleteMany({}),
|
||||||
prisma.socialLink.deleteMany({}),
|
prisma.socialLink.deleteMany({}),
|
||||||
prisma.stream.deleteMany({}),
|
prisma.stream.deleteMany({}),
|
||||||
prisma.category.deleteMany({}),
|
prisma.category.deleteMany({}),
|
||||||
])
|
]);
|
||||||
|
|
||||||
await prisma.category.createMany({ data: CATEGORIES })
|
await prisma.category.createMany({ data: CATEGORIES });
|
||||||
Logger.log('Категории успешно созданы')
|
Logger.log('Категории успешно созданы');
|
||||||
const categories = await prisma.category.findMany()
|
const categories = await prisma.category.findMany();
|
||||||
|
|
||||||
const categoriesBySlug = Object.fromEntries(
|
const categoriesBySlug = Object.fromEntries(
|
||||||
categories.map(category => [category.slug, category]),
|
categories.map((category) => [category.slug, category]),
|
||||||
)
|
);
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
for (const name of USERNAMES) {
|
for (const name of USERNAMES) {
|
||||||
@ -38,9 +39,9 @@ async function main() {
|
|||||||
Object.keys(categoriesBySlug)[
|
Object.keys(categoriesBySlug)[
|
||||||
Math.floor(Math.random() * Object.keys(categoriesBySlug).length)
|
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) {
|
if (!userExists) {
|
||||||
const createdUser = await tx.user.create({
|
const createdUser = await tx.user.create({
|
||||||
data: {
|
data: {
|
||||||
@ -70,10 +71,10 @@ async function main() {
|
|||||||
// create: {},
|
// create: {},
|
||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const randomTitles = STREAMS[randomCategory.slug] as string
|
const randomTitles = STREAMS[randomCategory.slug] as string;
|
||||||
const randomTitle = randomTitles[Math.floor(Math.random() * randomTitles.length)]
|
const randomTitle = randomTitles[Math.floor(Math.random() * randomTitles.length)];
|
||||||
|
|
||||||
await tx.stream.create({
|
await tx.stream.create({
|
||||||
data: {
|
data: {
|
||||||
@ -90,21 +91,20 @@ async function main() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
Logger.log(`Пользователь "${createdUser.name}" и его стрим успешно созданы`)
|
Logger.log(`Пользователь "${createdUser.name}" и его стрим успешно созданы`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
Logger.error(e);
|
||||||
Logger.error(e)
|
|
||||||
throw new BadRequestException('Ошибка при заполнении базы данных')
|
throw new BadRequestException('Ошибка при заполнении базы данных');
|
||||||
}
|
} finally {
|
||||||
finally {
|
Logger.log('Закрытие соединения с базой данных');
|
||||||
Logger.log('Закрытие соединения с базой данных')
|
await prisma.$disconnect();
|
||||||
await prisma.$disconnect()
|
Logger.log('Соединение с базой данныз успешно закрыто');
|
||||||
Logger.log('Соединение с базой данныз успешно закрыто')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
void main();
|
||||||
|
|||||||
@ -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()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect()
|
await this.$connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
await this.$disconnect()
|
await this.$disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Global, Module } from '@nestjs/common'
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { RedisService } from './redis.service'
|
|
||||||
|
import { RedisService } from './redis.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { ConfigService } from '@nestjs/config';
|
||||||
import Redis from 'ioredis'
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
import { ProcessEnv } from '../../shared/types/env';
|
import { ProcessEnv } from '../../shared/types/env';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -8,6 +9,6 @@ export class RedisService extends Redis {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService<ProcessEnv>,
|
private readonly configService: ConfigService<ProcessEnv>,
|
||||||
) {
|
) {
|
||||||
super(configService.getOrThrow('REDIS_URI'))
|
super(configService.getOrThrow('REDIS_URI'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
import { RedisService } from '@/src/core/redis/redis.service'
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { ms, StringValue } from '@/src/shared/util/ms.util'
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { parseBoolean } from '@/src/shared/util/parse-boolean.util'
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { ValidationPipe } from '@nestjs/common'
|
import RedisStore from 'connect-redis';
|
||||||
import { ConfigService } from '@nestjs/config'
|
import * as cookieParser from 'cookie-parser';
|
||||||
import { NestFactory } from '@nestjs/core'
|
import * as session from 'express-session';
|
||||||
import RedisStore from 'connect-redis'
|
import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';
|
||||||
import * as cookieParser from 'cookie-parser'
|
|
||||||
import { CoreModule } from '@/src/core/core.module'
|
import { CoreModule } from '@/src/core/core.module';
|
||||||
import * as session from 'express-session'
|
import { RedisService } from '@/src/core/redis/redis.service';
|
||||||
import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js'
|
import { ms } from '@/src/shared/util/ms.util';
|
||||||
|
import { parseBoolean } from '@/src/shared/util/parse-boolean.util';
|
||||||
|
|
||||||
async function bootstrap() {
|
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 config = app.get(ConfigService);
|
||||||
const redis = app.get(RedisService)
|
const redis = app.get(RedisService);
|
||||||
|
|
||||||
app.use(cookieParser(config.getOrThrow('COOKIE_SECRET')))
|
app.use(cookieParser(config.getOrThrow('COOKIE_SECRET')));
|
||||||
app.use(config.getOrThrow('GRAPHQL_PREFIX'), graphqlUploadExpress())
|
app.use(config.getOrThrow('GRAPHQL_PREFIX'), graphqlUploadExpress());
|
||||||
|
|
||||||
app.useGlobalPipes(new ValidationPipe({
|
app.useGlobalPipes(new ValidationPipe({
|
||||||
transform: true,
|
transform: true,
|
||||||
}))
|
}));
|
||||||
|
|
||||||
app.use(session({
|
app.use(session({
|
||||||
secret: config.getOrThrow('SESSION_SECRET'),
|
secret: config.getOrThrow('SESSION_SECRET'),
|
||||||
@ -39,14 +40,14 @@ async function bootstrap() {
|
|||||||
client: redis,
|
client: redis,
|
||||||
prefix: config.getOrThrow('SESSION_FOLDER'),
|
prefix: config.getOrThrow('SESSION_FOLDER'),
|
||||||
}),
|
}),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: config.getOrThrow('ALLOWED_ORIGIN'),
|
origin: config.getOrThrow<string>('ALLOWED_ORIGIN'),
|
||||||
credentials: true,
|
credentials: true,
|
||||||
exposedHeaders: ['set-cookie'],
|
exposedHeaders: ['set-cookie'],
|
||||||
})
|
});
|
||||||
|
|
||||||
await app.listen(config.getOrThrow('APPLICATION_PORT'))
|
await app.listen(config.getOrThrow('APPLICATION_PORT'));
|
||||||
}
|
}
|
||||||
void bootstrap()
|
void bootstrap();
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { VerificationService } from '@/src/module/auth/verification/verification.service';
|
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({
|
@Module({
|
||||||
providers: [AccountResolver, AccountService, VerificationService],
|
providers: [AccountResolver, AccountService, VerificationService],
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input'
|
import {
|
||||||
import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input'
|
Args, Mutation, Query, Resolver,
|
||||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
} from '@nestjs/graphql';
|
||||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
|
||||||
import { User } from '@prisma/generated'
|
import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input';
|
||||||
import { CreateUserInput } from './inputs/create-user.input'
|
import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
|
import { Authorization } from '@/src/shared/decorators/auth.decorator';
|
||||||
import { AccountService } from './account.service'
|
import { Authorized } from '@/src/shared/decorators/authorized.decorator';
|
||||||
import { UserModel } from './models/user.model'
|
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')
|
@Resolver('Account')
|
||||||
export class AccountResolver {
|
export class AccountResolver {
|
||||||
@ -14,13 +18,13 @@ export class AccountResolver {
|
|||||||
|
|
||||||
@Query(() => UserModel, { name: 'findProfile' })
|
@Query(() => UserModel, { name: 'findProfile' })
|
||||||
@Authorization()
|
@Authorization()
|
||||||
public me(@Authorized('id') id: UserModel['id']) {
|
public async me(@Authorized('id') id: UserModel['id']) {
|
||||||
return this.accountService.me(id)
|
return this.accountService.me(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => UserModel, { name: 'createUser' })
|
@Mutation(() => UserModel, { name: 'createUser' })
|
||||||
public async create(@Args('data') input: CreateUserInput) {
|
public async create(@Args('data') input: CreateUserInput) {
|
||||||
return this.accountService.create(input)
|
return this.accountService.create(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -29,7 +33,7 @@ export class AccountResolver {
|
|||||||
@Args('data') input: ChangeEmailInput,
|
@Args('data') input: ChangeEmailInput,
|
||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
) {
|
) {
|
||||||
return this.accountService.changeEmail(user, input)
|
return this.accountService.changeEmail(user, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -38,6 +42,6 @@ export class AccountResolver {
|
|||||||
@Args('data') input: ChangePasswordInput,
|
@Args('data') input: ChangePasswordInput,
|
||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
) {
|
) {
|
||||||
return this.accountService.changePassword(user, input)
|
return this.accountService.changePassword(user, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { User } from '@/prisma/generated'
|
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { hash, verify } from 'argon2';
|
||||||
import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input'
|
|
||||||
import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input'
|
import { User } from '@/prisma/generated';
|
||||||
import { CreateUserInput } from '@/src/module/auth/account/inputs/create-user.input'
|
import { PrismaService } from '@/src/core/prisma/prisma.service';
|
||||||
import { VerificationService } from '@/src/module/auth/verification/verification.service'
|
import { ChangeEmailInput } from '@/src/module/auth/account/inputs/change-email.input';
|
||||||
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'
|
import { ChangePasswordInput } from '@/src/module/auth/account/inputs/change-password.input';
|
||||||
import { hash, verify } from 'argon2'
|
import { CreateUserInput } from '@/src/module/auth/account/inputs/create-user.input';
|
||||||
|
import { VerificationService } from '@/src/module/auth/verification/verification.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountService {
|
export class AccountService {
|
||||||
@ -20,29 +21,29 @@ export class AccountService {
|
|||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(input: CreateUserInput) {
|
public async create(input: CreateUserInput) {
|
||||||
const { email, name, password } = input
|
const { email, name, password } = input;
|
||||||
const isUserNameExists = await this.prismaService.user.findUnique({
|
const isUserNameExists = await this.prismaService.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (isUserNameExists) {
|
if (isUserNameExists) {
|
||||||
throw new ConflictException('Пользователь с таким именем уже существует')
|
throw new ConflictException('Пользователь с таким именем уже существует');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUserEmailExists = await this.prismaService.user.findUnique({
|
const isUserEmailExists = await this.prismaService.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (isUserEmailExists) {
|
if (isUserEmailExists) {
|
||||||
throw new ConflictException('Пользователь с такоей почтой уже существует')
|
throw new ConflictException('Пользователь с такоей почтой уже существует');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.prismaService.user.create({
|
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) {
|
public async changeEmail(user: User, input: ChangeEmailInput) {
|
||||||
const { email } = input
|
const { email } = input;
|
||||||
|
|
||||||
return this.prismaService.user.update({
|
return this.prismaService.user.update({
|
||||||
where: {
|
where: {
|
||||||
@ -74,15 +75,15 @@ export class AccountService {
|
|||||||
data: {
|
data: {
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async changePassword(user: User, input: ChangePasswordInput) {
|
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) {
|
if (!isCorrectPassword) {
|
||||||
throw new BadRequestException('Неверный пароль')
|
throw new BadRequestException('Неверный пароль');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prismaService.user.update({
|
return this.prismaService.user.update({
|
||||||
@ -92,6 +93,6 @@ export class AccountService {
|
|||||||
data: {
|
data: {
|
||||||
password: await hash(newPassword),
|
password: await hash(newPassword),
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator'
|
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class ChangeEmailInput {
|
export class ChangeEmailInput {
|
||||||
@ -7,5 +7,5 @@ export class ChangeEmailInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string
|
email: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator'
|
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class ChangePasswordInput {
|
export class ChangePasswordInput {
|
||||||
@ -7,11 +7,11 @@ export class ChangePasswordInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
oldPassword: string
|
oldPassword: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
newPassword: string
|
newPassword: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsEmail, IsNotEmpty, IsString, Matches, MinLength } from 'class-validator'
|
import {
|
||||||
|
IsEmail, IsNotEmpty, IsString, Matches, MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class CreateUserInput {
|
export class CreateUserInput {
|
||||||
@ -7,17 +9,17 @@ export class CreateUserInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Matches(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/)
|
@Matches(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/)
|
||||||
name: string
|
name: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string
|
email: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
password: string
|
password: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
@ObjectType()
|
||||||
export class AuthModel {
|
export class AuthModel {
|
||||||
@Field(() => UserModel, { nullable: true })
|
@Field(() => UserModel, { nullable: true })
|
||||||
public user?: UserModel
|
public user?: UserModel;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
public message: string
|
public message: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,76 +1,77 @@
|
|||||||
import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model'
|
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||||
import { FollowModel } from '@/src/module/follow/model/follow.model'
|
|
||||||
import { NotificationSettingsModel } from '@/src/module/notification/models/notification-settings.model'
|
import { FollowModel } from '@/src/module/follow';
|
||||||
import { NotificationModel } from '@/src/module/notification/models/notification.model'
|
import { NotificationModel, NotificationSettingsModel } from '@/src/module/notification';
|
||||||
import { StreamModel } from '@/src/module/stream/models/stream.model'
|
import { StreamModel } from '@/src/module/stream/models/stream.model';
|
||||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
import { User } from '@prisma/generated';
|
||||||
import { User } from '@prisma/generated'
|
|
||||||
|
import { SocialLinkModel } from '../../profile/models/social-link.model';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UserModel implements User {
|
export class UserModel implements User {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
id: string
|
id: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
email: string
|
email: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
password: string
|
password: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
name: string
|
name: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
displayName: string
|
displayName: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
avatar: string
|
avatar: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
bio: string
|
bio: string;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
isEmailVerified: boolean
|
isEmailVerified: boolean;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
isVerified: boolean
|
isVerified: boolean;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
isTotpEnabled: boolean
|
isTotpEnabled: boolean;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
isDeactivated: boolean
|
isDeactivated: boolean;
|
||||||
|
|
||||||
@Field(() => Date, { nullable: true })
|
@Field(() => Date, { nullable: true })
|
||||||
deactivatedAt: Date
|
deactivatedAt: Date;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
totpSecret: string
|
totpSecret: string;
|
||||||
|
|
||||||
@Field(() => [SocialLinkModel])
|
@Field(() => [SocialLinkModel])
|
||||||
socialLink: SocialLinkModel[]
|
socialLink: SocialLinkModel[];
|
||||||
|
|
||||||
@Field(() => StreamModel)
|
@Field(() => StreamModel)
|
||||||
stream: StreamModel
|
stream: StreamModel;
|
||||||
|
|
||||||
@Field(() => [FollowModel])
|
@Field(() => [FollowModel])
|
||||||
followers: FollowModel[]
|
followers: FollowModel[];
|
||||||
|
|
||||||
@Field(() => [FollowModel])
|
@Field(() => [FollowModel])
|
||||||
followings: FollowModel[]
|
followings: FollowModel[];
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
telegramId: string
|
telegramId: string;
|
||||||
|
|
||||||
@Field(() => [NotificationModel])
|
@Field(() => [NotificationModel])
|
||||||
notification: NotificationModel[]
|
notification: NotificationModel[];
|
||||||
|
|
||||||
@Field(() => NotificationSettingsModel)
|
@Field(() => NotificationSettingsModel)
|
||||||
notificationSettings: NotificationSettingsModel
|
notificationSettings: NotificationSettingsModel;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
createdAt: Date
|
createdAt: Date;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
updatedAt: Date
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DeactivateService } from './deactivate.service';
|
|
||||||
import { DeactivateResolver } from './deactivate.resolver';
|
import { DeactivateResolver } from './deactivate.resolver';
|
||||||
|
import { DeactivateService } from './deactivate.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [DeactivateResolver, DeactivateService],
|
providers: [DeactivateResolver, DeactivateService],
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import { AuthModel } from '@/src/module/auth/account/models/auth.model'
|
import {
|
||||||
import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input'
|
Args, Context, Mutation, Resolver,
|
||||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
} from '@nestjs/graphql';
|
||||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
|
||||||
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'
|
import { AuthModel } from '@/src/module/auth/account/models/auth.model';
|
||||||
import { GqlContext } from '@/src/shared/types/gql-context.types'
|
import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input';
|
||||||
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql'
|
import { Authorization } from '@/src/shared/decorators/auth.decorator';
|
||||||
import { User } from '@prisma/generated'
|
import { Authorized } from '@/src/shared/decorators/authorized.decorator';
|
||||||
import { DeactivateService } from './deactivate.service'
|
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')
|
@Resolver('Deactivate')
|
||||||
export class DeactivateResolver {
|
export class DeactivateResolver {
|
||||||
@ -20,6 +24,6 @@ export class DeactivateResolver {
|
|||||||
@UserAgent() userAgent: string,
|
@UserAgent() userAgent: string,
|
||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
) {
|
) {
|
||||||
return this.deactivateService.deactivate(req, input, user, userAgent)
|
return this.deactivateService.deactivate(req, input, user, userAgent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input'
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { MailService } from '@/src/module/libs/mail/mail.service'
|
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 { TelegramService } from '@/src/module/libs/telegram/telegram.service';
|
||||||
import { generateToken } from '@/src/shared/util/generate-token.util'
|
import { generateToken } from '@/src/shared/util/generate-token.util';
|
||||||
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
|
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util';
|
||||||
import { destroySession } from '@/src/shared/util/session.util'
|
import { destroySession } from '@/src/shared/util/session.util';
|
||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
import { TokenType, User } from '@prisma/generated';
|
||||||
import { ConfigService } from '@nestjs/config'
|
|
||||||
import { TokenType, User } from '@prisma/generated'
|
import { ProcessEnv } from '../../../shared/types/env';
|
||||||
import { verify } from 'argon2'
|
|
||||||
import { Request } from 'express'
|
import type { Request } from 'express';
|
||||||
import { ProcessEnv } from '../../../shared/types/env'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeactivateService {
|
export class DeactivateService {
|
||||||
@ -18,66 +21,32 @@ export class DeactivateService {
|
|||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly configService: ConfigService<ProcessEnv>,
|
private readonly configService: ConfigService<ProcessEnv>,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
private readonly telegramService: TelegramService
|
private readonly telegramService: TelegramService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deactivate(req: Request, input: DeactivateAccountInput, user: User, userAgent: string) {
|
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) {
|
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) {
|
if (!isValidPassword) {
|
||||||
throw new BadRequestException('Неверный пароль')
|
throw new BadRequestException('Неверный пароль');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pin) {
|
if (!pin) {
|
||||||
await this.sendDeactivationToken(req, user, userAgent)
|
await this.sendDeactivationToken(req, user, userAgent);
|
||||||
return { message: 'Требуется ввести код подтверждения' }
|
|
||||||
|
return { message: 'Требуется ввести код подтверждения' };
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.validateDeactivateToken(req, pin)
|
await this.validateDeactivateToken(req, pin);
|
||||||
|
|
||||||
return { user }
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendDeactivationToken(req: Request, user: User, userAgent: string) {
|
public async sendDeactivationToken(req: Request, user: User, userAgent: string) {
|
||||||
@ -86,14 +55,48 @@ export class DeactivateService {
|
|||||||
user,
|
user,
|
||||||
TokenType.DEACTIVATE_ACCOUNT,
|
TokenType.DEACTIVATE_ACCOUNT,
|
||||||
false,
|
false,
|
||||||
)
|
);
|
||||||
|
|
||||||
const metadata = getSessionMetadata(req, userAgent)
|
const metadata = getSessionMetadata(req, userAgent);
|
||||||
await this.mailService.sendDeactivateToken(user.email, deactivateToken.token, metadata)
|
await this.mailService.sendDeactivateToken(user.email, deactivateToken.token, metadata);
|
||||||
|
|
||||||
if (deactivateToken.user?.notificationSettings?.telegramNotifications && deactivateToken.user?.telegramId) {
|
if (deactivateToken.user?.notificationSettings?.telegramNotifications && deactivateToken.user.telegramId) {
|
||||||
await this.telegramService.sendDeactivateToken(deactivateToken.user.telegramId, deactivateToken.token, metadata)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsEmail, IsNotEmpty, IsOptional, IsString, Length, MinLength } from 'class-validator'
|
import {
|
||||||
|
IsEmail, IsNotEmpty, IsOptional, IsString, Length, MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class DeactivateAccountInput {
|
export class DeactivateAccountInput {
|
||||||
@ -7,18 +9,18 @@ export class DeactivateAccountInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string
|
email: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
password: string
|
password: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Length(6, 6)
|
@Length(6, 6)
|
||||||
pin: string
|
pin: string;
|
||||||
}
|
}
|
||||||
|
|||||||
7
backend/src/module/auth/index.ts
Normal file
7
backend/src/module/auth/index.ts
Normal file
@ -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';
|
||||||
@ -1,8 +1,11 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
import {
|
||||||
|
IsNotEmpty, IsString, IsUUID, MinLength, Validate,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IsPasswordMatchingConstraintDecorator,
|
IsPasswordMatchingConstraintDecorator,
|
||||||
} from '@/src/shared/decorators/is-password-matching-constraint.decorator'
|
} from '@/src/shared/decorators/is-password-matching-constraint.decorator';
|
||||||
import { Field, InputType } from '@nestjs/graphql'
|
|
||||||
import { IsNotEmpty, IsString, IsUUID, MinLength, Validate } from 'class-validator'
|
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class NewPasswordInput {
|
export class NewPasswordInput {
|
||||||
@ -10,17 +13,17 @@ export class NewPasswordInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
password: string
|
password: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
@Validate(IsPasswordMatchingConstraintDecorator)
|
@Validate(IsPasswordMatchingConstraintDecorator)
|
||||||
passwordRepeat: string
|
passwordRepeat: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
token: string
|
token: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,8 @@ import { IsEmail, IsNotEmpty } from 'class-validator';
|
|||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class ResetPasswordInput {
|
export class ResetPasswordInput {
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string
|
email: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
import { PasswordRecoveryService } from './password-recovery.service'
|
|
||||||
import { PasswordRecoveryResolver } from './password-recovery.resolver'
|
import { PasswordRecoveryResolver } from './password-recovery.resolver';
|
||||||
|
import { PasswordRecoveryService } from './password-recovery.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PasswordRecoveryResolver, PasswordRecoveryService],
|
providers: [PasswordRecoveryResolver, PasswordRecoveryService],
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'
|
import {
|
||||||
import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input'
|
Args, Context, Mutation, Resolver,
|
||||||
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'
|
} from '@nestjs/graphql';
|
||||||
import { GqlContext } from '@/src/shared/types/gql-context.types'
|
|
||||||
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql'
|
import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input';
|
||||||
import { PasswordRecoveryService } from './password-recovery.service'
|
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')
|
@Resolver('PasswordRecovery')
|
||||||
export class PasswordRecoveryResolver {
|
export class PasswordRecoveryResolver {
|
||||||
@ -15,11 +19,11 @@ export class PasswordRecoveryResolver {
|
|||||||
@Args('data') input: ResetPasswordInput,
|
@Args('data') input: ResetPasswordInput,
|
||||||
@UserAgent() userAgent: string,
|
@UserAgent() userAgent: string,
|
||||||
) {
|
) {
|
||||||
return this.passwordRecoveryService.resetPassword(req, input, userAgent)
|
return this.passwordRecoveryService.resetPassword(req, input, userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, { name: 'setNewPassword' })
|
@Mutation(() => Boolean, { name: 'setNewPassword' })
|
||||||
public async setNewPassword(@Args('data') input: NewPasswordInput) {
|
public async setNewPassword(@Args('data') input: NewPasswordInput) {
|
||||||
return this.passwordRecoveryService.setNewPassword(input)
|
return this.passwordRecoveryService.setNewPassword(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,59 +1,61 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'
|
import { hash } from 'argon2';
|
||||||
import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input'
|
|
||||||
import { MailService } from '@/src/module/libs/mail/mail.service'
|
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 { TelegramService } from '@/src/module/libs/telegram/telegram.service';
|
||||||
import { generateToken } from '@/src/shared/util/generate-token.util'
|
import { generateToken } from '@/src/shared/util/generate-token.util';
|
||||||
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
|
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util';
|
||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
import { TokenType } from '@prisma/generated';
|
||||||
import { TokenType } from '@prisma/generated'
|
|
||||||
import { hash } from 'argon2'
|
import type { Request } from 'express';
|
||||||
import { Request } from 'express'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PasswordRecoveryService {
|
export class PasswordRecoveryService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
private readonly telegramService: TelegramService
|
private readonly telegramService: TelegramService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resetPassword(req: Request, input: ResetPasswordInput, userAgent: string) {
|
public async resetPassword(req: Request, input: ResetPasswordInput, userAgent: string) {
|
||||||
const { email } = input
|
const { email } = input;
|
||||||
const user = await this.prismaService.user.findFirst({
|
const user = await this.prismaService.user.findFirst({
|
||||||
where: { email },
|
where: { email },
|
||||||
include: { notificationSettings: true },
|
include: { notificationSettings: true },
|
||||||
})
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('Пользователь с такой почтой не найден')
|
throw new NotFoundException('Пользователь с такой почтой не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetToken = await generateToken(this.prismaService, user, TokenType.PASSWORD_RESET)
|
const resetToken = await generateToken(this.prismaService, user, TokenType.PASSWORD_RESET);
|
||||||
const metadata = getSessionMetadata(req, userAgent)
|
const metadata = getSessionMetadata(req, userAgent);
|
||||||
await this.mailService.sendPasswordResetToken(user.email, resetToken.token, metadata)
|
await this.mailService.sendPasswordResetToken(user.email, resetToken.token, metadata);
|
||||||
|
|
||||||
if (resetToken.user?.notificationSettings?.telegramNotifications && resetToken?.user.telegramId) {
|
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) {
|
public async setNewPassword(input: NewPasswordInput) {
|
||||||
const { password, token } = input
|
const { password, token } = input;
|
||||||
|
|
||||||
const existingToken = await this.prismaService.token.findUnique({
|
const existingToken = await this.prismaService.token.findUnique({
|
||||||
where: { token, type: TokenType.PASSWORD_RESET },
|
where: { token, type: TokenType.PASSWORD_RESET },
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!existingToken) {
|
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) {
|
if (hasExpired) {
|
||||||
throw new BadRequestException('Токен истек')
|
throw new BadRequestException('Токен истек');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaService.user.update({
|
await this.prismaService.user.update({
|
||||||
@ -63,15 +65,15 @@ export class PasswordRecoveryService {
|
|||||||
data: {
|
data: {
|
||||||
password: await hash(password),
|
password: await hash(password),
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
await this.prismaService.token.delete({
|
await this.prismaService.token.delete({
|
||||||
where: {
|
where: {
|
||||||
id: existingToken.id,
|
id: existingToken.id,
|
||||||
type: TokenType.PASSWORD_RESET,
|
type: TokenType.PASSWORD_RESET,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsString, Matches, MaxLength } from 'class-validator'
|
import {
|
||||||
|
IsNotEmpty, IsString, Matches, MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class ChangeProfileInfoInput {
|
export class ChangeProfileInfoInput {
|
||||||
@ -7,16 +9,16 @@ export class ChangeProfileInfoInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Matches(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/)
|
@Matches(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/)
|
||||||
name: string
|
name: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
displayName: string
|
displayName: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(300)
|
@MaxLength(300)
|
||||||
bio: string
|
bio: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
|
||||||
import { SocialLink } from '@prisma/generated'
|
import { UserModel } from '@/src/module/auth/account/models/user.model';
|
||||||
|
import { SocialLink } from '@prisma/generated';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class SocialLinkModel implements SocialLink {
|
export class SocialLinkModel implements SocialLink {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
id: string
|
id: string;
|
||||||
|
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
userId: UserModel['id']
|
userId: UserModel['id'];
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
title: string
|
title: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
url: string
|
url: string;
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
position: number
|
position: number;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
updatedAt: Date
|
updatedAt: Date;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
createdAt: Date
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsNumber, IsString, IsUrl } from 'class-validator'
|
import {
|
||||||
|
IsNotEmpty, IsNumber, IsString, IsUrl,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class SocialLinkInput {
|
export class SocialLinkInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
title: string
|
title: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsUrl()
|
@IsUrl()
|
||||||
url: string
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@ -20,10 +22,10 @@ export class SocialLinkOrderInput {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
id: string
|
id: string;
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
position: number
|
position: number;
|
||||||
}
|
}
|
||||||
|
|||||||
29
backend/src/module/auth/profile/models/social-link.model.ts
Normal file
29
backend/src/module/auth/profile/models/social-link.model.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
import { ProfileService } from './profile.service'
|
|
||||||
import { ProfileResolver } from './profile.resolver'
|
import { ProfileResolver } from './profile.resolver';
|
||||||
|
import { ProfileService } from './profile.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [ProfileResolver, ProfileService],
|
providers: [ProfileResolver, ProfileService],
|
||||||
|
|||||||
@ -1,18 +1,22 @@
|
|||||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
import {
|
||||||
import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'
|
Args, Mutation, Query, Resolver,
|
||||||
import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model'
|
} 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 {
|
import {
|
||||||
SocialLinkInput,
|
SocialLinkInput,
|
||||||
SocialLinkOrderInput,
|
SocialLinkOrderInput,
|
||||||
} from '@/src/module/auth/profile/inputs/social-link.input'
|
} from '@/src/module/auth/profile/inputs/social-link.input';
|
||||||
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 { Authorized } from '@/src/shared/decorators/authorized.decorator';
|
||||||
import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe'
|
import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
|
|
||||||
import { ProfileService } from './profile.service'
|
import { SocialLinkModel } from './models/social-link.model';
|
||||||
import { User } from '@/prisma/generated'
|
import { ProfileService } from './profile.service';
|
||||||
import * as Upload from 'graphql-upload/Upload.js'
|
|
||||||
import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
|
||||||
|
|
||||||
@Resolver('Profile')
|
@Resolver('Profile')
|
||||||
export class ProfileResolver {
|
export class ProfileResolver {
|
||||||
@ -24,13 +28,13 @@ export class ProfileResolver {
|
|||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
@Args('avatar', { type: () => GraphQLUpload }, FileValidationPipe) file: Upload,
|
@Args('avatar', { type: () => GraphQLUpload }, FileValidationPipe) file: Upload,
|
||||||
) {
|
) {
|
||||||
return this.profileService.changeAvatar(user, file)
|
return this.profileService.changeAvatar(user, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Mutation(() => Boolean, { name: 'removeProfileAvatar' })
|
@Mutation(() => Boolean, { name: 'removeProfileAvatar' })
|
||||||
public async removeAvatar(@Authorized() user: User) {
|
public async removeAvatar(@Authorized() user: User) {
|
||||||
return this.profileService.removeAvatar(user)
|
return this.profileService.removeAvatar(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -39,7 +43,7 @@ export class ProfileResolver {
|
|||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
@Args('data') input: ChangeProfileInfoInput,
|
@Args('data') input: ChangeProfileInfoInput,
|
||||||
) {
|
) {
|
||||||
return this.profileService.changeInfo(user, input)
|
return this.profileService.changeInfo(user, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -48,7 +52,7 @@ export class ProfileResolver {
|
|||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
@Args('data') input: SocialLinkInput,
|
@Args('data') input: SocialLinkInput,
|
||||||
) {
|
) {
|
||||||
return this.profileService.createSocialLink(user, input)
|
return this.profileService.createSocialLink(user, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -56,7 +60,7 @@ export class ProfileResolver {
|
|||||||
public async reorderSocialLinks(
|
public async reorderSocialLinks(
|
||||||
@Args('list', { type: () => [SocialLinkOrderInput] }) list: SocialLinkOrderInput[],
|
@Args('list', { type: () => [SocialLinkOrderInput] }) list: SocialLinkOrderInput[],
|
||||||
) {
|
) {
|
||||||
return this.profileService.reorderSocialLinks(list)
|
return this.profileService.reorderSocialLinks(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -65,7 +69,7 @@ export class ProfileResolver {
|
|||||||
@Args('id') id: string,
|
@Args('id') id: string,
|
||||||
@Args('data') input: SocialLinkInput,
|
@Args('data') input: SocialLinkInput,
|
||||||
) {
|
) {
|
||||||
return this.profileService.updateSocialLink(id, input)
|
return this.profileService.updateSocialLink(id, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -73,12 +77,12 @@ export class ProfileResolver {
|
|||||||
public async removeSocialLink(
|
public async removeSocialLink(
|
||||||
@Args('id') id: string,
|
@Args('id') id: string,
|
||||||
) {
|
) {
|
||||||
return this.profileService.removeSocialLink(id)
|
return this.profileService.removeSocialLink(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Query(() => [SocialLinkModel], { name: 'findSocialLinks' })
|
@Query(() => [SocialLinkModel], { name: 'findSocialLinks' })
|
||||||
public async findSocialLink(@Authorized() user: User) {
|
public async findSocialLink(@Authorized() user: User) {
|
||||||
return this.profileService.findSocialLink(user)
|
return this.profileService.findSocialLink(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { ConflictException, Injectable } from '@nestjs/common';
|
||||||
import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'
|
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 {
|
import {
|
||||||
SocialLinkInput,
|
SocialLinkInput,
|
||||||
SocialLinkOrderInput,
|
SocialLinkOrderInput,
|
||||||
} from '@/src/module/auth/profile/inputs/social-link.input'
|
} 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 { StorageService } from '../../libs/storage/storage.service'
|
|
||||||
import { SocialLink, User } from '@/prisma/generated'
|
|
||||||
import * as Upload from 'graphql-upload/Upload.js'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProfileService {
|
export class ProfileService {
|
||||||
@ -20,70 +22,71 @@ export class ProfileService {
|
|||||||
|
|
||||||
public async changeAvatar(user: User, file: Upload) {
|
public async changeAvatar(user: User, file: Upload) {
|
||||||
if (user.avatar) {
|
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()) {
|
for await (const chunk of file.createReadStream()) {
|
||||||
chunks.push(chunk)
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = Buffer.concat(chunks)
|
const buffer = Buffer.concat(chunks);
|
||||||
const fileName = `/channels/${user.name}.webp`
|
const fileName = `/channels/${user.name}.webp`;
|
||||||
|
|
||||||
const processedBuffer = await sharp(buffer, { animated: file.filename.endsWith('.gif') })
|
const processedBuffer = await sharp(buffer, { animated: file.filename.endsWith('.gif') })
|
||||||
.resize(512, 512)
|
.resize(512, 512)
|
||||||
.webp()
|
.webp()
|
||||||
.toBuffer()
|
.toBuffer();
|
||||||
|
|
||||||
await this.storageService.upload(processedBuffer, fileName, 'image/webp')
|
await this.storageService.upload(processedBuffer, fileName, 'image/webp');
|
||||||
|
|
||||||
await this.prismaService.user.update({
|
await this.prismaService.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { avatar: fileName },
|
data: { avatar: fileName },
|
||||||
})
|
});
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeAvatar(user: User) {
|
public async removeAvatar(user: User) {
|
||||||
if (!user.avatar) {
|
if (!user.avatar) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storageService.remove(user.avatar)
|
await this.storageService.remove(user.avatar);
|
||||||
|
|
||||||
await this.prismaService.user.update({
|
await this.prismaService.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { avatar: null },
|
data: { avatar: null },
|
||||||
})
|
});
|
||||||
return true
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async changeInfo(user: User, input: ChangeProfileInfoInput) {
|
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({
|
const existingUser = await this.prismaService.user.findUnique({
|
||||||
where: { name },
|
where: { name },
|
||||||
})
|
});
|
||||||
if (existingUser && user.name !== name) {
|
if (existingUser && user.name !== name) {
|
||||||
throw new ConflictException('Пользователь с таким именем уже существует')
|
throw new ConflictException('Пользователь с таким именем уже существует');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prismaService.user.update({
|
return this.prismaService.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { bio, displayName, name },
|
data: { bio, displayName, name },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createSocialLink(user: User, input: SocialLinkInput) {
|
public async createSocialLink(user: User, input: SocialLinkInput) {
|
||||||
const { title, url } = input
|
const { title, url } = input;
|
||||||
|
|
||||||
const lastSocialLink = await this.prismaService.socialLink.findFirst({
|
const lastSocialLink = await this.prismaService.socialLink.findFirst({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
orderBy: { position: 'desc' },
|
orderBy: { position: 'desc' },
|
||||||
})
|
});
|
||||||
|
|
||||||
return this.prismaService.socialLink.create({
|
return this.prismaService.socialLink.create({
|
||||||
data: {
|
data: {
|
||||||
@ -96,45 +99,44 @@ export class ProfileService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findSocialLink(user: User) {
|
public async findSocialLink(user: User) {
|
||||||
return this.prismaService.socialLink.findMany({
|
return this.prismaService.socialLink.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reorderSocialLinks(list: SocialLinkOrderInput[]) {
|
public async reorderSocialLinks(list: SocialLinkOrderInput[]) {
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePromises = list.map((socialLink) => {
|
const updatePromises = list.map(async (socialLink) => this.prismaService.socialLink.update({
|
||||||
return this.prismaService.socialLink.update({
|
where: { id: socialLink.id },
|
||||||
where: { id: socialLink.id },
|
data: { position: Number(socialLink.position) },
|
||||||
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) {
|
public async updateSocialLink(id: SocialLink['id'], input: SocialLinkInput) {
|
||||||
const { title, url } = input
|
const { title, url } = input;
|
||||||
|
|
||||||
return this.prismaService.socialLink.update({
|
return this.prismaService.socialLink.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { title, url },
|
data: { title, url },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeSocialLink(id: SocialLink['id']) {
|
public async removeSocialLink(id: SocialLink['id']) {
|
||||||
await this.prismaService.socialLink.delete({
|
await this.prismaService.socialLink.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
})
|
});
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,25 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsOptional, IsString, Length, MinLength } from 'class-validator'
|
import {
|
||||||
|
IsNotEmpty, IsOptional, IsString, Length, MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class LoginInput {
|
export class LoginInput {
|
||||||
@Field()
|
@Field()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
login: string
|
login: string;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
password: string
|
password: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Length(6, 6)
|
@Length(6, 6)
|
||||||
pin?: string
|
pin?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
@ObjectType()
|
||||||
export class LocationModel implements LocationInfo {
|
export class LocationModel implements LocationInfo {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
country: string
|
country: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
city: string
|
city: string;
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
latitude: number
|
latitude: number;
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
longitude: number
|
longitude: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class DeviceModel implements DeviceInfo {
|
export class DeviceModel implements DeviceInfo {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
browser: string
|
browser: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
os: string
|
os: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
type: string
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class SessionMetadataModel implements SessionInfo {
|
export class SessionMetadataModel implements SessionInfo {
|
||||||
@Field(() => LocationModel)
|
@Field(() => LocationModel)
|
||||||
location: LocationModel
|
location: LocationModel;
|
||||||
|
|
||||||
@Field(() => DeviceModel)
|
@Field(() => DeviceModel)
|
||||||
device: DeviceModel
|
device: DeviceModel;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
ip: string
|
ip: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class SessionModel {
|
export class SessionModel {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
id: string
|
id: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
userId: string
|
userId: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
createdAt: string
|
createdAt: string;
|
||||||
|
|
||||||
@Field(() => SessionMetadataModel)
|
@Field(() => SessionMetadataModel)
|
||||||
metadata: SessionMetadataModel
|
metadata: SessionMetadataModel;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { VerificationService } from '@/src/module/auth/verification/verification.service';
|
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({
|
@Module({
|
||||||
providers: [SessionResolver, SessionService, VerificationService],
|
providers: [SessionResolver, SessionService, VerificationService],
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
|
import {
|
||||||
|
Args, Context, Mutation, Query, Resolver,
|
||||||
|
} from '@nestjs/graphql';
|
||||||
|
|
||||||
import { AuthModel } from '@/src/module/auth/account/models/auth.model';
|
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 { LoginInput } from '@/src/module/auth/session/inputs/login.input'
|
import { Authorization } from '@/src/shared/decorators/auth.decorator';
|
||||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator';
|
||||||
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'
|
import { GqlContext } from '@/src/shared/types/gql-context.types';
|
||||||
import { GqlContext } from '@/src/shared/types/gql-context.types'
|
|
||||||
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql'
|
import { SessionModel } from './models/session.model';
|
||||||
import { SessionService } from './session.service'
|
import { SessionService } from './session.service';
|
||||||
import { SessionModel } from './models/session.model'
|
|
||||||
|
|
||||||
@Resolver('Session')
|
@Resolver('Session')
|
||||||
export class SessionResolver {
|
export class SessionResolver {
|
||||||
@ -17,7 +20,7 @@ export class SessionResolver {
|
|||||||
public async findByUser(
|
public async findByUser(
|
||||||
@Context() { req }: GqlContext,
|
@Context() { req }: GqlContext,
|
||||||
) {
|
) {
|
||||||
return this.sessionService.findByUser(req)
|
return this.sessionService.findByUser(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -25,7 +28,7 @@ export class SessionResolver {
|
|||||||
public async findCurrent(
|
public async findCurrent(
|
||||||
@Context() { req }: GqlContext,
|
@Context() { req }: GqlContext,
|
||||||
) {
|
) {
|
||||||
return this.sessionService.findCurrentSession(req)
|
return this.sessionService.findCurrentSession(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => AuthModel, { name: 'loginUser' })
|
@Mutation(() => AuthModel, { name: 'loginUser' })
|
||||||
@ -34,26 +37,26 @@ export class SessionResolver {
|
|||||||
@Args('data') input: LoginInput,
|
@Args('data') input: LoginInput,
|
||||||
@UserAgent() userAgent: string,
|
@UserAgent() userAgent: string,
|
||||||
) {
|
) {
|
||||||
return this.sessionService.login(req, input, userAgent)
|
return this.sessionService.login(req, input, userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Mutation(() => Boolean, { name: 'logoutUser' })
|
@Mutation(() => Boolean, { name: 'logoutUser' })
|
||||||
public async logout(@Context() { req }: GqlContext) {
|
public async logout(@Context() { req }: GqlContext) {
|
||||||
return this.sessionService.logout(req)
|
return this.sessionService.logout(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, { name: 'clearSessionCookie' })
|
@Mutation(() => Boolean, { name: 'clearSessionCookie' })
|
||||||
public clearSession(@Context() { req }: GqlContext) {
|
public clearSession(@Context() { req }: GqlContext) {
|
||||||
return this.sessionService.clearSession(req)
|
return this.sessionService.clearSession(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Mutation(() => Boolean, { name: 'removeSession' })
|
@Mutation(() => Boolean, { name: 'removeSession' })
|
||||||
public remove(
|
public async remove(
|
||||||
@Context() { req }: GqlContext,
|
@Context() { req }: GqlContext,
|
||||||
@Args('id') id: string,
|
@Args('id') id: string,
|
||||||
) {
|
) {
|
||||||
return this.sessionService.remove(req, id)
|
return this.sessionService.remove(req, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { verify } from 'argon2'
|
import { verify } from 'argon2';
|
||||||
import { Request } from 'express'
|
import { SessionData } from 'express-session';
|
||||||
import { SessionData } from 'express-session'
|
import { TOTP } from 'otpauth';
|
||||||
import { TOTP } from 'otpauth'
|
|
||||||
import { ProcessEnv } from '../../../shared/types/env'
|
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()
|
@Injectable()
|
||||||
export class SessionService {
|
export class SessionService {
|
||||||
@ -28,82 +31,84 @@ export class SessionService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async findByUser(req: Request) {
|
public async findByUser(req: Request) {
|
||||||
const userId = req.user?.id
|
const userId = req.user?.id;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new NotFoundException('Пользователь не найден')
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = await this.redisService.keys('*')
|
const keys = await this.redisService.keys('*');
|
||||||
const userSessions: Request['session'][] = []
|
const userSessions: Request['session'][] = [];
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const sessionData = await this.redisService.get(key)
|
const sessionData = await this.redisService.get(key);
|
||||||
if (sessionData) {
|
if (sessionData) {
|
||||||
const session = JSON.parse(sessionData) as Request['session']
|
const session = JSON.parse(sessionData) as Request['session'];
|
||||||
|
|
||||||
if (session.userId === userId) {
|
if (session.userId === userId) {
|
||||||
userSessions.push({
|
userSessions.push({
|
||||||
...session,
|
...session,
|
||||||
id: key.split(':')[1],
|
id: key.split(':')[1],
|
||||||
} as Request['session'])
|
} as Request['session']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- todo fixme
|
|
||||||
// @ts-expect-error
|
// @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) {
|
public async findCurrentSession(req: Request) {
|
||||||
const sessionId = req.session.id
|
const sessionId = req.session.id;
|
||||||
const key = `${this.configService.getOrThrow<string>('SESSION_FOLDER')}${sessionId}`
|
const key = `${this.configService.getOrThrow<string>('SESSION_FOLDER')}${sessionId}`;
|
||||||
const sessionData = await this.redisService.get(key)
|
const sessionData = await this.redisService.get(key);
|
||||||
|
|
||||||
if (!sessionData) {
|
if (!sessionData) {
|
||||||
throw new NotFoundException('Сессия не найдена')
|
throw new NotFoundException('Сессия не найдена');
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = JSON.parse(sessionData) as SessionData
|
const session = JSON.parse(sessionData) as SessionData;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(req: Request, input: LoginInput, userAgent: string) {
|
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: {
|
const user = await this.prismaService.user.findFirst({
|
||||||
OR: [
|
where: {
|
||||||
{ name: { equals: login } },
|
OR: [
|
||||||
{ email: { equals: login } },
|
{ name: { equals: login } },
|
||||||
],
|
{ email: { equals: login } },
|
||||||
} })
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException(`Пользователь не найден`)
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidPassword = await verify(user.password, password)
|
const isValidPassword = await verify(user.password, password);
|
||||||
if (!isValidPassword) {
|
if (!isValidPassword) {
|
||||||
throw new UnauthorizedException('Логин или пароль неверный')
|
throw new UnauthorizedException('Логин или пароль неверный');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.isEmailVerified) {
|
if (!user.isEmailVerified) {
|
||||||
await this.verificationService.sendVerificationToken(user)
|
await this.verificationService.sendVerificationToken(user);
|
||||||
throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения')
|
|
||||||
|
throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isTotpEnabled) {
|
if (user.isTotpEnabled) {
|
||||||
if (!pin) {
|
if (!pin) {
|
||||||
return {
|
return {
|
||||||
message: 'Необходимо ввести пин-код для завершения операции',
|
message: 'Необходимо ввести пин-код для завершения операции',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const totp = new TOTP({
|
const totp = new TOTP({
|
||||||
@ -112,36 +117,36 @@ export class SessionService {
|
|||||||
algorithm: 'SHA-1',
|
algorithm: 'SHA-1',
|
||||||
digits: 6,
|
digits: 6,
|
||||||
secret: user.totpSecret!,
|
secret: user.totpSecret!,
|
||||||
})
|
});
|
||||||
|
|
||||||
const delta = totp.validate({ token: pin })
|
const delta = totp.validate({ token: pin });
|
||||||
|
|
||||||
if (delta === null) {
|
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) {
|
public async logout(req: Request) {
|
||||||
return destroySession(req, this.configService)
|
return destroySession(req, this.configService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearSession(req: Request) {
|
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) {
|
public async remove(req: Request, id: string) {
|
||||||
if (req.session.id === id) {
|
if (req.session.id === id) {
|
||||||
throw new ConflictException('Текущую сессию удалить нельзя')
|
throw new ConflictException('Текущую сессию удалить нельзя');
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = `${this.configService.getOrThrow<string>('SESSION_FOLDER')}${id}`
|
const key = `${this.configService.getOrThrow<string>('SESSION_FOLDER')}${id}`;
|
||||||
await this.redisService.del(key)
|
await this.redisService.del(key);
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsString, Length } from 'class-validator'
|
import { IsNotEmpty, IsString, Length } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class EnableTotpInput {
|
export class EnableTotpInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
secret: string
|
secret: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Length(6, 6)
|
@Length(6, 6)
|
||||||
pin: string
|
pin: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
|||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class TotpModel {
|
export class TotpModel {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
public qrcodeUrl: string
|
public qrcodeUrl: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
public secret: string
|
public secret: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TotpService } from './totp.service';
|
|
||||||
import { TotpResolver } from './totp.resolver';
|
import { TotpResolver } from './totp.resolver';
|
||||||
|
import { TotpService } from './totp.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [TotpResolver, TotpService],
|
providers: [TotpResolver, TotpService],
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input'
|
import {
|
||||||
import { TotpModel } from '@/src/module/auth/totp/models/totp.model'
|
Args, Mutation, Query, Resolver,
|
||||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
} from '@nestjs/graphql';
|
||||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
|
import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input';
|
||||||
import { User } from '@prisma/generated'
|
import { TotpModel } from '@/src/module/auth/totp/models/totp.model';
|
||||||
import { TotpService } from './totp.service'
|
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')
|
@Resolver('Totp')
|
||||||
export class TotpResolver {
|
export class TotpResolver {
|
||||||
@ -13,7 +17,7 @@ export class TotpResolver {
|
|||||||
@Authorization()
|
@Authorization()
|
||||||
@Query(() => TotpModel, { name: 'generateTotpSecret' })
|
@Query(() => TotpModel, { name: 'generateTotpSecret' })
|
||||||
public async generate(@Authorized() user: User) {
|
public async generate(@Authorized() user: User) {
|
||||||
return this.totpService.generate(user)
|
return this.totpService.generate(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -22,12 +26,12 @@ export class TotpResolver {
|
|||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
@Args('data') input: EnableTotpInput,
|
@Args('data') input: EnableTotpInput,
|
||||||
) {
|
) {
|
||||||
return this.totpService.enable(user, input)
|
return this.totpService.enable(user, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Mutation(() => Boolean, { name: 'disableTotp' })
|
@Mutation(() => Boolean, { name: 'disableTotp' })
|
||||||
public async disable(@Authorized() user: User) {
|
public async disable(@Authorized() user: User) {
|
||||||
return this.totpService.disable(user)
|
return this.totpService.disable(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { randomBytes } from 'node:crypto';
|
||||||
import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input'
|
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common'
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { User } from '@prisma/generated'
|
import { encode } from 'hi-base32';
|
||||||
import { encode } from 'hi-base32'
|
import { TOTP } from 'otpauth';
|
||||||
import { randomBytes } from 'node:crypto'
|
import * as QRCode from 'qrcode';
|
||||||
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()
|
@Injectable()
|
||||||
export class TotpService {
|
export class TotpService {
|
||||||
@ -17,7 +19,7 @@ export class TotpService {
|
|||||||
async generate(user: User) {
|
async generate(user: User) {
|
||||||
const secret = encode(randomBytes(15))
|
const secret = encode(randomBytes(15))
|
||||||
.replace(/=/g, '')
|
.replace(/=/g, '')
|
||||||
.substring(0, 24)
|
.substring(0, 24);
|
||||||
|
|
||||||
const totp = new TOTP({
|
const totp = new TOTP({
|
||||||
issuer: 'TeaStream',
|
issuer: 'TeaStream',
|
||||||
@ -25,18 +27,18 @@ export class TotpService {
|
|||||||
algorithm: 'SHA-1',
|
algorithm: 'SHA-1',
|
||||||
digits: 6,
|
digits: 6,
|
||||||
secret,
|
secret,
|
||||||
})
|
});
|
||||||
|
|
||||||
const qrcodeUrl = await QRCode.toDataURL(totp.toString())
|
const qrcodeUrl = await QRCode.toDataURL(totp.toString());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
qrcodeUrl,
|
qrcodeUrl,
|
||||||
secret,
|
secret,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async enable(user: User, input: EnableTotpInput) {
|
async enable(user: User, input: EnableTotpInput) {
|
||||||
const { pin, secret } = input
|
const { pin, secret } = input;
|
||||||
|
|
||||||
const totp = new TOTP({
|
const totp = new TOTP({
|
||||||
issuer: 'TeaStream',
|
issuer: 'TeaStream',
|
||||||
@ -44,12 +46,12 @@ export class TotpService {
|
|||||||
algorithm: 'SHA-1',
|
algorithm: 'SHA-1',
|
||||||
digits: 6,
|
digits: 6,
|
||||||
secret,
|
secret,
|
||||||
})
|
});
|
||||||
|
|
||||||
const delta = totp.validate({ token: pin })
|
const delta = totp.validate({ token: pin });
|
||||||
|
|
||||||
if (delta === null) {
|
if (delta === null) {
|
||||||
throw new BadRequestException('Невереый код')
|
throw new BadRequestException('Невереый код');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaService.user.update({
|
await this.prismaService.user.update({
|
||||||
@ -58,9 +60,9 @@ export class TotpService {
|
|||||||
isTotpEnabled: true,
|
isTotpEnabled: true,
|
||||||
totpSecret: secret,
|
totpSecret: secret,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async disable(user: User) {
|
async disable(user: User) {
|
||||||
@ -70,8 +72,8 @@ export class TotpService {
|
|||||||
isTotpEnabled: false,
|
isTotpEnabled: false,
|
||||||
totpSecret: null,
|
totpSecret: null,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsUUID } from 'class-validator'
|
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class VerificationInput {
|
export class VerificationInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
public token: string
|
public token: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
import { VerificationService } from './verification.service'
|
|
||||||
import { VerificationResolver } from './verification.resolver'
|
import { VerificationResolver } from './verification.resolver';
|
||||||
|
import { VerificationService } from './verification.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [VerificationResolver, VerificationService],
|
providers: [VerificationResolver, VerificationService],
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { AuthModel } from '@/src/module/auth/account/models/auth.model'
|
import {
|
||||||
import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input'
|
Args, Context, Mutation, Resolver,
|
||||||
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'
|
} from '@nestjs/graphql';
|
||||||
import { GqlContext } from '@/src/shared/types/gql-context.types'
|
|
||||||
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql'
|
import { AuthModel } from '@/src/module/auth/account/models/auth.model';
|
||||||
import { VerificationService } from './verification.service'
|
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')
|
@Resolver('Verification')
|
||||||
export class VerificationResolver {
|
export class VerificationResolver {
|
||||||
@ -15,6 +19,6 @@ export class VerificationResolver {
|
|||||||
@Args('data') input: VerificationInput,
|
@Args('data') input: VerificationInput,
|
||||||
@UserAgent() userAgent: string,
|
@UserAgent() userAgent: string,
|
||||||
) {
|
) {
|
||||||
return this.verificationService.verify(req, input, userAgent)
|
return this.verificationService.verify(req, input, userAgent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input'
|
import { Request } from 'express';
|
||||||
import { MailService } from '@/src/module/libs/mail/mail.service'
|
|
||||||
import { generateToken } from '@/src/shared/util/generate-token.util'
|
import { PrismaService } from '@/src/core/prisma/prisma.service';
|
||||||
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
|
import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input';
|
||||||
import { saveSession } from '@/src/shared/util/session.util'
|
import { MailService } from '@/src/module/libs/mail/mail.service';
|
||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
import { generateToken } from '@/src/shared/util/generate-token.util';
|
||||||
import { TokenType, User } from '@prisma/generated'
|
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util';
|
||||||
import { Request } from 'express'
|
import { saveSession } from '@/src/shared/util/session.util';
|
||||||
|
import { TokenType, User } from '@prisma/generated';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VerificationService {
|
export class VerificationService {
|
||||||
@ -17,19 +18,19 @@ export class VerificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async verify(req: Request, input: VerificationInput, userAgent: string) {
|
public async verify(req: Request, input: VerificationInput, userAgent: string) {
|
||||||
const { token } = input
|
const { token } = input;
|
||||||
const existingToken = await this.prismaService.token.findUnique({
|
const existingToken = await this.prismaService.token.findUnique({
|
||||||
where: { token, type: TokenType.EMAIL_VERIFY },
|
where: { token, type: TokenType.EMAIL_VERIFY },
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!existingToken) {
|
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) {
|
if (hasExpired) {
|
||||||
throw new BadRequestException('Токен истек')
|
throw new BadRequestException('Токен истек');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.prismaService.user.update({
|
const user = await this.prismaService.user.update({
|
||||||
@ -39,16 +40,16 @@ export class VerificationService {
|
|||||||
data: {
|
data: {
|
||||||
isEmailVerified: true,
|
isEmailVerified: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
await this.prismaService.token.delete({
|
await this.prismaService.token.delete({
|
||||||
where: {
|
where: {
|
||||||
id: existingToken.id,
|
id: existingToken.id,
|
||||||
type: TokenType.EMAIL_VERIFY,
|
type: TokenType.EMAIL_VERIFY,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return saveSession(req, user, getSessionMetadata(req, userAgent))
|
return saveSession(req, user, getSessionMetadata(req, userAgent));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendVerificationToken(user: User) {
|
public async sendVerificationToken(user: User) {
|
||||||
@ -56,10 +57,10 @@ export class VerificationService {
|
|||||||
this.prismaService,
|
this.prismaService,
|
||||||
user,
|
user,
|
||||||
TokenType.EMAIL_VERIFY,
|
TokenType.EMAIL_VERIFY,
|
||||||
)
|
);
|
||||||
|
|
||||||
await this.mailService.sendVerificationToken(user.email, verificationToken.token);
|
await this.mailService.sendVerificationToken(user.email, verificationToken.token);
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CategoryService } from './category.service';
|
|
||||||
import { CategoryResolver } from './category.resolver';
|
import { CategoryResolver } from './category.resolver';
|
||||||
|
import { CategoryService } from './category.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [CategoryResolver, CategoryService],
|
providers: [CategoryResolver, CategoryService],
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { CategoryModel } from '@/src/module/category/models/category.model'
|
import { Args, Query, Resolver } from '@nestjs/graphql';
|
||||||
import { Args, Query, Resolver } from '@nestjs/graphql'
|
|
||||||
import { CategoryService } from './category.service'
|
import { CategoryModel } from '@/src/module/category/models/category.model';
|
||||||
|
|
||||||
|
import { CategoryService } from './category.service';
|
||||||
|
|
||||||
@Resolver('Category')
|
@Resolver('Category')
|
||||||
export class CategoryResolver {
|
export class CategoryResolver {
|
||||||
@ -8,16 +10,16 @@ export class CategoryResolver {
|
|||||||
|
|
||||||
@Query(() => [CategoryModel], { name: 'findAllCategories' })
|
@Query(() => [CategoryModel], { name: 'findAllCategories' })
|
||||||
public async findAll() {
|
public async findAll() {
|
||||||
return this.categoryService.findAll()
|
return this.categoryService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [CategoryModel], { name: 'findRandomCategories' })
|
@Query(() => [CategoryModel], { name: 'findRandomCategories' })
|
||||||
public async findRandom() {
|
public async findRandom() {
|
||||||
return this.categoryService.findRandom()
|
return this.categoryService.findRandom();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => CategoryModel, { name: 'findCategoryBySlug' })
|
@Query(() => CategoryModel, { name: 'findCategoryBySlug' })
|
||||||
public async findByhSlug(@Args('slug') slug: string) {
|
public async findByhSlug(@Args('slug') slug: string) {
|
||||||
return this.categoryService.findBySlug(slug)
|
return this.categoryService.findBySlug(slug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
@Injectable()
|
||||||
export class CategoryService {
|
export class CategoryService {
|
||||||
@ -19,16 +20,16 @@ export class CategoryService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findRandom() {
|
public async findRandom() {
|
||||||
const total = await this.prismaService.category.count({})
|
const total = await this.prismaService.category.count({});
|
||||||
|
|
||||||
const randomIndexes = new Set<number>()
|
const randomIndexes = new Set<number>();
|
||||||
while (randomIndexes.size < 7) {
|
while (randomIndexes.size < 7) {
|
||||||
const randomIndex = Math.floor(Math.random() * total)
|
const randomIndex = Math.floor(Math.random() * total);
|
||||||
randomIndexes.add(randomIndex)
|
randomIndexes.add(randomIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = await this.prismaService.category.findMany({
|
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) {
|
public async findBySlug(slug: string) {
|
||||||
@ -60,12 +61,12 @@ export class CategoryService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new NotFoundException('Категория не найдена')
|
throw new NotFoundException('Категория не найдена');
|
||||||
}
|
}
|
||||||
|
|
||||||
return category
|
return category;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
backend/src/module/category/index.ts
Normal file
1
backend/src/module/category/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { CategoryModel } from './models/category.model';
|
||||||
@ -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()
|
@ObjectType()
|
||||||
export class CategoryModel implements Category {
|
export class CategoryModel implements Category {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
public id: string
|
public id: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
public title: string
|
public title: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
public slug: string
|
public slug: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
public description: string
|
public description: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
public thumbnailUrl: string
|
public thumbnailUrl: string;
|
||||||
|
|
||||||
@Field(() => [StreamModel])
|
@Field(() => [StreamModel])
|
||||||
public streams: StreamModel[]
|
public streams: StreamModel[];
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
public createdAt: Date
|
public createdAt: Date;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
public updatedAt: Date
|
public updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
import { ChannelService } from './channel.service'
|
|
||||||
import { ChannelResolver } from './channel.resolver'
|
import { ChannelResolver } from './channel.resolver';
|
||||||
|
import { ChannelService } from './channel.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [ChannelResolver, ChannelService],
|
providers: [ChannelResolver, ChannelService],
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
import { Args, Query, Resolver } from '@nestjs/graphql';
|
||||||
import { SubscriptionModel } from '@/src/module/sponsorship/subscription/model/subscription.model'
|
|
||||||
import { Args, Query, Resolver } from '@nestjs/graphql'
|
import { UserModel } from '@/src/module/auth/account/models/user.model';
|
||||||
import { ChannelService } from './channel.service'
|
import { SubscriptionModel } from '@/src/module/sponsorship/subscription/model/subscription.model';
|
||||||
|
|
||||||
|
import { ChannelService } from './channel.service';
|
||||||
|
|
||||||
@Resolver('Channel')
|
@Resolver('Channel')
|
||||||
export class ChannelResolver {
|
export class ChannelResolver {
|
||||||
@ -9,21 +11,21 @@ export class ChannelResolver {
|
|||||||
|
|
||||||
@Query(() => [UserModel], { name: 'findRecommendedChannels' })
|
@Query(() => [UserModel], { name: 'findRecommendedChannels' })
|
||||||
public async findRecommended() {
|
public async findRecommended() {
|
||||||
return this.channelService.findRecommendedChannel()
|
return this.channelService.findRecommendedChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => UserModel, { name: 'findChannelByUsername' })
|
@Query(() => UserModel, { name: 'findChannelByUsername' })
|
||||||
public async findByUsername(@Args('name') name: string) {
|
public async findByUsername(@Args('name') name: string) {
|
||||||
return this.channelService.findByUsername(name)
|
return this.channelService.findByUsername(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => Number, { name: 'findChannelFollowersCount' })
|
@Query(() => Number, { name: 'findChannelFollowersCount' })
|
||||||
public async findFollowersCount(@Args('channelId') channelId: string) {
|
public async findFollowersCount(@Args('channelId') channelId: string) {
|
||||||
return this.channelService.findFollowersCountByChannel(channelId)
|
return this.channelService.findFollowersCountByChannel(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [SubscriptionModel], { name: 'findSponsorsByChannel' })
|
@Query(() => [SubscriptionModel], { name: 'findSponsorsByChannel' })
|
||||||
public async findSponsorsByChannel(@Args('channelId') channelId: string) {
|
public async findSponsorsByChannel(@Args('channelId') channelId: string) {
|
||||||
return this.channelService.findSponsorsByChannel(channelId)
|
return this.channelService.findSponsorsByChannel(channelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
@Injectable()
|
||||||
export class ChannelService {
|
export class ChannelService {
|
||||||
@ -14,7 +15,7 @@ export class ChannelService {
|
|||||||
orderBy: { followings: { _count: 'desc' } },
|
orderBy: { followings: { _count: 'desc' } },
|
||||||
include: { stream: true },
|
include: { stream: true },
|
||||||
take: 7,
|
take: 7,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findByUsername(name: string) {
|
public async findByUsername(name: string) {
|
||||||
@ -25,28 +26,28 @@ export class ChannelService {
|
|||||||
stream: { include: { category: true } },
|
stream: { include: { category: true } },
|
||||||
followings: true,
|
followings: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
throw new NotFoundException('Канал не найден')
|
throw new NotFoundException('Канал не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
return channel
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findFollowersCountByChannel(channelId: string) {
|
public async findFollowersCountByChannel(channelId: string) {
|
||||||
return this.prismaService.follow.count({
|
return this.prismaService.follow.count({
|
||||||
where: { following: { id: channelId } },
|
where: { following: { id: channelId } },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findSponsorsByChannel(channelId: string) {
|
public async findSponsorsByChannel(channelId: string) {
|
||||||
const channel = await this.prismaService.user.findUnique({
|
const channel = await this.prismaService.user.findUnique({
|
||||||
where: { id: channelId, isDeactivated: false },
|
where: { id: channelId, isDeactivated: false },
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
throw new NotFoundException('Канал не найден')
|
throw new NotFoundException('Канал не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prismaService.sponsorshipSubscription.findMany({
|
return this.prismaService.sponsorshipSubscription.findMany({
|
||||||
@ -55,6 +56,6 @@ export class ChannelService {
|
|||||||
include: {
|
include: {
|
||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
import { ChatService } from './chat.service'
|
|
||||||
import { ChatResolver } from './chat.resolver'
|
import { ChatResolver } from './chat.resolver';
|
||||||
|
import { ChatService } from './chat.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [ChatResolver, ChatService],
|
providers: [ChatResolver, ChatService],
|
||||||
|
|||||||
@ -1,24 +1,29 @@
|
|||||||
import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input'
|
import {
|
||||||
import { SendMessageInput } from '@/src/module/chat/input/send-message.input'
|
Args, Mutation, Query, Resolver, Subscription,
|
||||||
import { StreamModel } from '@/src/module/stream/models/stream.model'
|
} from '@nestjs/graphql';
|
||||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
import { PubSub } from 'graphql-subscriptions';
|
||||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
|
||||||
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'
|
import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input';
|
||||||
import { ChatMessage, User } from '@prisma/generated'
|
import { SendMessageInput } from '@/src/module/chat/input/send-message.input';
|
||||||
import { PubSub } from 'graphql-subscriptions'
|
import { StreamModel } from '@/src/module/stream/models/stream.model';
|
||||||
import { ChatService } from './chat.service'
|
import { Authorization } from '@/src/shared/decorators/auth.decorator';
|
||||||
import { ChatMessageModel } from './models/chat-message.model'
|
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')
|
@Resolver('Chat')
|
||||||
export class ChatResolver {
|
export class ChatResolver {
|
||||||
public pubSub: PubSub
|
public pubSub: PubSub;
|
||||||
|
|
||||||
constructor(private readonly chatService: ChatService) {
|
constructor(private readonly chatService: ChatService) {
|
||||||
this.pubSub = new PubSub()
|
this.pubSub = new PubSub();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [ChatMessageModel], { name: 'findMessagesByStream' })
|
@Query(() => [ChatMessageModel], { name: 'findMessagesByStream' })
|
||||||
public async findMessagesByStream(@Args('streamId') streamId: string) {
|
public async findMessagesByStream(@Args('streamId') streamId: string) {
|
||||||
return this.chatService.findMessagesByStream(streamId)
|
return this.chatService.findMessagesByStream(streamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -27,15 +32,15 @@ export class ChatResolver {
|
|||||||
@Authorized('id') userId: User['id'],
|
@Authorized('id') userId: User['id'],
|
||||||
@Args('data') input: SendMessageInput,
|
@Args('data') input: SendMessageInput,
|
||||||
) {
|
) {
|
||||||
const message = this.chatService.sendMessage(userId, input)
|
const message = this.chatService.sendMessage(userId, input);
|
||||||
void this.pubSub.publish('CHAT_MESSAGE_ADDED', { message })
|
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 })
|
@Subscription(() => ChatMessageModel, { name: 'chatMessageAdded', filter: (payload: { message: ChatMessage }, variables: ChatMessageModel) => payload.message.streamId === variables.streamId })
|
||||||
public async chatMessageAdded(@Args('streamId') streamId: string) {
|
public async chatMessageAdded(@Args('streamId') streamId: string) {
|
||||||
return this.pubSub.asyncIterableIterator('CHAT_MESSAGE_ADDED')
|
return this.pubSub.asyncIterableIterator('CHAT_MESSAGE_ADDED');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@ -44,6 +49,6 @@ export class ChatResolver {
|
|||||||
@Args('data') input: ChangeChatSettingsInput,
|
@Args('data') input: ChangeChatSettingsInput,
|
||||||
@Authorized() user: User,
|
@Authorized() user: User,
|
||||||
) {
|
) {
|
||||||
return this.chatService.changeSettings(user, input)
|
return this.chatService.changeSettings(user, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input'
|
|
||||||
import { SendMessageInput } from '@/src/module/chat/input/send-message.input'
|
import { PrismaService } from '@/src/core/prisma/prisma.service';
|
||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input';
|
||||||
import { Stream, User } from '@prisma/generated'
|
import { SendMessageInput } from '@/src/module/chat/input/send-message.input';
|
||||||
|
import { Stream, User } from '@prisma/generated';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChatService {
|
export class ChatService {
|
||||||
@ -16,27 +17,27 @@ export class ChatService {
|
|||||||
where: { streamId },
|
where: { streamId },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: { user: true },
|
include: { user: true },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendMessage(userId: User['id'], input: SendMessageInput) {
|
public async sendMessage(userId: User['id'], input: SendMessageInput) {
|
||||||
const { text, streamId } = input
|
const { text, streamId } = input;
|
||||||
|
|
||||||
const stream = await this.prismaService.stream.findUnique({
|
const stream = await this.prismaService.stream.findUnique({
|
||||||
where: { id: streamId },
|
where: { id: streamId },
|
||||||
})
|
});
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw new NotFoundException('Стрим не найден')
|
throw new NotFoundException('Стрим не найден');
|
||||||
}
|
}
|
||||||
if (!stream.isLive) {
|
if (!stream.isLive) {
|
||||||
throw new BadRequestException('Стрим не запущен')
|
throw new BadRequestException('Стрим не запущен');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.prismaService.user.findUnique({
|
const user = await this.prismaService.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
})
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('Пользователь не найден')
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prismaService.chatMessage.create({
|
return this.prismaService.chatMessage.create({
|
||||||
@ -56,15 +57,15 @@ export class ChatService {
|
|||||||
include: {
|
include: {
|
||||||
stream: true,
|
stream: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async changeSettings(user: User, input: ChangeChatSettingsInput) {
|
public async changeSettings(user: User, input: ChangeChatSettingsInput) {
|
||||||
const { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly } = input
|
const { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly } = input;
|
||||||
|
|
||||||
return this.prismaService.stream.update({
|
return this.prismaService.stream.update({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
data: { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly },
|
data: { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
backend/src/module/chat/index.ts
Normal file
1
backend/src/module/chat/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ChatMessageModel } from './models/chat-message.model';
|
||||||
@ -1,17 +1,17 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsBoolean } from 'class-validator'
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class ChangeChatSettingsInput {
|
export class ChangeChatSettingsInput {
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
public isChatEnable: boolean
|
public isChatEnable: boolean;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
public isChatFollowersOnly: boolean
|
public isChatFollowersOnly: boolean;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
public isChatPremiumFollowersOnly: boolean
|
public isChatPremiumFollowersOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { Stream } from '@prisma/generated'
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
import { IsNotEmpty, IsString } from 'class-validator'
|
|
||||||
|
import { Stream } from '@prisma/generated';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class SendMessageInput {
|
export class SendMessageInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
public text: string
|
public text: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
streamId: Stream['id']
|
streamId: Stream['id'];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,26 @@
|
|||||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||||
import { StreamModel } from '@/src/module/stream/models/stream.model'
|
|
||||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
import { UserModel } from '@/src/module/auth/account/models/user.model';
|
||||||
import { ChatMessage } from '@prisma/generated'
|
import { StreamModel } from '@/src/module/stream/models/stream.model';
|
||||||
|
import { ChatMessage } from '@prisma/generated';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class ChatMessageModel implements ChatMessage {
|
export class ChatMessageModel implements ChatMessage {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
public id: string
|
public id: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
text: string
|
text: string;
|
||||||
|
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
streamId: StreamModel['id']
|
streamId: StreamModel['id'];
|
||||||
|
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
userId: UserModel['id']
|
userId: UserModel['id'];
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
public createdAt: Date
|
public createdAt: Date;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
public updatedAt: Date
|
public updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { NotificationService } from '@/src/module/notification/notification.service'
|
import { Module } from '@nestjs/common';
|
||||||
import { Module } from '@nestjs/common'
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { ScheduleModule } from '@nestjs/schedule'
|
|
||||||
import { CronService } from './cron.service'
|
import { NotificationService } from '@/src/module/notification/notification.service';
|
||||||
|
|
||||||
|
import { CronService } from './cron.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ScheduleModule.forRoot()],
|
imports: [ScheduleModule.forRoot()],
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { Injectable } from '@nestjs/common';
|
||||||
import { MailService } from '@/src/module/libs/mail/mail.service'
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { StorageService } from '@/src/module/libs/storage/storage.service'
|
|
||||||
import { TelegramService } from '@/src/module/libs/telegram/telegram.service'
|
import { PrismaService } from '@/src/core/prisma/prisma.service';
|
||||||
import { NotificationService } from '@/src/module/notification/notification.service'
|
import { MailService } from '@/src/module/libs/mail/mail.service';
|
||||||
import { Injectable } from '@nestjs/common'
|
import { StorageService } from '@/src/module/libs/storage/storage.service';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule'
|
import { TelegramService } from '@/src/module/libs/telegram/telegram.service';
|
||||||
|
import { NotificationService } from '@/src/module/notification/notification.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CronService {
|
export class CronService {
|
||||||
@ -19,8 +20,8 @@ export class CronService {
|
|||||||
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_NOON)
|
@Cron(CronExpression.EVERY_DAY_AT_NOON)
|
||||||
private async deleteDeactivatedAccounts() {
|
private async deleteDeactivatedAccounts() {
|
||||||
const sevenDayAgo = new Date()
|
const sevenDayAgo = new Date();
|
||||||
sevenDayAgo.setDate(sevenDayAgo.getDay() - 7)
|
sevenDayAgo.setDate(sevenDayAgo.getDay() - 7);
|
||||||
|
|
||||||
const deactivatedAccounts = await this.prismaService.user.findMany({
|
const deactivatedAccounts = await this.prismaService.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
@ -32,17 +33,16 @@ export class CronService {
|
|||||||
include: {
|
include: {
|
||||||
notificationSettings: true,
|
notificationSettings: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
for (const user of deactivatedAccounts) {
|
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) {
|
if (user?.telegramId) {
|
||||||
await this.telegramService.sendAccountDeletionToken(user?.telegramId)
|
await this.telegramService.sendAccountDeletionToken(user?.telegramId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.avatar) {
|
if (user.avatar) {
|
||||||
await this.storageService.remove(user.avatar)
|
await this.storageService.remove(user.avatar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export class CronService {
|
|||||||
lte: sevenDayAgo,
|
lte: sevenDayAgo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cron('0 0 */4 * *')
|
@Cron('0 0 */4 * *')
|
||||||
@ -61,21 +61,21 @@ export class CronService {
|
|||||||
const users = await this.prismaService.user.findMany({
|
const users = await this.prismaService.user.findMany({
|
||||||
where: { isTotpEnabled: false },
|
where: { isTotpEnabled: false },
|
||||||
include: { notificationSettings: true },
|
include: { notificationSettings: true },
|
||||||
})
|
});
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mailService.sendEnableTwoFactor(user.email)
|
await this.mailService.sendEnableTwoFactor(user.email);
|
||||||
|
|
||||||
if (user.notificationSettings?.siteNotifications) {
|
if (user.notificationSettings?.siteNotifications) {
|
||||||
await this.notificationService.createEnableTwoFactor(user.id)
|
await this.notificationService.createEnableTwoFactor(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.notificationSettings?.telegramNotifications && user.telegramId) {
|
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() {
|
public async verifyChannels() {
|
||||||
const users = await this.prismaService.user.findMany({
|
const users = await this.prismaService.user.findMany({
|
||||||
include: { notificationSettings: true },
|
include: { notificationSettings: true },
|
||||||
})
|
});
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
const followersCount = await this.prismaService.follow.count({
|
const followersCount = await this.prismaService.follow.count({
|
||||||
where: { followingId: user.id },
|
where: { followingId: user.id },
|
||||||
})
|
});
|
||||||
|
|
||||||
if (followersCount > 10 && !user.isVerified) {
|
if (followersCount > 10 && !user.isVerified) {
|
||||||
await this.prismaService.user.update({
|
await this.prismaService.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { isVerified: true },
|
data: { isVerified: true },
|
||||||
})
|
});
|
||||||
|
|
||||||
await this.mailService.sendVerifyChannel(user.email)
|
await this.mailService.sendVerifyChannel(user.email);
|
||||||
|
|
||||||
if (user.notificationSettings?.siteNotifications) {
|
if (user.notificationSettings?.siteNotifications) {
|
||||||
await this.notificationService.createVerifyChannel(user.id)
|
await this.notificationService.createVerifyChannel(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.notificationSettings?.telegramNotifications && user.telegramId) {
|
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)
|
@Cron(CronExpression.EVERY_DAY_AT_1AM)
|
||||||
public async deleteOldNotifications() {
|
public async deleteOldNotifications() {
|
||||||
const sevenDaysAgo = new Date()
|
const sevenDaysAgo = new Date();
|
||||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||||
|
|
||||||
await this.prismaService.notification.deleteMany({
|
await this.prismaService.notification.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
@ -121,6 +121,6 @@ export class CronService {
|
|||||||
lte: sevenDaysAgo,
|
lte: sevenDaysAgo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { NotificationService } from '@/src/module/notification/notification.service'
|
import { Module } from '@nestjs/common';
|
||||||
import { Module } from '@nestjs/common'
|
|
||||||
import { FollowService } from './follow.service'
|
import { NotificationService } from '@/src/module/notification/notification.service';
|
||||||
import { FollowResolver } from './follow.resolver'
|
|
||||||
|
import { FollowResolver } from './follow.resolver';
|
||||||
|
import { FollowService } from './follow.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [FollowResolver, FollowService, NotificationService],
|
providers: [FollowResolver, FollowService, NotificationService],
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { FollowModel } from '@/src/module/follow/model/follow.model'
|
import {
|
||||||
import { Authorization } from '@/src/shared/decorators/auth.decorator'
|
Args, Mutation, Query, Resolver,
|
||||||
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
|
} from '@nestjs/graphql';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
|
|
||||||
import { User } from '@prisma/generated'
|
import { FollowModel } from '@/src/module/follow/model/follow.model';
|
||||||
import { FollowService } from './follow.service'
|
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')
|
@Resolver('Follow')
|
||||||
export class FollowResolver {
|
export class FollowResolver {
|
||||||
@ -12,24 +16,24 @@ export class FollowResolver {
|
|||||||
@Authorization()
|
@Authorization()
|
||||||
@Query(() => [FollowModel], { name: 'findMyFollowers' })
|
@Query(() => [FollowModel], { name: 'findMyFollowers' })
|
||||||
public async findMyFollowers(@Authorized() user: User) {
|
public async findMyFollowers(@Authorized() user: User) {
|
||||||
return this.followService.findMyFollowers(user)
|
return this.followService.findMyFollowers(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Query(() => [FollowModel], { name: 'findMyFollowings' })
|
@Query(() => [FollowModel], { name: 'findMyFollowings' })
|
||||||
public async findMyFollowings(@Authorized() user: User) {
|
public async findMyFollowings(@Authorized() user: User) {
|
||||||
return this.followService.findMyFollowings(user)
|
return this.followService.findMyFollowings(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Mutation(() => FollowModel, { name: 'followChannel' })
|
@Mutation(() => FollowModel, { name: 'followChannel' })
|
||||||
public async follow(@Authorized() user: User, @Args('channelId') channelId: string) {
|
public async follow(@Authorized() user: User, @Args('channelId') channelId: string) {
|
||||||
return this.followService.follow(user, channelId)
|
return this.followService.follow(user, channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorization()
|
@Authorization()
|
||||||
@Mutation(() => FollowModel, { name: 'unfollowChannel' })
|
@Mutation(() => FollowModel, { name: 'unfollowChannel' })
|
||||||
public async unfollow(@Authorized() user: User, @Args('channelId') channelId: string) {
|
public async unfollow(@Authorized() user: User, @Args('channelId') channelId: string) {
|
||||||
return this.followService.unfollow(user, channelId)
|
return this.followService.unfollow(user, channelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { TelegramService } from '@/src/module/libs/telegram/telegram.service';
|
||||||
import { NotificationService } from '@/src/module/notification/notification.service'
|
import { NotificationService } from '@/src/module/notification/notification.service';
|
||||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'
|
import { User } from '@prisma/generated';
|
||||||
import { User } from '@prisma/generated'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FollowService {
|
export class FollowService {
|
||||||
@ -18,7 +19,7 @@ export class FollowService {
|
|||||||
where: { followingId: user.id },
|
where: { followingId: user.id },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: { follower: true, following: true },
|
include: { follower: true, following: true },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findMyFollowings(user: User) {
|
public async findMyFollowings(user: User) {
|
||||||
@ -26,19 +27,19 @@ export class FollowService {
|
|||||||
where: { followerId: user.id },
|
where: { followerId: user.id },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: { follower: true, following: true },
|
include: { follower: true, following: true },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async follow(user: User, channelId: string) {
|
public async follow(user: User, channelId: string) {
|
||||||
const channel = await this.prismaService.user.findUnique({
|
const channel = await this.prismaService.user.findUnique({
|
||||||
where: { id: channelId },
|
where: { id: channelId },
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
throw new NotFoundException('Пользователь не найден')
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
if (channel.id === user.id) {
|
if (channel.id === user.id) {
|
||||||
throw new ConflictException('Нельзя подписаться на себя')
|
throw new ConflictException('Нельзя подписаться на себя');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingFollow = await this.prismaService.follow.findFirst({
|
const existingFollow = await this.prismaService.follow.findFirst({
|
||||||
@ -46,10 +47,10 @@ export class FollowService {
|
|||||||
followerId: user.id,
|
followerId: user.id,
|
||||||
followingId: channel.id,
|
followingId: channel.id,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (existingFollow) {
|
if (existingFollow) {
|
||||||
throw new ConflictException('Подписка уже существует')
|
throw new ConflictException('Подписка уже существует');
|
||||||
}
|
}
|
||||||
|
|
||||||
const follow = await this.prismaService.follow.create({
|
const follow = await this.prismaService.follow.create({
|
||||||
@ -65,27 +66,28 @@ export class FollowService {
|
|||||||
},
|
},
|
||||||
follower: true,
|
follower: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (follow.following.notificationSettings?.siteNotifications) {
|
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) {
|
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) {
|
public async unfollow(user: User, channelId: string) {
|
||||||
const channel = await this.prismaService.user.findUnique({
|
const channel = await this.prismaService.user.findUnique({
|
||||||
where: { id: channelId },
|
where: { id: channelId },
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
throw new NotFoundException('Пользователь не найден')
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
if (channel.id === user.id) {
|
if (channel.id === user.id) {
|
||||||
throw new ConflictException('Нельзя отписаться от себя')
|
throw new ConflictException('Нельзя отписаться от себя');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingFollow = await this.prismaService.follow.findFirst({
|
const existingFollow = await this.prismaService.follow.findFirst({
|
||||||
@ -93,10 +95,10 @@ export class FollowService {
|
|||||||
followerId: user.id,
|
followerId: user.id,
|
||||||
followingId: channel.id,
|
followingId: channel.id,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!existingFollow) {
|
if (!existingFollow) {
|
||||||
throw new ConflictException('Подписки не существует')
|
throw new ConflictException('Подписки не существует');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prismaService.follow.delete({
|
return this.prismaService.follow.delete({
|
||||||
@ -105,6 +107,6 @@ export class FollowService {
|
|||||||
following: true,
|
following: true,
|
||||||
follower: true,
|
follower: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
backend/src/module/follow/index.ts
Normal file
1
backend/src/module/follow/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { FollowModel } from './model/follow.model';
|
||||||
@ -1,27 +1,28 @@
|
|||||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
|
||||||
import { Follow } from '@prisma/generated'
|
import { UserModel } from '@/src/module/auth/account/models/user.model';
|
||||||
|
import { Follow } from '@prisma/generated';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class FollowModel implements Follow {
|
export class FollowModel implements Follow {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
public id: string
|
public id: string;
|
||||||
|
|
||||||
@Field(() => UserModel)
|
@Field(() => UserModel)
|
||||||
public following: UserModel
|
public following: UserModel;
|
||||||
|
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
public followingId: string
|
public followingId: string;
|
||||||
|
|
||||||
@Field(() => UserModel)
|
@Field(() => UserModel)
|
||||||
public follower: UserModel
|
public follower: UserModel;
|
||||||
|
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
public followerId: string
|
public followerId: string;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
public createdAt: Date
|
public createdAt: Date;
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
public updatedAt: Date
|
public updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
import {
|
||||||
LiveKitOptionSymbol,
|
LiveKitOptionSymbol,
|
||||||
TypeLiveKitAsyncOptions,
|
TypeLiveKitAsyncOptions,
|
||||||
TypeLiveKitOptions,
|
TypeLiveKitOptions,
|
||||||
} from '@/src/module/libs/livekit/type/livekit.type'
|
} from '@/src/module/libs/livekit/type/livekit.type';
|
||||||
import { DynamicModule, Module } from '@nestjs/common'
|
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class LiveKitModule {
|
export class LiveKitModule {
|
||||||
@ -20,7 +21,7 @@ export class LiveKitModule {
|
|||||||
],
|
],
|
||||||
exports: [LiveKitService],
|
exports: [LiveKitService],
|
||||||
global: true,
|
global: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static registerAsync(options: TypeLiveKitAsyncOptions): DynamicModule {
|
public static registerAsync(options: TypeLiveKitAsyncOptions): DynamicModule {
|
||||||
@ -37,6 +38,6 @@ export class LiveKitModule {
|
|||||||
],
|
],
|
||||||
exports: [LiveKitService],
|
exports: [LiveKitService],
|
||||||
global: true,
|
global: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +1,47 @@
|
|||||||
import { LiveKitOptionSymbol, TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type'
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Inject, Injectable } from '@nestjs/common'
|
import { IngressClient, RoomServiceClient, WebhookReceiver } from 'livekit-server-sdk';
|
||||||
import { IngressClient, RoomServiceClient, WebhookReceiver } from 'livekit-server-sdk'
|
|
||||||
|
import { LiveKitOptionSymbol, TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LiveKitService {
|
export class LiveKitService {
|
||||||
private roomService: RoomServiceClient
|
private readonly roomService: RoomServiceClient;
|
||||||
private ingressClient: IngressClient
|
|
||||||
private webhookReceiver: WebhookReceiver
|
private readonly ingressClient: IngressClient;
|
||||||
|
|
||||||
|
private readonly webhookReceiver: WebhookReceiver;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LiveKitOptionSymbol) private readonly options: TypeLiveKitOptions,
|
@Inject(LiveKitOptionSymbol) private readonly options: TypeLiveKitOptions,
|
||||||
) {
|
) {
|
||||||
this.roomService = new RoomServiceClient(options.apiUrl, options.apiKey, options.apiSecret)
|
this.roomService = new RoomServiceClient(options.apiUrl, options.apiKey, options.apiSecret);
|
||||||
this.ingressClient = new IngressClient(options.apiUrl)
|
this.ingressClient = new IngressClient(options.apiUrl);
|
||||||
this.webhookReceiver = new WebhookReceiver(options.apiKey, options.apiSecret)
|
this.webhookReceiver = new WebhookReceiver(options.apiKey, options.apiSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get ingress(): IngressClient {
|
public get ingress(): IngressClient {
|
||||||
return this.createProxy(this.ingressClient)
|
return this.createProxy(this.ingressClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get room(): RoomServiceClient {
|
public get room(): RoomServiceClient {
|
||||||
return this.createProxy(this.roomService)
|
return this.createProxy(this.roomService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get webhook(): WebhookReceiver {
|
public get webhook(): WebhookReceiver {
|
||||||
return this.createProxy(this.webhookReceiver)
|
return this.createProxy(this.webhookReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createProxy<Target extends object, Prop extends keyof Target>(target: Target) {
|
private createProxy<Target extends object, Prop extends keyof Target>(target: Target) {
|
||||||
return new Proxy(target, {
|
return new Proxy(target, {
|
||||||
get: (obj, prop) => {
|
get: (obj, prop) => {
|
||||||
const value = obj[prop as Prop]
|
const value = obj[prop as Prop];
|
||||||
|
|
||||||
if (typeof value === 'function') {
|
if (typeof value === 'function') {
|
||||||
return value.bind(obj)
|
return value.bind(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value;
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 = {
|
export type TypeLiveKitOptions = {
|
||||||
apiUrl: string
|
apiUrl: string;
|
||||||
apiKey: string
|
apiKey: string;
|
||||||
apiSecret: string
|
apiSecret: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TypeLiveKitAsyncOptions = Pick<ModuleMetadata, 'imports'>
|
export type TypeLiveKitAsyncOptions = Pick<FactoryProvider<TypeLiveKitOptions>, 'inject' | 'useFactory'> & Pick<ModuleMetadata, 'imports'>;
|
||||||
& Pick<FactoryProvider<TypeLiveKitOptions>, 'useFactory' | 'inject'>
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { getMailConfig } from '@/src/core/config/mailer.config'
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { MailerModule } from '@nestjs-modules/mailer'
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { Global, Module } from '@nestjs/common'
|
import { MailerModule } from '@nestjs-modules/mailer';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config'
|
|
||||||
import { MailService } from './mail.service'
|
import { getMailConfig } from '@/src/core/config/mailer.config';
|
||||||
|
|
||||||
|
import { MailService } from './mail.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Token } from '@prisma/generated'
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ProcessEnv } from '@/src/shared/types/env'
|
import { MailerService } from '@nestjs-modules/mailer';
|
||||||
import { MailerService } from '@nestjs-modules/mailer'
|
import { render } from '@react-email/components';
|
||||||
import { Injectable } from '@nestjs/common'
|
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { ProcessEnv } from '@/src/shared/types/env';
|
||||||
import { render } from '@react-email/components'
|
import { SessionInfo } from '@/src/shared/types/session-metadata.types';
|
||||||
import { EnableTwoFactorTemplate } from './templates'
|
import { Token } from '@prisma/generated';
|
||||||
import { VerifyChannelTemplate } from './templates'
|
|
||||||
import { VerificationTemplate } from './templates'
|
import {
|
||||||
import { PasswordRecoveryTemplate } from './templates'
|
EnableTwoFactorTemplate, VerifyChannelTemplate, VerificationTemplate, PasswordRecoveryTemplate, DeactivateTemplate, AccountDeletionTemplate,
|
||||||
import { DeactivateTemplate } from './templates'
|
} from './templates';
|
||||||
import { AccountDeletionTemplate } from './templates'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MailService {
|
export class MailService {
|
||||||
@ -20,50 +19,50 @@ export class MailService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async sendVerificationToken(email: string, token: Token['token']) {
|
public async sendVerificationToken(email: string, token: Token['token']) {
|
||||||
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN')
|
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN');
|
||||||
const html = await render(VerificationTemplate({ domain, token }))
|
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) {
|
public async sendPasswordResetToken(email: string, token: Token['token'], metadata: SessionInfo) {
|
||||||
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN')
|
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN');
|
||||||
const html = await render(PasswordRecoveryTemplate({ domain, token, metadata }))
|
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) {
|
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) {
|
public async sendAccountDeletion(email: string) {
|
||||||
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN')
|
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN');
|
||||||
const html = await render(AccountDeletionTemplate({ domain }))
|
const html = await render(AccountDeletionTemplate({ domain }));
|
||||||
|
|
||||||
void this.sendMail(email, 'Удаление аккаунта', html)
|
void this.sendMail(email, 'Удаление аккаунта', html);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendEnableTwoFactor(email: string) {
|
public async sendEnableTwoFactor(email: string) {
|
||||||
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN')
|
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN');
|
||||||
const html = await render(EnableTwoFactorTemplate({ domain }))
|
const html = await render(EnableTwoFactorTemplate({ domain }));
|
||||||
|
|
||||||
void this.sendMail(email, 'Обеспечьте свою безопасность', html)
|
void this.sendMail(email, 'Обеспечьте свою безопасность', html);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendVerifyChannel(email: string) {
|
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({
|
return this.mailerService.sendMail({
|
||||||
to: email,
|
to: email,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,26 +8,29 @@ import {
|
|||||||
Section,
|
Section,
|
||||||
Tailwind,
|
Tailwind,
|
||||||
Text,
|
Text,
|
||||||
} from '@react-email/components'
|
} from '@react-email/components';
|
||||||
import * as React from 'react'
|
import * as React from 'react';
|
||||||
|
|
||||||
interface AccountDeletionTemplateProps {
|
type AccountDeletionTemplateProps = {
|
||||||
domain: string
|
domain: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function AccountDeletionTemplate({ domain }: AccountDeletionTemplateProps) {
|
export const AccountDeletionTemplate = ({ domain }: AccountDeletionTemplateProps) => {
|
||||||
const registerLink = `${domain}/account/create`
|
const registerLink = `${domain}/account/create`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
|
|
||||||
<Preview>Аккаунт удалён</Preview>
|
<Preview>Аккаунт удалён</Preview>
|
||||||
|
|
||||||
<Tailwind>
|
<Tailwind>
|
||||||
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
||||||
<Section className="text-center">
|
<Section className="text-center">
|
||||||
<Heading className="text-3xl text-black font-bold">
|
<Heading className="text-3xl text-black font-bold">
|
||||||
Ваш аккаунт был полностью удалён
|
Ваш аккаунт был полностью удалён
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text className="text-base text-black mt-2">
|
<Text className="text-base text-black mt-2">
|
||||||
Ваш аккаунт был полностью стерт из базы данных TeaStream. Все ваши данные и информация были удалены безвозвратно.
|
Ваш аккаунт был полностью стерт из базы данных TeaStream. Все ваши данные и информация были удалены безвозвратно.
|
||||||
</Text>
|
</Text>
|
||||||
@ -37,12 +40,14 @@ export function AccountDeletionTemplate({ domain }: AccountDeletionTemplateProps
|
|||||||
<Text>
|
<Text>
|
||||||
Вы больше не будете получать уведомления в Telegram и на почту.
|
Вы больше не будете получать уведомления в Telegram и на почту.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке:
|
Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке:
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={registerLink}
|
|
||||||
className="inline-flex justify-center items-center rounded-md mt-2 text-sm font-medium text-white bg-[#18B9AE] px-5 py-2 rounded-full"
|
className="inline-flex justify-center items-center rounded-md mt-2 text-sm font-medium text-white bg-[#18B9AE] px-5 py-2 rounded-full"
|
||||||
|
href={registerLink}
|
||||||
>
|
>
|
||||||
Зарегистрироваться на Teastream
|
Зарегистрироваться на Teastream
|
||||||
</Link>
|
</Link>
|
||||||
@ -56,5 +61,5 @@ export function AccountDeletionTemplate({ domain }: AccountDeletionTemplateProps
|
|||||||
</Body>
|
</Body>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
</Html>
|
</Html>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,90 +1,104 @@
|
|||||||
import type { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
import {
|
||||||
import { Body, Head, Heading, Link, Preview, Section, Tailwind, Text } from '@react-email/components'
|
Body, Head, Heading, Link, Preview, Section, Tailwind, Text,
|
||||||
import { Html } from '@react-email/html'
|
} from '@react-email/components';
|
||||||
import * as React from 'react'
|
import { Html } from '@react-email/html';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
interface DeactivateTemplateProps {
|
import type { SessionInfo } from '@/src/shared/types/session-metadata.types';
|
||||||
token: string
|
|
||||||
metadata: SessionInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DeactivateTemplate({ token, metadata }: DeactivateTemplateProps) {
|
type DeactivateTemplateProps = {
|
||||||
return (
|
token: string;
|
||||||
<Html>
|
metadata: SessionInfo;
|
||||||
<Head />
|
};
|
||||||
<Preview>Деактивация аккаунта</Preview>
|
|
||||||
<Tailwind>
|
|
||||||
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
|
||||||
<Section className="text-center mb-8">
|
|
||||||
<Heading className="text-3xl text-black font-bold">
|
|
||||||
Запрос на деактивацию аккаунта
|
|
||||||
</Heading>
|
|
||||||
<Text className="text-black text-base mt-2">
|
|
||||||
Вы инициировали процесс деактивации вашего аккаунта на платформе
|
|
||||||
{' '}
|
|
||||||
<b>TeaStream</b>
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section className="bg-gray-100 rounded-lg p-6 text-center mb-6">
|
export const DeactivateTemplate = ({ token, metadata }: DeactivateTemplateProps) => (
|
||||||
<Heading className="text-2xl text-black font-semibold">
|
<Html>
|
||||||
Код подтверждения:
|
<Head />
|
||||||
</Heading>
|
|
||||||
<Heading className="text-3xl text-black font-semibold">
|
|
||||||
{token}
|
|
||||||
</Heading>
|
|
||||||
<Text className="text-black">
|
|
||||||
Этот код действителен в течение 5 минут.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section className="bg-gray-100 rounded-lg p-6 mb-6">
|
<Preview>Деактивация аккаунта</Preview>
|
||||||
<Heading
|
|
||||||
className="text-xl font-semibold text-[#18B9AE]"
|
<Tailwind>
|
||||||
|
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
||||||
|
<Section className="text-center mb-8">
|
||||||
|
<Heading className="text-3xl text-black font-bold">
|
||||||
|
Запрос на деактивацию аккаунта
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text className="text-black text-base mt-2">
|
||||||
|
Вы инициировали процесс деактивации вашего аккаунта на платформе
|
||||||
|
{' '}
|
||||||
|
|
||||||
|
<b>TeaStream</b>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="bg-gray-100 rounded-lg p-6 text-center mb-6">
|
||||||
|
<Heading className="text-2xl text-black font-semibold">
|
||||||
|
Код подтверждения:
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Heading className="text-3xl text-black font-semibold">
|
||||||
|
{token}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text className="text-black">
|
||||||
|
Этот код действителен в течение 5 минут.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="bg-gray-100 rounded-lg p-6 mb-6">
|
||||||
|
<Heading
|
||||||
|
className="text-xl font-semibold text-[#18B9AE]"
|
||||||
|
>
|
||||||
|
Информация о запросе:
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<ul className="list-disc list-inside text-black mt-2">
|
||||||
|
<li>
|
||||||
|
🌍 Расположение:
|
||||||
|
{metadata.location.country}
|
||||||
|
,
|
||||||
|
|
||||||
|
{metadata.location.city}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
📱 Операционная система:
|
||||||
|
{metadata.device.os}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
🌐 Браузер:
|
||||||
|
{metadata.device.browser}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
💻 IP-адрес:
|
||||||
|
{metadata.ip}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Text className="text-gray-600 mt-2">
|
||||||
|
Если вы не инициировали этот запрос, пожалуйста, игнорируйте это сообщение.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="text-center mt-8">
|
||||||
|
<Text className="text-gray-600">
|
||||||
|
Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу
|
||||||
|
{' '}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className="text-[#18b9ae] underline"
|
||||||
|
href="mailto:help@teastream.ru"
|
||||||
>
|
>
|
||||||
Информация о запросе:
|
help@teastream.ru
|
||||||
</Heading>
|
</Link>
|
||||||
<ul className="list-disc list-inside text-black mt-2">
|
.
|
||||||
<li>
|
</Text>
|
||||||
🌍 Расположение:
|
</Section>
|
||||||
{metadata.location.country}
|
</Body>
|
||||||
,
|
</Tailwind>
|
||||||
{metadata.location.city}
|
</Html>
|
||||||
</li>
|
);
|
||||||
<li>
|
|
||||||
📱 Операционная система:
|
|
||||||
{metadata.device.os}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
🌐 Браузер:
|
|
||||||
{metadata.device.browser}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
💻 IP-адрес:
|
|
||||||
{metadata.ip}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Text className="text-gray-600 mt-2">
|
|
||||||
Если вы не инициировали этот запрос, пожалуйста, игнорируйте это сообщение.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section className="text-center mt-8">
|
|
||||||
<Text className="text-gray-600">
|
|
||||||
Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу
|
|
||||||
{' '}
|
|
||||||
<Link
|
|
||||||
href="mailto:help@teastream.ru"
|
|
||||||
className="text-[#18b9ae] underline"
|
|
||||||
>
|
|
||||||
help@teastream.ru
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Body>
|
|
||||||
</Tailwind>
|
|
||||||
</Html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,26 +8,29 @@ import {
|
|||||||
Section,
|
Section,
|
||||||
Tailwind,
|
Tailwind,
|
||||||
Text,
|
Text,
|
||||||
} from '@react-email/components'
|
} from '@react-email/components';
|
||||||
import * as React from 'react'
|
import * as React from 'react';
|
||||||
|
|
||||||
interface EnableTwoFactorTemplateProps {
|
type EnableTwoFactorTemplateProps = {
|
||||||
domain: string
|
domain: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps) {
|
export const EnableTwoFactorTemplate = ({ domain }: EnableTwoFactorTemplateProps) => {
|
||||||
const settingsLink = `${domain}/dashboard/settings`
|
const settingsLink = `${domain}/dashboard/settings`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
|
|
||||||
<Preview>Обеспечьте свою безопасность</Preview>
|
<Preview>Обеспечьте свою безопасность</Preview>
|
||||||
|
|
||||||
<Tailwind>
|
<Tailwind>
|
||||||
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
||||||
<Section className="text-center mb-8">
|
<Section className="text-center mb-8">
|
||||||
<Heading className="text-3xl text-black font-bold">
|
<Heading className="text-3xl text-black font-bold">
|
||||||
Защитите свой аккаунт с двухфакторной аутентификацией
|
Защитите свой аккаунт с двухфакторной аутентификацией
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text className="text-black text-base mt-2">
|
<Text className="text-black text-base mt-2">
|
||||||
Включите двухфакторную аутентификацию, чтобы повысить безопасность вашего аккаунта.
|
Включите двухфакторную аутентификацию, чтобы повысить безопасность вашего аккаунта.
|
||||||
</Text>
|
</Text>
|
||||||
@ -37,12 +40,14 @@ export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps
|
|||||||
<Heading className="text-2xl text-black font-semibold">
|
<Heading className="text-2xl text-black font-semibold">
|
||||||
Почему это важно?
|
Почему это важно?
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text className="text-base text-black mt-2">
|
<Text className="text-base text-black mt-2">
|
||||||
Двухфакторная аутентификация добавляет дополнительный уровень защиты, требуя код, который известен только вам.
|
Двухфакторная аутентификация добавляет дополнительный уровень защиты, требуя код, который известен только вам.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={settingsLink}
|
|
||||||
className="inline-flex justify-center items-center rounded-md text-sm font-medium text-white bg-[#18B9AE] px-5 py-2 rounded-full"
|
className="inline-flex justify-center items-center rounded-md text-sm font-medium text-white bg-[#18B9AE] px-5 py-2 rounded-full"
|
||||||
|
href={settingsLink}
|
||||||
>
|
>
|
||||||
Перейти в настройки аккаунта
|
Перейти в настройки аккаунта
|
||||||
</Link>
|
</Link>
|
||||||
@ -52,9 +57,10 @@ export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps
|
|||||||
<Text className="text-gray-600">
|
<Text className="text-gray-600">
|
||||||
Если у вас возникли вопросы, обращайтесь в службу поддержки по адресу
|
Если у вас возникли вопросы, обращайтесь в службу поддержки по адресу
|
||||||
{' '}
|
{' '}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="mailto:help@teastream.ru"
|
|
||||||
className="text-[#18b9ae] underline"
|
className="text-[#18b9ae] underline"
|
||||||
|
href="mailto:help@teastream.ru"
|
||||||
>
|
>
|
||||||
help@teastream.ru
|
help@teastream.ru
|
||||||
</Link>
|
</Link>
|
||||||
@ -64,5 +70,5 @@ export function EnableTwoFactorTemplate({ domain }: EnableTwoFactorTemplateProps
|
|||||||
</Body>
|
</Body>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
</Html>
|
</Html>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export { default as VerificationTemplate } from './verification.template'
|
export { default as VerificationTemplate } from './verification.template';
|
||||||
export { AccountDeletionTemplate } from './account-deletion.template'
|
export { AccountDeletionTemplate } from './account-deletion.template';
|
||||||
export { DeactivateTemplate } from './deactivate.template'
|
export { DeactivateTemplate } from './deactivate.template';
|
||||||
export { EnableTwoFactorTemplate } from './enable-two-factor.template'
|
export { EnableTwoFactorTemplate } from './enable-two-factor.template';
|
||||||
export { default as PasswordRecoveryTemplate } from './password-recovery.template'
|
export { default as PasswordRecoveryTemplate } from './password-recovery.template';
|
||||||
export { VerifyChannelTemplate } from './verify-channel.template'
|
export { VerifyChannelTemplate } from './verify-channel.template';
|
||||||
|
|||||||
@ -1,35 +1,43 @@
|
|||||||
import * as React from 'react'
|
import {
|
||||||
import { Body, Head, Heading, Preview, Section, Tailwind, Text, Link } from '@react-email/components'
|
Body, Head, Heading, Preview, Section, Tailwind, Text, Link,
|
||||||
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
} from '@react-email/components';
|
||||||
import { Html } from '@react-email/html'
|
import { Html } from '@react-email/html';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import type { SessionInfo } from '@/src/shared/types/session-metadata.types';
|
||||||
|
|
||||||
type PasswordRecoveryTemplateProps = {
|
type PasswordRecoveryTemplateProps = {
|
||||||
domain: string
|
domain: string;
|
||||||
token: string
|
token: string;
|
||||||
metadata: SessionInfo
|
metadata: SessionInfo;
|
||||||
}
|
};
|
||||||
|
|
||||||
const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => {
|
const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => {
|
||||||
const { domain, metadata, token } = props
|
const { domain, metadata, token } = props;
|
||||||
const resetLink = `${domain}/account/recovery/${token}`
|
const resetLink = `${domain}/account/recovery/${token}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
|
|
||||||
<Preview>Сброс пароля</Preview>
|
<Preview>Сброс пароля</Preview>
|
||||||
|
|
||||||
<Tailwind>
|
<Tailwind>
|
||||||
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
||||||
<Section className="text-center mb-8">
|
<Section className="text-center mb-8">
|
||||||
<Heading className="text-3xl text-black font-bold">
|
<Heading className="text-3xl text-black font-bold">
|
||||||
Сброс пароля
|
Сброс пароля
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Text className="text-black text-base mt-2">
|
<Text className="text-black text-base mt-2">
|
||||||
Вы запросили сброс пароля для вашей учетной записи.
|
Вы запросили сброс пароля для вашей учетной записи.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="text-black text-base mt-2">
|
<Text className="text-black text-base mt-2">
|
||||||
Чтобы создать новый пароль, нажмите на ссылку ниже:
|
Чтобы создать новый пароль, нажмите на ссылку ниже:
|
||||||
</Text>
|
</Text>
|
||||||
<Link href={resetLink} className="inline-flex justify-center items-center rounded-full text-sm font-medium text-white bg-[#18B9AE] px-5 py-2">
|
|
||||||
|
<Link className="inline-flex justify-center items-center rounded-full text-sm font-medium text-white bg-[#18B9AE] px-5 py-2" href={resetLink}>
|
||||||
Сбросить пароль
|
Сбросить пароль
|
||||||
</Link>
|
</Link>
|
||||||
</Section>
|
</Section>
|
||||||
@ -40,26 +48,32 @@ const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => {
|
|||||||
>
|
>
|
||||||
Информация о запросе:
|
Информация о запросе:
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<ul className="list-disc list-inside text-black mt-2">
|
<ul className="list-disc list-inside text-black mt-2">
|
||||||
<li>
|
<li>
|
||||||
🌍 Расположение:
|
🌍 Расположение:
|
||||||
{metadata.location.country}
|
{metadata.location.country}
|
||||||
,
|
,
|
||||||
|
|
||||||
{metadata.location.city}
|
{metadata.location.city}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
📱 Операционная система:
|
📱 Операционная система:
|
||||||
{metadata.device.os}
|
{metadata.device.os}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
🌐 Браузер:
|
🌐 Браузер:
|
||||||
{metadata.device.browser}
|
{metadata.device.browser}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
💻 IP-адрес:
|
💻 IP-адрес:
|
||||||
{metadata.ip}
|
{metadata.ip}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Text className="text-gray-600 mt-2">
|
<Text className="text-gray-600 mt-2">
|
||||||
Если вы не инициировали этот запрос, пожалуйста, игнорируйте это сообщение.
|
Если вы не инициировали этот запрос, пожалуйста, игнорируйте это сообщение.
|
||||||
</Text>
|
</Text>
|
||||||
@ -69,9 +83,10 @@ const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => {
|
|||||||
<Text className="text-gray-600">
|
<Text className="text-gray-600">
|
||||||
Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу
|
Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу
|
||||||
{' '}
|
{' '}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="mailto:help@teastream.ru"
|
|
||||||
className="text-[#18b9ae] underline"
|
className="text-[#18b9ae] underline"
|
||||||
|
href="mailto:help@teastream.ru"
|
||||||
>
|
>
|
||||||
help@teastream.ru
|
help@teastream.ru
|
||||||
</Link>
|
</Link>
|
||||||
@ -81,7 +96,7 @@ const PasswordRecoveryTemplate = (props: PasswordRecoveryTemplateProps) => {
|
|||||||
</Body>
|
</Body>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
</Html>
|
</Html>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PasswordRecoveryTemplate
|
export default PasswordRecoveryTemplate;
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import * as React from 'react'
|
import {
|
||||||
import { Body, Head, Heading, Link, Preview, Section, Tailwind, Text } from '@react-email/components'
|
Body, Head, Heading, Link, Preview, Section, Tailwind, Text,
|
||||||
import { Html } from '@react-email/html'
|
} from '@react-email/components';
|
||||||
|
import { Html } from '@react-email/html';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
type VerificationTemplateProps = {
|
type VerificationTemplateProps = {
|
||||||
domain: string
|
domain: string;
|
||||||
token: string
|
token: string;
|
||||||
}
|
};
|
||||||
const VerificationTemplate = (props: VerificationTemplateProps) => {
|
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 (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
@ -31,7 +33,7 @@ const VerificationTemplate = (props: VerificationTemplateProps) => {
|
|||||||
Чтобы подтвердить свой адрес электронной почты, перейдите по следующей ссылке
|
Чтобы подтвердить свой адрес электронной почты, перейдите по следующей ссылке
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Link href={verificationLink} className="inline-flex justify-center items-center rounded-full text-sm font-medium text-white bg-[#18B9AE] px-5 py-2">
|
<Link className="inline-flex justify-center items-center rounded-full text-sm font-medium text-white bg-[#18B9AE] px-5 py-2" href={verificationLink}>
|
||||||
Подтвердить почту
|
Подтвердить почту
|
||||||
</Link>
|
</Link>
|
||||||
</Section>
|
</Section>
|
||||||
@ -40,9 +42,10 @@ const VerificationTemplate = (props: VerificationTemplateProps) => {
|
|||||||
<Text className="text-gray-600">
|
<Text className="text-gray-600">
|
||||||
Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу
|
Если у вас есть вопросы или вы столкнулись с трудностями, не стесняйтесь обращаться в нашу службу поддержки по адресу
|
||||||
{' '}
|
{' '}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="mailto:help@teastream.ru"
|
|
||||||
className="text-[#18b9ae] underline"
|
className="text-[#18b9ae] underline"
|
||||||
|
href="mailto:help@teastream.ru"
|
||||||
>
|
>
|
||||||
help@teastream.ru
|
help@teastream.ru
|
||||||
</Link>
|
</Link>
|
||||||
@ -53,7 +56,7 @@ const VerificationTemplate = (props: VerificationTemplateProps) => {
|
|||||||
</Tailwind>
|
</Tailwind>
|
||||||
|
|
||||||
</Html>
|
</Html>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default VerificationTemplate
|
export default VerificationTemplate;
|
||||||
|
|||||||
@ -8,49 +8,52 @@ import {
|
|||||||
Section,
|
Section,
|
||||||
Tailwind,
|
Tailwind,
|
||||||
Text,
|
Text,
|
||||||
} from '@react-email/components'
|
} from '@react-email/components';
|
||||||
import * as React from 'react'
|
import * as React from 'react';
|
||||||
|
|
||||||
export function VerifyChannelTemplate() {
|
export const VerifyChannelTemplate = () => (
|
||||||
return (
|
<Html>
|
||||||
<Html>
|
<Head />
|
||||||
<Head />
|
|
||||||
<Preview>Ваш канал верифицирован</Preview>
|
|
||||||
<Tailwind>
|
|
||||||
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
|
||||||
<Section className="text-center mb-8">
|
|
||||||
<Heading className="text-3xl text-black font-bold">
|
|
||||||
Поздравляем! Ваш канал верифицирован
|
|
||||||
</Heading>
|
|
||||||
<Text className="text-black text-base mt-2">
|
|
||||||
Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section className="bg-white rounded-lg shadow-md p-6 text-center mb-6">
|
<Preview>Ваш канал верифицирован</Preview>
|
||||||
<Heading className="text-2xl text-black font-semibold">
|
|
||||||
Что это значит?
|
|
||||||
</Heading>
|
|
||||||
<Text className="text-base text-black mt-2">
|
|
||||||
Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section className="text-center mt-8">
|
<Tailwind>
|
||||||
<Text className="text-gray-600">
|
<Body className="max-w-2xl mx-auto p-6 bg-slate-50">
|
||||||
Если у вас есть вопросы, напишите нам на
|
<Section className="text-center mb-8">
|
||||||
{' '}
|
<Heading className="text-3xl text-black font-bold">
|
||||||
<Link
|
Поздравляем! Ваш канал верифицирован
|
||||||
href="mailto:help@teastream.ru"
|
</Heading>
|
||||||
className="text-[#18b9ae] underline"
|
|
||||||
>
|
<Text className="text-black text-base mt-2">
|
||||||
help@teastream.ru
|
Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.
|
||||||
</Link>
|
</Text>
|
||||||
.
|
</Section>
|
||||||
</Text>
|
|
||||||
</Section>
|
<Section className="bg-white rounded-lg shadow-md p-6 text-center mb-6">
|
||||||
</Body>
|
<Heading className="text-2xl text-black font-semibold">
|
||||||
</Tailwind>
|
Что это значит?
|
||||||
</Html>
|
</Heading>
|
||||||
)
|
|
||||||
}
|
<Text className="text-base text-black mt-2">
|
||||||
|
Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section className="text-center mt-8">
|
||||||
|
<Text className="text-gray-600">
|
||||||
|
Если у вас есть вопросы, напишите нам на
|
||||||
|
{' '}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className="text-[#18b9ae] underline"
|
||||||
|
href="mailto:help@teastream.ru"
|
||||||
|
>
|
||||||
|
help@teastream.ru
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Global, Module } from '@nestjs/common'
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { StorageService } from './storage.service'
|
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -4,15 +4,17 @@ import {
|
|||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
PutObjectCommandInput,
|
PutObjectCommandInput,
|
||||||
S3Client,
|
S3Client,
|
||||||
} from '@aws-sdk/client-s3'
|
} from '@aws-sdk/client-s3';
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common'
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
import { ProcessEnv } from '../../../shared/types/env';
|
import { ProcessEnv } from '../../../shared/types/env';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageService {
|
export class StorageService {
|
||||||
private readonly client: S3Client
|
private readonly client: S3Client;
|
||||||
private readonly bucket: string
|
|
||||||
|
private readonly bucket: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly configService: ConfigService<ProcessEnv>,
|
private readonly configService: ConfigService<ProcessEnv>,
|
||||||
@ -24,9 +26,9 @@ export class StorageService {
|
|||||||
accessKeyId: this.configService.getOrThrow('S3_ACCESS_KEY_ID'),
|
accessKeyId: this.configService.getOrThrow('S3_ACCESS_KEY_ID'),
|
||||||
secretAccessKey: this.configService.getOrThrow('S3_SECRET_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) {
|
public async upload(buffer: Buffer, key: string, mimetype: string) {
|
||||||
@ -35,28 +37,25 @@ export class StorageService {
|
|||||||
Key: String(key),
|
Key: String(key),
|
||||||
Body: buffer,
|
Body: buffer,
|
||||||
ContentType: mimetype,
|
ContentType: mimetype,
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.client.send(new PutObjectCommand(command))
|
await this.client.send(new PutObjectCommand(command));
|
||||||
}
|
} catch {
|
||||||
catch (e) {
|
throw new BadRequestException('Ошибка при загрузке файла');
|
||||||
throw new BadRequestException('Ошибка при загрузке файла')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(key: string) {
|
public async remove(key: string) {
|
||||||
const command: DeleteObjectCommandInput = {
|
const command: DeleteObjectCommandInput = {
|
||||||
Bucket: this.bucket,
|
Bucket: this.bucket,
|
||||||
Key: String(key),
|
Key: key,
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.client.send(new DeleteObjectCommand(command))
|
await this.client.send(new DeleteObjectCommand(command));
|
||||||
}
|
} catch {
|
||||||
catch (e) {
|
throw new BadRequestException('Ошибка при удалении файла');
|
||||||
console.log('e', e);
|
|
||||||
throw new BadRequestException('Ошибка при удалении файла')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { DynamicModule, Module } from '@nestjs/common'
|
import { DynamicModule, Module } from '@nestjs/common';
|
||||||
import { StripeOptionSymbol, TypeStripeAsyncOptions, TypeStripeOptions } from './types/stripe.type'
|
|
||||||
import { StripeService } from './stripe.service'
|
import { StripeService } from './stripe.service';
|
||||||
|
import { StripeOptionSymbol, TypeStripeAsyncOptions, TypeStripeOptions } from './types/stripe.type';
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class StripeModule {
|
export class StripeModule {
|
||||||
@ -16,7 +17,7 @@ export class StripeModule {
|
|||||||
],
|
],
|
||||||
exports: [StripeService],
|
exports: [StripeService],
|
||||||
global: true,
|
global: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static registerAsync(options: TypeStripeAsyncOptions): DynamicModule {
|
public static registerAsync(options: TypeStripeAsyncOptions): DynamicModule {
|
||||||
@ -33,6 +34,6 @@ export class StripeModule {
|
|||||||
],
|
],
|
||||||
exports: [StripeService],
|
exports: [StripeService],
|
||||||
global: true,
|
global: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { StripeOptionSymbol, TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type'
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Inject, Injectable } from '@nestjs/common'
|
import Stripe from 'stripe';
|
||||||
import Stripe from 'stripe'
|
|
||||||
|
import { StripeOptionSymbol, TypeStripeOptions } from '@/src/module/libs/stripe/types/stripe.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StripeService extends Stripe {
|
export class StripeService extends Stripe {
|
||||||
@ -8,6 +9,6 @@ export class StripeService extends Stripe {
|
|||||||
@Inject(StripeOptionSymbol)
|
@Inject(StripeOptionSymbol)
|
||||||
private readonly options: TypeStripeOptions,
|
private readonly options: TypeStripeOptions,
|
||||||
) {
|
) {
|
||||||
super(options.apiKey, options.config)
|
super(options.apiKey, options.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common'
|
import type { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||||
import Stripe from 'stripe'
|
import type Stripe from 'stripe';
|
||||||
|
|
||||||
export const StripeOptionSymbol = Symbol('StripeOptionSymbol')
|
export const StripeOptionSymbol = Symbol('StripeOptionSymbol');
|
||||||
|
|
||||||
export type TypeStripeOptions = {
|
export type TypeStripeOptions = {
|
||||||
apiKey: string
|
apiKey: string;
|
||||||
config?: Stripe.StripeConfig
|
config?: Stripe.StripeConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TypeStripeAsyncOptions = Pick<ModuleMetadata, 'imports'>
|
export type TypeStripeAsyncOptions = Pick<FactoryProvider<TypeStripeOptions>, 'inject' | 'useFactory'> & Pick<ModuleMetadata, 'imports'>;
|
||||||
& Pick<FactoryProvider<TypeStripeOptions>, 'useFactory' | 'inject'>
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Markup } from 'telegraf'
|
import { Markup } from 'telegraf';
|
||||||
|
|
||||||
export const BUTTONS = {
|
export const BUTTONS = {
|
||||||
authSuccess: Markup.inlineKeyboard([
|
authSuccess: Markup.inlineKeyboard([
|
||||||
@ -14,4 +14,4 @@ export const BUTTONS = {
|
|||||||
'https://teastream.ru/dashboard/settings',
|
'https://teastream.ru/dashboard/settings',
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
import type { SessionInfo } from '@/src/shared/types/session-metadata.types';
|
||||||
import type { SponsorshipPlan, User } from '@prisma/generated'
|
import type { SponsorshipPlan, User } from '@prisma/generated';
|
||||||
|
|
||||||
export const MESSAGES = {
|
export const MESSAGES = {
|
||||||
welcome:
|
welcome:
|
||||||
@ -14,68 +14,61 @@ export const MESSAGES = {
|
|||||||
+ `👥 Количество подписчиков: <b>${followersCount}</b>\n`
|
+ `👥 Количество подписчиков: <b>${followersCount}</b>\n`
|
||||||
+ `📝 О себе: <b>${user.bio ?? 'Не указано'}</b>\n\n`
|
+ `📝 О себе: <b>${user.bio ?? 'Не указано'}</b>\n\n`
|
||||||
+ '🔧 Нажмите на кнопку ниже, чтобы перейти к настройкам профиля.',
|
+ '🔧 Нажмите на кнопку ниже, чтобы перейти к настройкам профиля.',
|
||||||
follows: (user: User) =>
|
follows: (user: User) => `📺 <a href="https://teastream.ru/${user.name}">${user.name}</a>`,
|
||||||
`📺 <a href="https://teastream.ru/${user.name}">${user.name}</a>`,
|
resetPassword: (token: string, metadata: SessionInfo) => '<b>🔒 Сброс пароля</b>\n\n'
|
||||||
resetPassword: (token: string, metadata: SessionInfo) =>
|
+ 'Вы запросили сброс пароля для вашей учетной записи на платформе <b>TeaStream</b>.\n\n'
|
||||||
`<b>🔒 Сброс пароля</b>\n\n`
|
+ 'Чтобы создать новый пароль, пожалуйста, перейдите по следующей ссылке:\n\n'
|
||||||
+ `Вы запросили сброс пароля для вашей учетной записи на платформе <b>TeaStream</b>.\n\n`
|
|
||||||
+ `Чтобы создать новый пароль, пожалуйста, перейдите по следующей ссылке:\n\n`
|
|
||||||
+ `<b><a href="https://teastream.ru/account/recovery/${token}">Сбросить пароль</a></b>\n\n`
|
+ `<b><a href="https://teastream.ru/account/recovery/${token}">Сбросить пароль</a></b>\n\n`
|
||||||
+ `📅 <b>Дата запроса:</b> ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n`
|
+ `📅 <b>Дата запроса:</b> ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n`
|
||||||
+ `🖥️ <b>Информация о запросе:</b>\n\n`
|
+ '🖥️ <b>Информация о запросе:</b>\n\n'
|
||||||
+ `🌍 <b>Расположение:</b> ${metadata.location.country}, ${metadata.location.city}\n`
|
+ `🌍 <b>Расположение:</b> ${metadata.location.country}, ${metadata.location.city}\n`
|
||||||
+ `📱 <b>Операционная система:</b> ${metadata.device.os}\n`
|
+ `📱 <b>Операционная система:</b> ${metadata.device.os}\n`
|
||||||
+ `🌐 <b>Браузер:</b> ${metadata.device.browser}\n`
|
+ `🌐 <b>Браузер:</b> ${metadata.device.browser}\n`
|
||||||
+ `💻 <b>IP-адрес:</b> ${metadata.ip}\n\n`
|
+ `💻 <b>IP-адрес:</b> ${metadata.ip}\n\n`
|
||||||
+ `Если вы не делали этот запрос, просто проигнорируйте это сообщение.\n\n`
|
+ 'Если вы не делали этот запрос, просто проигнорируйте это сообщение.\n\n'
|
||||||
+ `Спасибо за использование <b>TeaStream</b>! 🚀`,
|
+ 'Спасибо за использование <b>TeaStream</b>! 🚀',
|
||||||
deactivate: (token: string, metadata: SessionInfo) =>
|
deactivate: (token: string, metadata: SessionInfo) => '<b>⚠️ Запрос на деактивацию аккаунта</b>\n\n'
|
||||||
`<b>⚠️ Запрос на деактивацию аккаунта</b>\n\n`
|
+ 'Вы инициировали процесс деактивации вашего аккаунта на платформе <b>Teastream</b>.\n\n'
|
||||||
+ `Вы инициировали процесс деактивации вашего аккаунта на платформе <b>Teastream</b>.\n\n`
|
+ 'Для завершения операции, пожалуйста, подтвердите свой запрос, введя следующий код подтверждения:\n\n'
|
||||||
+ `Для завершения операции, пожалуйста, подтвердите свой запрос, введя следующий код подтверждения:\n\n`
|
|
||||||
+ `<b>Код подтверждения: ${token}</b>\n\n`
|
+ `<b>Код подтверждения: ${token}</b>\n\n`
|
||||||
+ `📅 <b>Дата запроса:</b> ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n`
|
+ `📅 <b>Дата запроса:</b> ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n`
|
||||||
+ `🖥️ <b>Информация о запросе:</b>\n\n`
|
+ '🖥️ <b>Информация о запросе:</b>\n\n'
|
||||||
+ `• 🌍 <b>Расположение:</b> ${metadata.location.country}, ${metadata.location.city}\n`
|
+ `• 🌍 <b>Расположение:</b> ${metadata.location.country}, ${metadata.location.city}\n`
|
||||||
+ `• 📱 <b>Операционная система:</b> ${metadata.device.os}\n`
|
+ `• 📱 <b>Операционная система:</b> ${metadata.device.os}\n`
|
||||||
+ `• 🌐 <b>Браузер:</b> ${metadata.device.browser}\n`
|
+ `• 🌐 <b>Браузер:</b> ${metadata.device.browser}\n`
|
||||||
+ `• 💻 <b>IP-адрес:</b> ${metadata.ip}\n\n`
|
+ `• 💻 <b>IP-адрес:</b> ${metadata.ip}\n\n`
|
||||||
+ `<b>Что произойдет после деактивации?</b>\n\n`
|
+ '<b>Что произойдет после деактивации?</b>\n\n'
|
||||||
+ `1. Вы автоматически выйдете из системы и потеряете доступ к аккаунту.\n`
|
+ '1. Вы автоматически выйдете из системы и потеряете доступ к аккаунту.\n'
|
||||||
+ `2. Если вы не отмените деактивацию в течение 7 дней, ваш аккаунт будет <b>безвозвратно удален</b> со всей вашей информацией, данными и подписками.\n\n`
|
+ '2. Если вы не отмените деактивацию в течение 7 дней, ваш аккаунт будет <b>безвозвратно удален</b> со всей вашей информацией, данными и подписками.\n\n'
|
||||||
+ `<b>⏳ Обратите внимание:</b> Если в течение 7 дней вы передумаете, вы можете обратиться в нашу поддержку для восстановления доступа к вашему аккаунту до момента его полного удаления.\n\n`
|
+ '<b>⏳ Обратите внимание:</b> Если в течение 7 дней вы передумаете, вы можете обратиться в нашу поддержку для восстановления доступа к вашему аккаунту до момента его полного удаления.\n\n'
|
||||||
+ `После удаления аккаунта восстановить его будет невозможно, и все данные будут потеряны без возможности восстановления.\n\n`
|
+ 'После удаления аккаунта восстановить его будет невозможно, и все данные будут потеряны без возможности восстановления.\n\n'
|
||||||
+ `Если вы передумали, просто проигнорируйте это сообщение. Ваш аккаунт останется активным.\n\n`
|
+ 'Если вы передумали, просто проигнорируйте это сообщение. Ваш аккаунт останется активным.\n\n'
|
||||||
+ `Спасибо, что пользуетесь <b>TeaStream</b>! Мы всегда рады видеть вас на нашей платформе и надеемся, что вы останетесь с нами. 🚀\n\n`
|
+ 'Спасибо, что пользуетесь <b>TeaStream</b>! Мы всегда рады видеть вас на нашей платформе и надеемся, что вы останетесь с нами. 🚀\n\n'
|
||||||
+ `С уважением,\n`
|
+ 'С уважением,\n'
|
||||||
+ `Команда TeaStream`,
|
+ 'Команда TeaStream',
|
||||||
accountDeleted:
|
accountDeleted:
|
||||||
`<b>⚠️ Ваш аккаунт был полностью удалён.</b>\n\n`
|
'<b>⚠️ Ваш аккаунт был полностью удалён.</b>\n\n'
|
||||||
+ `Ваш аккаунт был полностью стерт из базы данных Teastream. Все ваши данные и информация были удалены безвозвратно. ❌\n\n`
|
+ 'Ваш аккаунт был полностью стерт из базы данных Teastream. Все ваши данные и информация были удалены безвозвратно. ❌\n\n'
|
||||||
+ `🔒 Вы больше не будете получать уведомления в Telegram и на почту.\n\n`
|
+ '🔒 Вы больше не будете получать уведомления в Telegram и на почту.\n\n'
|
||||||
+ `Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке:\n`
|
+ 'Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке:\n'
|
||||||
+ `<b><a href="https://teastream.ru/account/create">Зарегистрироваться на Teastream</a></b>\n\n`
|
+ '<b><a href="https://teastream.ru/account/create">Зарегистрироваться на Teastream</a></b>\n\n'
|
||||||
+ `Спасибо, что были с нами! Мы всегда будем рады видеть вас на платформе. 🚀\n\n`
|
+ 'Спасибо, что были с нами! Мы всегда будем рады видеть вас на платформе. 🚀\n\n'
|
||||||
+ `С уважением,\n`
|
+ 'С уважением,\n'
|
||||||
+ `Команда TeaStream`,
|
+ 'Команда TeaStream',
|
||||||
streamStart: (channel: User) =>
|
streamStart: (channel: User) => `<b>📡 На канале ${channel.displayName} началась трансляция!</b>\n\n`
|
||||||
`<b>📡 На канале ${channel.displayName} началась трансляция!</b>\n\n`
|
|
||||||
+ `Смотрите здесь: <a href="https://teastream.ru/${channel.name}">Перейти к трансляции</a>`,
|
+ `Смотрите здесь: <a href="https://teastream.ru/${channel.name}">Перейти к трансляции</a>`,
|
||||||
newFollowing: (follower: User, followersCount: number) =>
|
newFollowing: (follower: User, followersCount: number) => `<b>У вас новый подписчик!</b>\n\nЭто пользователь <a href="https://teastream.ru/${follower.name}">${follower.displayName}</a>\n\nИтоговое количество подписчиков на вашем канале: ${followersCount}`,
|
||||||
|
|
||||||
`<b>У вас новый подписчик!</b>\n\nЭто пользователь <a href="https://teastream.ru/${follower.name}">${follower.displayName}</a>\n\nИтоговое количество подписчиков на вашем канале: ${followersCount}`,
|
|
||||||
enableTwoFactor:
|
enableTwoFactor:
|
||||||
`🔐 Обеспечьте свою безопасность!\n\n`
|
'🔐 Обеспечьте свою безопасность!\n\n'
|
||||||
+ `Включите двухфакторную аутентификацию в <a href="https://teastream.ru/dashboard/settings">настройках аккаунта</a>.`,
|
+ 'Включите двухфакторную аутентификацию в <a href="https://teastream.ru/dashboard/settings">настройках аккаунта</a>.',
|
||||||
verifyChannel:
|
verifyChannel:
|
||||||
`<b>🎉 Поздравляем! Ваш канал верифицирован</b>\n\n`
|
'<b>🎉 Поздравляем! Ваш канал верифицирован</b>\n\n'
|
||||||
+ `Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.\n\n`
|
+ 'Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.\n\n'
|
||||||
+ `Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.\n\n`
|
+ 'Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.\n\n'
|
||||||
+ `Спасибо, что вы с нами и продолжаете развивать свой канал вместе с TeaStream!`,
|
+ 'Спасибо, что вы с нами и продолжаете развивать свой канал вместе с TeaStream!',
|
||||||
newSponsorship: (plan: SponsorshipPlan, sponsor: User) =>
|
newSponsorship: (plan: SponsorshipPlan, sponsor: User) => '<b>🎉 Новое спонсор!</b>\n\n'
|
||||||
`<b>🎉 Новое спонсор!</b>\n\n`
|
|
||||||
+ `Вы получили новое спонсорство на план <b>${plan.title}</b>.\n`
|
+ `Вы получили новое спонсорство на план <b>${plan.title}</b>.\n`
|
||||||
+ `💰 Сумма: <b>${plan.price} ₽</b>\n`
|
+ `💰 Сумма: <b>${plan.price} ₽</b>\n`
|
||||||
+ `👤 Спонсор: <a href="https://teastream.ru/${sponsor.name}">${sponsor.displayName}</a>\n`
|
+ `👤 Спонсор: <a href="https://teastream.ru/${sponsor.name}">${sponsor.displayName}</a>\n`
|
||||||
+ `📅 Дата оформления: <b>${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}</b>`,
|
+ `📅 Дата оформления: <b>${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}</b>`,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { getTelegrafOptions } from '@/src/core/config/telegraf.config'
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { Global, Module } from '@nestjs/common'
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config'
|
import { TelegrafModule } from 'nestjs-telegraf';
|
||||||
import { TelegrafModule } from 'nestjs-telegraf'
|
|
||||||
import { TelegramService } from './telegram.service'
|
import { getTelegrafOptions } from '@/src/core/config/telegraf.config';
|
||||||
|
|
||||||
|
import { TelegramService } from './telegram.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@ -1,37 +1,37 @@
|
|||||||
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ConfigService } from '@nestjs/config'
|
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
Command, Ctx, Start, Update,
|
Command, Ctx, Start, Update,
|
||||||
} from 'nestjs-telegraf'
|
} from 'nestjs-telegraf';
|
||||||
import { Context, Telegraf } from 'telegraf'
|
import { Context, Telegraf } from 'telegraf';
|
||||||
|
|
||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { PrismaService } from '@/src/core/prisma/prisma.service';
|
||||||
import { BUTTONS } from '@/src/module/libs/telegram/telegram.button'
|
import { BUTTONS } from '@/src/module/libs/telegram/telegram.button';
|
||||||
import { MESSAGES } from '@/src/module/libs/telegram/telegram.message'
|
import { MESSAGES } from '@/src/module/libs/telegram/telegram.message';
|
||||||
import { SponsorshipPlan, TokenType, User } from '@prisma/generated'
|
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()
|
@Update()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TelegramService extends Telegraf {
|
export class TelegramService extends Telegraf {
|
||||||
private readonly _token: string
|
private readonly _token: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly configService: ConfigService<ProcessEnv>,
|
private readonly configService: ConfigService<ProcessEnv>,
|
||||||
) {
|
) {
|
||||||
super(configService.getOrThrow('TELEGRAM_BOT_TOKEN'))
|
super(configService.getOrThrow('TELEGRAM_BOT_TOKEN'));
|
||||||
this._token = configService.getOrThrow('TELEGRAM_BOT_TOKEN')
|
this._token = configService.getOrThrow('TELEGRAM_BOT_TOKEN');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Start()
|
@Start()
|
||||||
public async onStart(@Ctx() ctx: Context) {
|
public async onStart(@Ctx() ctx: Context) {
|
||||||
const chatId = ctx.chat?.id.toString()
|
const chatId = ctx.chat?.id.toString();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const token = ctx.message.text.split(' ')[1] as string
|
const token = ctx.message.text.split(' ')[1] as string;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const authToken = await this.prismaService.token.findUnique({
|
const authToken = await this.prismaService.token.findUnique({
|
||||||
@ -39,124 +39,130 @@ export class TelegramService extends Telegraf {
|
|||||||
token,
|
token,
|
||||||
type: TokenType.TELEGRAM_AUTH,
|
type: TokenType.TELEGRAM_AUTH,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!authToken?.userId) {
|
if (!authToken?.userId) {
|
||||||
await ctx.reply('Токен не найден')
|
await ctx.reply('Токен не найден');
|
||||||
return
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasExpired = new Date(authToken.expiresIn) < new Date()
|
const hasExpired = new Date(authToken.expiresIn) < new Date();
|
||||||
if (hasExpired) {
|
if (hasExpired) {
|
||||||
await ctx.reply(MESSAGES.invalidToken)
|
await ctx.reply(MESSAGES.invalidToken);
|
||||||
return
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.connectTelegram(authToken.userId, chatId!)
|
await this.connectTelegram(authToken.userId, chatId!);
|
||||||
await this.prismaService.token.delete({
|
await this.prismaService.token.delete({
|
||||||
where: { id: authToken.id },
|
where: { id: authToken.id },
|
||||||
})
|
});
|
||||||
|
|
||||||
await ctx.replyWithHTML(MESSAGES.authSuccess, BUTTONS.authSuccess)
|
await ctx.replyWithHTML(MESSAGES.authSuccess, BUTTONS.authSuccess);
|
||||||
return
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.findUserByChatId(chatId!)
|
const user = await this.findUserByChatId(chatId!);
|
||||||
if (user) {
|
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')
|
@Command('me')
|
||||||
@Action('me')
|
@Action('me')
|
||||||
public async onMe(@Ctx() ctx: Context) {
|
public async onMe(@Ctx() ctx: Context) {
|
||||||
const chatId = ctx.chat?.id.toString()
|
const chatId = ctx.chat?.id.toString();
|
||||||
if (typeof chatId === 'undefined') {
|
if (typeof chatId === 'undefined') {
|
||||||
throw new NotFoundException('Пользователь не найден')
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.findUserByChatId(chatId)
|
const user = await this.findUserByChatId(chatId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('Пользователь не найден')
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
const followersCount = await this.prismaService.follow.count({
|
const followersCount = await this.prismaService.follow.count({
|
||||||
where: { followingId: user.id },
|
where: { followingId: user.id },
|
||||||
})
|
});
|
||||||
|
|
||||||
await ctx.replyWithHTML(MESSAGES.profile(user, followersCount), BUTTONS.profile)
|
await ctx.replyWithHTML(MESSAGES.profile(user, followersCount), BUTTONS.profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Command('follows')
|
@Command('follows')
|
||||||
@Action('follows')
|
@Action('follows')
|
||||||
public async onFollow(@Ctx() ctx: Context) {
|
public async onFollow(@Ctx() ctx: Context) {
|
||||||
const chatId = ctx.chat?.id
|
const chatId = ctx.chat?.id;
|
||||||
if (typeof chatId === 'undefined') {
|
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) {
|
if (!user) {
|
||||||
throw new NotFoundException('Пользователь не найден')
|
throw new NotFoundException('Пользователь не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
const follows = await this.prismaService.follow.findMany({
|
const follows = await this.prismaService.follow.findMany({
|
||||||
where: { followerId: user.id },
|
where: { followerId: user.id },
|
||||||
include: { following: true },
|
include: { following: true },
|
||||||
})
|
});
|
||||||
|
|
||||||
if (follows.length > 0) {
|
if (follows.length > 0) {
|
||||||
const followList = follows.map(follow => MESSAGES.follows(follow.following)).join('\n')
|
const followList = follows.map((follow) => MESSAGES.follows(follow.following)).join('\n');
|
||||||
const message = `<b>Каналы, на которые Вы подписаны</b>\n\n${followList}`
|
const message = `<b>Каналы, на которые Вы подписаны</b>\n\n${followList}`;
|
||||||
await ctx.replyWithHTML(message)
|
await ctx.replyWithHTML(message);
|
||||||
return
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.replyWithHTML('❌ <b>У Вас нет подписок</b>')
|
await ctx.replyWithHTML('❌ <b>У Вас нет подписок</b>');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendPasswordResetToken(chatId: string, token: string, metadata: SessionInfo) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
public async sendNewFollowing(chatId: string, follower: User) {
|
||||||
const user = await this.findUserByChatId(chatId)
|
const user = await this.findUserByChatId(chatId);
|
||||||
if (!user) {
|
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) {
|
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) {
|
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) {
|
private async connectTelegram(userId: string, chatId: string) {
|
||||||
return this.prismaService.user.update({
|
return this.prismaService.user.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: { telegramId: chatId },
|
data: { telegramId: chatId },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findUserByChatId(chatId: string) {
|
private async findUserByChatId(chatId: string) {
|
||||||
@ -166,6 +172,6 @@ export class TelegramService extends Telegraf {
|
|||||||
followings: true,
|
followings: true,
|
||||||
followers: true,
|
followers: true,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
backend/src/module/notification/index.ts
Normal file
2
backend/src/module/notification/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { NotificationSettingsModel, ChangeNotificationsSettingsResponse } from './models/notification-settings.model';
|
||||||
|
export { NotificationModel } from './models/notification.model';
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql'
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { IsBoolean } from 'class-validator'
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class ChangeNotificationSettingsInput {
|
export class ChangeNotificationSettingsInput {
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
public siteNotifications: boolean
|
public siteNotifications: boolean;
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
public telegramNotifications: boolean
|
public telegramNotifications: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user