Initial commit: restructure project with Docker Compose setup

This commit is contained in:
Admin
2026-05-26 20:16:01 +05:00
commit 47b15787f3
48 changed files with 6074 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
.env
+18
View File
@@ -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
+17
View File
@@ -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"]
+1853
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -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"
}
}
+11
View File
@@ -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;
}
+14
View File
@@ -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' };
}
}
+9
View File
@@ -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 {}
+84
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
}
+19
View File
@@ -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();
+24
View File
@@ -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"]
}