add email verification
This commit is contained in:
parent
f6ea096842
commit
7c297879f9
@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/server": "^4.12.2",
|
"@apollo/server": "^4.12.2",
|
||||||
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
"@nestjs/apollo": "^13.1.0",
|
"@nestjs/apollo": "^13.1.0",
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
@ -31,6 +32,9 @@
|
|||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@prisma/client": "^6.10.1",
|
"@prisma/client": "^6.10.1",
|
||||||
|
"@react-email/components": "^0.1.1",
|
||||||
|
"@react-email/html": "^0.0.11",
|
||||||
|
"@react-email/tailwind": "^1.0.5",
|
||||||
"argon2": "^0.43.0",
|
"argon2": "^0.43.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
@ -43,6 +47,8 @@
|
|||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
"prisma": "^6.10.1",
|
"prisma": "^6.10.1",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
@ -61,7 +67,9 @@
|
|||||||
"@types/geoip-lite": "^1.4.4",
|
"@types/geoip-lite": "^1.4.4",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
|||||||
@ -16,8 +16,30 @@ model User {
|
|||||||
displayName String @map("display_name")
|
displayName String @map("display_name")
|
||||||
avatar String?
|
avatar String?
|
||||||
bio String?
|
bio String?
|
||||||
|
token Token[]
|
||||||
|
isVerified Boolean @default(false) @map("is_verified")
|
||||||
|
isEmailVerified Boolean @default(false) @map("is_email_verified")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Token {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
token String @unique
|
||||||
|
type TokenType
|
||||||
|
expiresIn DateTime @map("expires_in")
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String? @map("user_id")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TokenType {
|
||||||
|
EMAIL_VERIFY
|
||||||
|
|
||||||
|
@@map("token_types")
|
||||||
|
}
|
||||||
|
|||||||
19
backend/src/core/config/mailer.config.ts
Normal file
19
backend/src/core/config/mailer.config.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { MailerOptions } from '@nestjs-modules/mailer'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
|
||||||
|
export function getMailConfig(configService: ConfigService): MailerOptions {
|
||||||
|
return {
|
||||||
|
transport: {
|
||||||
|
host: configService.getOrThrow<string>('MAIL_HOST'),
|
||||||
|
port: configService.getOrThrow<number>('MAIL_PORT'),
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: configService.getOrThrow<string>('MAIL_LOGIN'),
|
||||||
|
pass: configService.getOrThrow<string>('MAIL_PASSWORD'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
from: `"TeaStream" ${configService.getOrThrow<string>('MAIL_LOGIN')}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { getGraphQLConfig } from '@/src/core/config/graphql.config'
|
import { getGraphQLConfig } from '@/src/core/config/graphql.config'
|
||||||
import { AccountModule } from '@/src/module/auth/account/account.module'
|
import { AccountModule } from '@/src/module/auth/account/account.module'
|
||||||
import { SessionModule } from '@/src/module/auth/session/session.module'
|
import { SessionModule } from '@/src/module/auth/session/session.module'
|
||||||
|
import { VerificationModule } from '@/src/module/auth/verification/verification.module'
|
||||||
|
import { MailModule } from '@/src/module/libs/mail/mail.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 { ApolloDriver } from '@nestjs/apollo'
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
@ -23,8 +25,10 @@ import { RedisModule } from './redis/redis.module'
|
|||||||
}),
|
}),
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
|
MailModule,
|
||||||
AccountModule,
|
AccountModule,
|
||||||
SessionModule,
|
SessionModule,
|
||||||
|
VerificationModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {}
|
export class CoreModule {}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ type Mutation {
|
|||||||
loginUser(data: LoginInput!): UserModel!
|
loginUser(data: LoginInput!): UserModel!
|
||||||
logoutUser: Boolean!
|
logoutUser: Boolean!
|
||||||
removeSession(id: String!): Boolean!
|
removeSession(id: String!): Boolean!
|
||||||
|
verifyAccount(data: VerificationInput!): UserModel!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
@ -65,7 +66,13 @@ type UserModel {
|
|||||||
displayName: String!
|
displayName: String!
|
||||||
email: String!
|
email: String!
|
||||||
id: ID!
|
id: ID!
|
||||||
|
isEmailVerified: Boolean!
|
||||||
|
isVerified: Boolean!
|
||||||
name: String!
|
name: String!
|
||||||
password: String!
|
password: String!
|
||||||
updatedAt: DateTime!
|
updatedAt: DateTime!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input VerificationInput {
|
||||||
|
token: String!
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
|
import { VerificationService } from '@/src/module/auth/verification/verification.service';
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { AccountService } from './account.service'
|
import { AccountService } from './account.service'
|
||||||
import { AccountResolver } from './account.resolver'
|
import { AccountResolver } from './account.resolver'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [AccountResolver, AccountService],
|
providers: [AccountResolver, AccountService, VerificationService],
|
||||||
})
|
})
|
||||||
export class AccountModule {}
|
export class AccountModule {}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { User } from '@/prisma/generated'
|
import { User } from '@/prisma/generated'
|
||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||||
import { CreateUserInput } from '@/src/module/auth/account/inputs/create-user.input'
|
import { CreateUserInput } from '@/src/module/auth/account/inputs/create-user.input'
|
||||||
|
import { VerificationService } from '@/src/module/auth/verification/verification.service'
|
||||||
import { ConflictException, Injectable } from '@nestjs/common'
|
import { ConflictException, Injectable } from '@nestjs/common'
|
||||||
import { hash } from 'argon2'
|
import { hash } from 'argon2'
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ import { hash } from 'argon2'
|
|||||||
export class AccountService {
|
export class AccountService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly verificationService: VerificationService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ export class AccountService {
|
|||||||
throw new ConflictException('Пользователь с такоей почтой уже существует')
|
throw new ConflictException('Пользователь с такоей почтой уже существует')
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prismaService.user.create({
|
const user = await this.prismaService.user.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
@ -49,5 +51,9 @@ export class AccountService {
|
|||||||
displayName: name,
|
displayName: name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await this.verificationService.sendVerificationToken(user)
|
||||||
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,12 @@ export class UserModel implements User {
|
|||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
bio: string
|
bio: string
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
isEmailVerified: boolean
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
isVerified: boolean
|
||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
|
import { VerificationService } from '@/src/module/auth/verification/verification.service';
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common'
|
||||||
import { SessionService } from './session.service'
|
import { SessionService } from './session.service'
|
||||||
import { SessionResolver } from './session.resolver'
|
import { SessionResolver } from './session.resolver'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [SessionResolver, SessionService],
|
providers: [SessionResolver, SessionService, VerificationService],
|
||||||
})
|
})
|
||||||
export class SessionModule {}
|
export class SessionModule {}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||||
import { RedisService } from '@/src/core/redis/redis.service'
|
import { RedisService } from '@/src/core/redis/redis.service'
|
||||||
import { LoginInput } from '@/src/module/auth/session/inputs/login.input'
|
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 { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
|
||||||
|
import { destroySession, saveSession } from '@/src/shared/util/session.util'
|
||||||
import {
|
import {
|
||||||
|
BadRequestException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
@ -20,6 +22,7 @@ export class SessionService {
|
|||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly redisService: RedisService,
|
private readonly redisService: RedisService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
|
private readonly verificationService: VerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async findByUser(req: Request) {
|
public async findByUser(req: Request) {
|
||||||
@ -89,32 +92,16 @@ export class SessionService {
|
|||||||
throw new UnauthorizedException('Логин или пароль неверный')
|
throw new UnauthorizedException('Логин или пароль неверный')
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
if (!user.isEmailVerified) {
|
||||||
req.session.userId = user.id
|
await this.verificationService.sendVerificationToken(user)
|
||||||
req.session.createdAt = new Date()
|
throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения')
|
||||||
req.session.metadata = getSessionMetadata(req, userAgent)
|
|
||||||
|
|
||||||
req.session.save((error) => {
|
|
||||||
if (error) {
|
|
||||||
reject(new InternalServerErrorException('Не удалось сохранить сессию'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(user)
|
return saveSession(req, user, getSessionMetadata(req, userAgent))
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout(req: Request) {
|
public async logout(req: Request) {
|
||||||
return new Promise((resolve, reject) => {
|
return destroySession(req, this.configService)
|
||||||
req.session.destroy((error) => {
|
|
||||||
if (error) {
|
|
||||||
reject(new InternalServerErrorException('Не удалось завершить сессию'))
|
|
||||||
}
|
|
||||||
|
|
||||||
req.res?.clearCookie(this.configService.getOrThrow('SESSION_NAME'))
|
|
||||||
resolve(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearSession(req: Request) {
|
public clearSession(req: Request) {
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql'
|
||||||
|
import { IsNotEmpty, IsUUID } from 'class-validator'
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class VerificationInput {
|
||||||
|
@Field(() => String)
|
||||||
|
@IsUUID('4')
|
||||||
|
@IsNotEmpty()
|
||||||
|
public token: string
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { VerificationService } from './verification.service'
|
||||||
|
import { VerificationResolver } from './verification.resolver'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [VerificationResolver, VerificationService],
|
||||||
|
})
|
||||||
|
export class VerificationModule {}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
||||||
|
import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input'
|
||||||
|
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'
|
||||||
|
import { GqlContext } from '@/src/shared/types/gql-context.types'
|
||||||
|
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql'
|
||||||
|
import { VerificationService } from './verification.service'
|
||||||
|
|
||||||
|
@Resolver('Verification')
|
||||||
|
export class VerificationResolver {
|
||||||
|
constructor(private readonly verificationService: VerificationService) {}
|
||||||
|
|
||||||
|
@Mutation(() => UserModel, { name: 'verifyAccount' })
|
||||||
|
public async verify(
|
||||||
|
@Context() { req }: GqlContext,
|
||||||
|
@Args('data') input: VerificationInput,
|
||||||
|
@UserAgent() userAgent: string,
|
||||||
|
) {
|
||||||
|
return this.verificationService.verify(req, input, userAgent)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
backend/src/module/auth/verification/verification.service.ts
Normal file
66
backend/src/module/auth/verification/verification.service.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||||
|
import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input'
|
||||||
|
import { MailService } from '@/src/module/libs/mail/mail.service'
|
||||||
|
import { generateToken } from '@/src/shared/util/generate-token.util'
|
||||||
|
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
|
||||||
|
import { saveSession } from '@/src/shared/util/session.util'
|
||||||
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
||||||
|
import { TokenType, User } from '@prisma/generated'
|
||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VerificationService {
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly mailService: MailService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verify(req: Request, input: VerificationInput, userAgent: string) {
|
||||||
|
const { token } = input
|
||||||
|
const existingToken = await this.prismaService.token.findUnique({
|
||||||
|
where: { token, type: TokenType.EMAIL_VERIFY },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingToken) {
|
||||||
|
throw new NotFoundException('Токен не найден')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExpired = new Date(existingToken.expiresIn) < new Date()
|
||||||
|
|
||||||
|
if (hasExpired) {
|
||||||
|
throw new BadRequestException('Токен истек')
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.prismaService.user.update({
|
||||||
|
where: {
|
||||||
|
id: existingToken.userId!, // todo fixme
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isEmailVerified: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.prismaService.token.delete({
|
||||||
|
where: {
|
||||||
|
id: existingToken.id,
|
||||||
|
type: TokenType.EMAIL_VERIFY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return saveSession(req, user, getSessionMetadata(req, userAgent))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendVerificationToken(user: User) {
|
||||||
|
const verificationToken = await generateToken(
|
||||||
|
this.prismaService,
|
||||||
|
user,
|
||||||
|
TokenType.EMAIL_VERIFY,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.mailService.sendVerificationToken(user.email, verificationToken.token);
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/src/module/libs/mail/mail.module.ts
Normal file
17
backend/src/module/libs/mail/mail.module.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { getMailConfig } from '@/src/core/config/mailer.config'
|
||||||
|
import { MailerModule } from '@nestjs-modules/mailer'
|
||||||
|
import { Global, Module } from '@nestjs/common'
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config'
|
||||||
|
import { MailService } from './mail.service'
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
imports: [MailerModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: getMailConfig,
|
||||||
|
inject: [ConfigService],
|
||||||
|
})],
|
||||||
|
providers: [MailService],
|
||||||
|
exports: [MailService],
|
||||||
|
})
|
||||||
|
export class MailModule {}
|
||||||
30
backend/src/module/libs/mail/mail.service.ts
Normal file
30
backend/src/module/libs/mail/mail.service.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Token } from '@prisma/generated'
|
||||||
|
import VerificationTemplate from './templates/verification.template'
|
||||||
|
import { MailerService } from '@nestjs-modules/mailer'
|
||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { render } from '@react-email/components'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly mailerService: MailerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async sendVerificationToken(email: string, token: Token['token']) {
|
||||||
|
const domain = this.configService.getOrThrow<string>('ALLOWED_ORIGIN')
|
||||||
|
const html = await render(VerificationTemplate({ domain, token }))
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- todo fixme
|
||||||
|
return this.sendMail(email, 'Верификация аккаунта', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMail(email: string, subject: string, html: string) {
|
||||||
|
return this.mailerService.sendMail({
|
||||||
|
to: email,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { Body, Head, Heading, Link, Preview, Section, Tailwind, Text } from '@react-email/components'
|
||||||
|
import { Html } from '@react-email/html'
|
||||||
|
|
||||||
|
type VerificationTemplateProps = {
|
||||||
|
domain: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
const VerificationTemplate = (props: VerificationTemplateProps) => {
|
||||||
|
const { domain, token } = props
|
||||||
|
|
||||||
|
const verificationLink = `${domain}/account/verify?token=${token}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<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-3-xl text-black font-bold">
|
||||||
|
Подтверждение вашей почты
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text className="text-base text-black">
|
||||||
|
Спасибо за регистрацию.
|
||||||
|
Чтобы подтвердить свой адрес электронной почты, перейдите по следующей ссылке
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VerificationTemplate
|
||||||
38
backend/src/shared/util/generate-token.util.ts
Normal file
38
backend/src/shared/util/generate-token.util.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||||
|
import { $Enums, User } from '@prisma/generated'
|
||||||
|
import TokenType = $Enums.TokenType
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export async function generateToken(prismaService: PrismaService, user: User, type: TokenType, isUUID: boolean = false) {
|
||||||
|
const token = isUUID
|
||||||
|
? uuidv4()
|
||||||
|
: Math.floor(Math.random() * (1_000_000 - 100_000) + 100_000).toString()
|
||||||
|
|
||||||
|
const expiresIn = new Date(new Date().getTime() + 15 * 60 * 1000)
|
||||||
|
const existingToken = await prismaService.token.findFirst({
|
||||||
|
where: {
|
||||||
|
type,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingToken) {
|
||||||
|
await prismaService.token.delete({ where: { id: existingToken.id } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return prismaService.token.create({
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
expiresIn,
|
||||||
|
type,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
34
backend/src/shared/util/session.util.ts
Normal file
34
backend/src/shared/util/session.util.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
|
||||||
|
import { InternalServerErrorException } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { User } from '@prisma/generated'
|
||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
export function saveSession(req: Request, user: User, metadata: SessionInfo) {
|
||||||
|
return new Promise<User>((resolve, reject) => {
|
||||||
|
req.session.createdAt = new Date()
|
||||||
|
req.session.userId = user.id
|
||||||
|
req.session.metadata = metadata
|
||||||
|
|
||||||
|
req.session.save((error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(new InternalServerErrorException('Не удалось сохранить сессию'))
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(user)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroySession(req: Request, configService: ConfigService) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req.session.destroy((error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(new InternalServerErrorException('Не удалось завершить сессию'))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.res?.clearCookie(configService.getOrThrow('SESSION_NAME'))
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"jsx": "react"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1754
backend/yarn.lock
1754
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user