diff --git a/package-lock.json b/package-lock.json index 6e0d63e..eb79ec2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@maskito/phone": "^3.9.1", "@maskito/react": "^3.9.1", "next": "15.3.4", - "nodemailer": "^7.0.3", + "nodemailer": "^7.0.5", "react": "^19.0.0", "react-dom": "^19.0.0", "swiper": "^11.2.10" @@ -20,6 +20,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@types/node": "^20", + "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", "clsx": "^2.1.1", @@ -995,6 +996,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -4492,9 +4503,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", - "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", "license": "MIT-0", "engines": { "node": ">=6.0.0" diff --git a/package.json b/package.json index ab7497d..bacafd1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@maskito/phone": "^3.9.1", "@maskito/react": "^3.9.1", "next": "15.3.4", - "nodemailer": "^7.0.3", + "nodemailer": "^7.0.5", "react": "^19.0.0", "react-dom": "^19.0.0", "swiper": "^11.2.10" @@ -23,6 +23,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@types/node": "^20", + "@types/nodemailer": "^6.4.17", "@types/react": "^19", "@types/react-dom": "^19", "clsx": "^2.1.1", diff --git a/src/app/api/heartbeat/route.ts b/src/app/api/heartbeat/route.ts new file mode 100644 index 0000000..4867ec7 --- /dev/null +++ b/src/app/api/heartbeat/route.ts @@ -0,0 +1,9 @@ +export async function GET(request: Request) { + return new Response('Heartbeat is OK!', { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + }, + }); +} diff --git a/src/app/api/sendform/route.ts b/src/app/api/sendform/route.ts new file mode 100644 index 0000000..a563d1c --- /dev/null +++ b/src/app/api/sendform/route.ts @@ -0,0 +1,66 @@ +import nodemailer from 'nodemailer'; +import { TBaseForm } from '@shared/api/api.types'; +import { CORE } from '@shared/config/core'; + +async function sendMail(data: TBaseForm) { + const { name, phone, message, form } = data; + + const formattedBody = ` + + +

Сообщение с сайта "Экспертиза и Оценка"

+

Форма отправки: ${form}

+

Имя отправителя: ${name ?? 'не указано'}

+

Номер телефона: ${phone}

+

Сообщение: ${message ?? 'отсутствует'}

+ + + `; + + const transporter = nodemailer.createTransport({ + service: 'Yandex', + auth: { + user: CORE.MAIL_USER, + pass: CORE.MAIL_PASS, + }, + }); + + return await transporter.sendMail({ + from: CORE.MAIL_FROM, + to: CORE.MAIL_TO, + subject: 'Заявка с сайта Ocenka-Sochi', + html: formattedBody, + }); +} + +export async function POST(request: Request) { + try { + const payload = await request.json(); + + if (payload.secure !== CORE.MAIL_SECURE_KEY) { + await Promise.reject('Request failure!'); + } + + const sendResult = await sendMail({ ...payload }); + + const data = { message: 'Form accepted' }; + const headers = new Headers({ + 'Content-Type': 'application/json', + }); + const options = { + status: 200, + statusText: 'OK', + headers: headers, + }; + + if (sendResult?.messageId) { + return new Response(JSON.stringify(data), options); + } else { + await Promise.reject('Sending request failure!'); + } + } catch (error) { + return new Response(`Api error: ${error}`, { + status: 400, + }); + } +} diff --git a/src/shared/api/api.service.ts b/src/shared/api/api.service.ts new file mode 100644 index 0000000..2ee7171 --- /dev/null +++ b/src/shared/api/api.service.ts @@ -0,0 +1,35 @@ +import { API_ROUTES } from '@shared/config/routes'; +import { TBaseForm } from '@shared/api/api.types'; +import { CORE } from '@shared/config/core'; + +type TRequest = TBaseForm; + +const sendFormFn = async ({ ...props }: TRequest) => { + try { + const response = await fetch('/api' + API_ROUTES.SEND_FORM, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...props, + secure: CORE.MAIL_SECURE_KEY, + }), + }); + + if (response.ok) { + return response; + } else { + if (response.status === 400) throw new Error(`400 Bad request`); + if (response.status === 401) throw new Error(`401 Unauthorized`); + if (response.status === 500) throw new Error('500 Internal server error'); + + throw new Error(`${response.status} - Network response failure`); + } + } catch (e) { + console.error(e); + } +}; + +export { sendFormFn }; diff --git a/src/shared/api/api.types.ts b/src/shared/api/api.types.ts new file mode 100644 index 0000000..e46571f --- /dev/null +++ b/src/shared/api/api.types.ts @@ -0,0 +1,6 @@ +export type TBaseForm = { + form: string; + name?: string; + phone: string; + message?: string; +}; diff --git a/src/shared/config/core.ts b/src/shared/config/core.ts new file mode 100644 index 0000000..6b301d4 --- /dev/null +++ b/src/shared/config/core.ts @@ -0,0 +1,7 @@ +export const CORE = { + MAIL_USER: process.env.NEXT_PUBLIC_MAIL_USER, + MAIL_PASS: process.env.NEXT_PUBLIC_MAIL_PASS, + MAIL_FROM: process.env.NEXT_PUBLIC_MAIL_FROM, + MAIL_TO: process.env.NEXT_PUBLIC_MAIL_TO, + MAIL_SECURE_KEY: process.env.NEXT_PUBLIC_MAIL_SECURE_KEY, +} as const; diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts new file mode 100644 index 0000000..cebba69 --- /dev/null +++ b/src/shared/config/routes.ts @@ -0,0 +1,4 @@ +export const API_ROUTES = { + SEND_FORM: '/sendform', + HEARTBEAT: '/heartbeat', +} as const;