Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
11d73cf
feat(api): добавлен mail module
Khimish009 Mar 15, 2026
6e88eec
feat(api): добавлен mail provider
Khimish009 Mar 15, 2026
e564f79
feat(api): добавлен mail service
Khimish009 Mar 15, 2026
96dcf5e
feat(api): добавлен mail types
Khimish009 Mar 15, 2026
b973075
feat(api): добавлен токен MAIL_PROVIDER
Khimish009 Mar 15, 2026
a98560f
feat(api): добавлен BrevoMailProvider
Khimish009 Mar 15, 2026
aef8c76
chore(api): обновлён MailModule
Khimish009 Mar 15, 2026
51ffc1d
chore(api): добавлен метод sendWelcomeEmail в класс MailService
Khimish009 Mar 16, 2026
bbb6f13
feat(api): в метод регистрации добавлена отправка welcome письма
Khimish009 Mar 16, 2026
f5a8f7e
chore(api): добавлен Logger в метод register
Khimish009 Mar 16, 2026
561123f
feat(api): создан getMailConfig
Khimish009 Mar 16, 2026
d3857ff
feat(api): установлены пакеты @nestjs-modules/mailer и nodemailer
Khimish009 Mar 16, 2026
3b95d81
feat(api): добавлен MailerModule в MailModule
Khimish009 Mar 16, 2026
288d5d6
feat(api): обвновлён метод send класса BrevoMailProvider
Khimish009 Mar 16, 2026
691ad9e
chore(api): переименовано название smtp провайдера
Khimish009 Mar 16, 2026
10238e7
chore(api): обновлён env.example
Khimish009 Mar 16, 2026
bc8fe11
chore(api): обновлён env.example
Khimish009 Mar 18, 2026
67a8dc5
chore(api): добавлен пакет resend
Khimish009 Mar 18, 2026
e1e9aea
chore(api): исправлено определение logger
Khimish009 Mar 18, 2026
7e4490c
chore(api): добавлен ResendMailProvider
Khimish009 Mar 18, 2026
f4b08e5
chore(api): добавлены пакеты @react-email/render, react, react-dom
Khimish009 Mar 18, 2026
3cadf24
chore(api): добавлены пакеты @react-email/components @types/react
Khimish009 Mar 18, 2026
e84c9d0
chore(api): добавлен WelcomeEmail template
Khimish009 Mar 18, 2026
733dbd7
chore(api): удалены пакеты @nestjs-modules/mailer и nodemailer
Khimish009 Mar 18, 2026
56fb411
chore(api): удалён SmtpMailProvider
Khimish009 Mar 18, 2026
c98864e
chore(api): обработка ошибки при отпраке письма
Khimish009 Mar 18, 2026
72314fc
feat(api): добавлена документация про модуль отправки писем
Khimish009 Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ DIRECT_URL="postgresql://postgres:password@host:port/mydb"

# Node Environment
NODE_ENV="development"

# Mail configuration
RESEND_API_KEY=your_api_key
MAIL_FROM=noreply@example.com
MAIL_FROM_NAME=Tracker Task
6 changes: 6 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@nestjs/swagger": "^11.2.6",
"@prisma/adapter-pg": "^7.4.0",
"@prisma/client": "^7.4.0",
"@react-email/components": "^1.0.10",
"@react-email/render": "^2.0.4",
"@repo/types": "workspace:*",
"argon2": "^0.44.0",
"cookie-parser": "^1.4.7",
Expand All @@ -41,7 +43,10 @@
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.18.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"reflect-metadata": "^0.2.2",
"resend": "^6.9.4",
"rxjs": "^7.8.2",
"zod": "^4.3.6"
},
Expand All @@ -58,6 +63,7 @@
"@types/passport": "^1.0.17",
"@types/passport-jwt": "^4.0.1",
"@types/pg": "^8.16.0",
"@types/react": "19.2.14",
"@types/supertest": "^6.0.3",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^4.0.18",
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config'
import { getJwtConfig } from './config/jwt.config'
import { JwtStrategy } from 'src/strategies/jwt.strategy'
import { PassportModule } from '@nestjs/passport'
import { MailModule } from 'src/mail/mail.module'

