Initial commit: restructure project with Docker Compose setup
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
@@ -0,0 +1,18 @@
|
||||
# Server
|
||||
PORT=3001
|
||||
|
||||
# SMTP (Яндекс)
|
||||
SMTP_HOST=smtp.yandex.ru
|
||||
SMTP_PORT=465
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=your-email@yandex.ru
|
||||
SMTP_PASS=your-app-password
|
||||
|
||||
# Email recipient
|
||||
RECIPIENT_EMAIL=your-email@yandex.ru
|
||||
|
||||
# Site info
|
||||
SITE_NAME=Ирина — Практикующий психолог
|
||||
|
||||
# CORS allowed origin (для Docker: http://localhost, для dev: http://localhost:5173)
|
||||
FRONTEND_URL=http://localhost
|
||||
@@ -0,0 +1,17 @@
|
||||
# Этап сборки
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Продакшен этап
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY --from=builder /app/dist ./dist
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/main.js"]
|
||||
Generated
+1853
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "psychologist-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for psychologist website - email applications",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/main.js",
|
||||
"start:dev": "ts-node src/main.ts",
|
||||
"start:debug": "ts-node --inspect src/main.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"nodemailer": "^6.9.8",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { MailModule } from './mail/mail.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
MailModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { IsString, IsOptional, IsEmail, MinLength } from 'class-validator';
|
||||
|
||||
export class SendApplicationDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
phone: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { MailService } from './mail.service';
|
||||
import { SendApplicationDto } from './dto/send-application.dto';
|
||||
|
||||
@Controller('api/mail')
|
||||
export class MailController {
|
||||
constructor(private readonly mailService: MailService) {}
|
||||
|
||||
@Post('application')
|
||||
async sendApplication(@Body() dto: SendApplicationDto) {
|
||||
await this.mailService.sendApplication(dto);
|
||||
return { success: true, message: 'Application sent successfully' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailService } from './mail.service';
|
||||
import { MailController } from './mail.controller';
|
||||
|
||||
@Module({
|
||||
providers: [MailService],
|
||||
controllers: [MailController],
|
||||
})
|
||||
export class MailModule {}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import { SendApplicationDto } from './dto/send-application.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
private transporter: nodemailer.Transporter;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: this.configService.get('SMTP_HOST', 'smtp.yandex.ru'),
|
||||
port: parseInt(this.configService.get('SMTP_PORT', '465'), 10),
|
||||
secure: this.configService.get('SMTP_SECURE', 'true') === 'true',
|
||||
auth: {
|
||||
user: this.configService.getOrThrow('SMTP_USER'),
|
||||
pass: this.configService.getOrThrow('SMTP_PASS'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async sendApplication(dto: SendApplicationDto) {
|
||||
const recipientEmail = this.configService.get('RECIPIENT_EMAIL') || this.configService.get('SMTP_USER');
|
||||
const siteName = this.configService.get('SITE_NAME', 'Сайт психолога');
|
||||
|
||||
const html = `
|
||||
<h2>Новая заявка с сайта «${siteName}»</h2>
|
||||
<table style="border-collapse:collapse;width:100%;max-width:600px;font-family:sans-serif">
|
||||
<tr style="border-bottom:1px solid #eee">
|
||||
<td style="padding:10px 0;font-weight:bold;width:140px">Имя:</td>
|
||||
<td style="padding:10px 0">${this.escapeHtml(dto.name)}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #eee">
|
||||
<td style="padding:10px 0;font-weight:bold">Телефон:</td>
|
||||
<td style="padding:10px 0">${this.escapeHtml(dto.phone)}</td>
|
||||
</tr>
|
||||
${dto.email ? `
|
||||
<tr style="border-bottom:1px solid #eee">
|
||||
<td style="padding:10px 0;font-weight:bold">Email:</td>
|
||||
<td style="padding:10px 0">${this.escapeHtml(dto.email)}</td>
|
||||
</tr>
|
||||
` : ''}
|
||||
${dto.message ? `
|
||||
<tr style="border-bottom:1px solid #eee">
|
||||
<td style="padding:10px 0;font-weight:bold">Сообщение:</td>
|
||||
<td style="padding:10px 0">${this.escapeHtml(dto.message).replace(/\n/g, '<br>')}</td>
|
||||
</tr>
|
||||
` : ''}
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-weight:bold">Дата:</td>
|
||||
<td style="padding:10px 0">${new Date().toLocaleString('ru-RU')}</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
const text = `
|
||||
Новая заявка с сайта «${siteName}»
|
||||
|
||||
Имя: ${dto.name}
|
||||
Телефон: ${dto.phone}
|
||||
${dto.email ? `Email: ${dto.email}\n` : ''}${dto.message ? `Сообщение: ${dto.message}\n` : ''}
|
||||
Дата: ${new Date().toLocaleString('ru-RU')}
|
||||
`.trim();
|
||||
|
||||
await this.transporter.sendMail({
|
||||
from: `"${siteName}" <${this.configService.get('SMTP_USER')}>`,
|
||||
to: recipientEmail,
|
||||
subject: `Новая заявка от ${dto.name}`,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
private escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.enableCors({
|
||||
origin: process.env.FRONTEND_URL || true,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
|
||||
const port = process.env.PORT || 3001;
|
||||
await app.listen(port);
|
||||
console.log(`Server running on http://localhost:${port}`);
|
||||
}
|
||||
bootstrap();
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user