@Module({
imports: [
PassportModule,
MailModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: getJwtConfig,
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
ConflictException,
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common'
Expand All @@ -16,19 +17,22 @@
import { isDev } from 'src/utils/is-dev.util'
import { parseTTLToMs } from 'src/utils/ms.util'
import { RedisService } from 'src/common/redis/redis.service'
import { MailService } from 'src/mail/mail.service'

@Injectable()
export class AuthService {
private readonly JWT_ACCESS_TOKEN_TTL
private readonly JWT_REFRESH_TOKEN_TTL
private readonly COOKIE_DOMAIN: string
private readonly COOKIE_TTL: string
private readonly logger = new Logger(AuthService.name)

constructor(
private readonly prismaService: PrismaService,
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly redisService: RedisService,
private readonly mailService: MailService,
) {
this.JWT_ACCESS_TOKEN_TTL =
this.configService.getOrThrow<string>('JWT_ACCESS_TOKEN_TTL')
Expand Down Expand Up @@ -60,7 +64,13 @@
},
})

try {
await this.mailService.sendWelcomeEmail(user.email, user.name)

Check warning on line 68 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 68 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`
} catch (error) {
this.logger.error('Не удалось отправить письмо приветствия', error)
}

return await this.auth(res, user.id)

Check warning on line 73 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`
}

async login(res: Response, dto: LoginRequestDto) {
Expand All @@ -80,13 +90,13 @@
throw new NotFoundException('Пользователь не найден')
}

const isPasswordValid = await verify(user.password, password)

Check warning on line 93 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`

if (!isPasswordValid) {
throw new NotFoundException('Пользователь не найден')
}

return await this.auth(res, user.id)

Check warning on line 99 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`
}

async refresh(req: Request, res: Response) {
Expand Down Expand Up @@ -118,7 +128,7 @@
throw new NotFoundException('Пользователь не найден')
}

return await this.auth(res, user.id)

Check warning on line 131 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`
}
}

Expand Down
25 changes: 25 additions & 0 deletions apps/api/src/mail/config/mail.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ConfigService } from '@nestjs/config'

export function getMailConfig(configService: ConfigService) {
const host = configService.getOrThrow<string>('MAIL_HOST')
const port = Number(configService.getOrThrow<string>('MAIL_PORT'))
const secure = configService.getOrThrow<string>('MAIL_SECURE') === 'true'
const user = configService.getOrThrow<string>('MAIL_USER')
const pass = configService.getOrThrow<string>('MAIL_PASSWORD')
const fromAddress = configService.getOrThrow<string>('MAIL_FROM')
const fromName = configService.getOrThrow<string>('MAIL_FROM_NAME')

return {
host,
port,
secure,
auth: {
user,
pass,
},
from: {
name: fromName,
address: fromAddress,
},
}
}
2 changes: 2 additions & 0 deletions apps/api/src/mail/mail.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MAIL_PROVIDER = 'MAIL_PROVIDER'
export const RESEND_CLIENT = 'RESEND_CLIENT'
24 changes: 24 additions & 0 deletions apps/api/src/mail/mail.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common'
import { MailService } from './mail.service'
import { MAIL_PROVIDER, RESEND_CLIENT } from './mail.constants'
import { ConfigService } from '@nestjs/config'
import { ResendMailProvider } from './providers/resend-mail.provider'
import { Resend } from 'resend'

@Module({
providers: [
MailService,
{
provide: MAIL_PROVIDER,
useClass: ResendMailProvider,
},
{
provide: RESEND_CLIENT,
useFactory: (configService: ConfigService) =>
new Resend(configService.getOrThrow('RESEND_API_KEY')),
inject: [ConfigService],
},
],
exports: [MailService],
})
export class MailModule {}
5 changes: 5 additions & 0 deletions apps/api/src/mail/mail.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { MailPayload } from './mail.types'

export interface MailProvider {
send(payload: MailPayload): Promise<void>
}
29 changes: 29 additions & 0 deletions apps/api/src/mail/mail.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Inject, Injectable } from '@nestjs/common'
import type { MailProvider } from './mail.provider'
import { MAIL_PROVIDER } from './mail.constants'
import { ConfigService } from '@nestjs/config'
import { render } from '@react-email/render'
import { WELCOME_EMAIL_SUBJECT, WelcomeEmail } from './templates/welcome.email'

@Injectable()
export class MailService {
constructor(
@Inject(MAIL_PROVIDER)
private readonly mailProvider: MailProvider,
private readonly configService: ConfigService,
) {}

async sendWelcomeEmail(email: string, name: string) {
const mailName = this.configService.getOrThrow('MAIL_FROM_NAME')
const mailAddress = this.configService.getOrThrow('MAIL_FROM')

const html = await render(WelcomeEmail({ name }))

await this.mailProvider.send({
from: `${mailName} <${mailAddress}>`,
to: email,
subject: WELCOME_EMAIL_SUBJECT,
html,
})
}
}
7 changes: 7 additions & 0 deletions apps/api/src/mail/mail.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type MailPayload = {
from: string
to: string | string[]
subject: string
text?: string
html?: string
}
23 changes: 23 additions & 0 deletions apps/api/src/mail/providers/resend-mail.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Inject, Injectable } from '@nestjs/common'
import { Resend } from 'resend'
import type { MailProvider } from '../mail.provider'
import type { MailPayload } from '../mail.types'
import { RESEND_CLIENT } from '../mail.constants'

@Injectable()
export class ResendMailProvider implements MailProvider {
constructor(@Inject(RESEND_CLIENT) private readonly resend: Resend) {}

async send(payload: MailPayload): Promise<void> {
const { error } = await this.resend.emails.send({
from: payload.from,
to: payload.to,
subject: payload.subject,
html: payload.html,
})

if (error) {
throw error
}
}
}
126 changes: 126 additions & 0 deletions apps/api/src/mail/templates/welcome.email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
Body,
Button,
Container,
Head,
Heading,
Hr,
Html,
Preview,
Section,
Text,
} from '@react-email/components'

interface Props {
name: string
}

export const WELCOME_EMAIL_SUBJECT = 'Добро пожаловать в Tracker Task'

export const WelcomeEmail = ({ name }: Props) => (
<Html lang='ru'>
<Head />
<Preview>Рады видеть тебя в Tracker Task, {name}!</Preview>
<Body style={body}>
<Container style={container}>
<Section style={logoSection}>
<Text style={logo}>Tracker Task</Text>
</Section>

<Section style={content}>
<Heading style={heading}>Привет, {name}! 👋</Heading>

<Text style={paragraph}>
Твой аккаунт успешно создан. Теперь ты можешь управлять задачами, создавать
проекты и работать в команде.
</Text>

<Text style={paragraph}>Начни прямо сейчас:</Text>

<Section style={buttonSection}>
{/* Заменить href позже на реальный url */}
<Button style={button} href='http://localhost:3000'>
Открыть Tracker Task
</Button>
</Section>

<Hr style={hr} />

<Text style={footer}>
Если ты не регистрировался — просто проигнорируй это письмо.
</Text>
</Section>
</Container>
</Body>
</Html>
)

const body = {
backgroundColor: '#f6f9fc',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}

const container = {
margin: '40px auto',
maxWidth: '560px',
}

const logoSection = {
padding: '32px 40px 0',
}

const logo = {
fontSize: '20px',
fontWeight: '700',
color: '#1a1a1a',
margin: '0',
}

const content = {
backgroundColor: '#ffffff',
borderRadius: '12px',
padding: '40px',
marginTop: '16px',
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
}

const heading = {
fontSize: '24px',
fontWeight: '700',
color: '#1a1a1a',
margin: '0 0 20px',
}

const paragraph = {
fontSize: '16px',
lineHeight: '24px',
color: '#4a5568',
margin: '0 0 16px',
}

const buttonSection = {
margin: '28px 0',
}

const button = {
backgroundColor: '#1a1a1a',
borderRadius: '8px',
color: '#ffffff',
fontSize: '15px',
fontWeight: '600',
padding: '14px 28px',
textDecoration: 'none',
display: 'inline-block',
}

const hr = {
border: 'none',
borderTop: '1px solid #e2e8f0',
margin: '28px 0 20px',
}

const footer = {
fontSize: '13px',
color: '#a0aec0',
margin: '0',
}
11 changes: 6 additions & 5 deletions apps/api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"extends": "@repo/typescript-config/nestjs.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist"
}
"extends": "@repo/typescript-config/nestjs.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist",
"jsx": "react-jsx"
}
}
Loading
Loading