Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
273c340
feat: add mail.ru smtp email sender and add smartcaptcha
MDI74 Aug 8, 2025
cc0315f
refactor(FormRedesign): rename state variable for verification captcha
MDI74 Aug 8, 2025
46e3356
fix(validateCaptchaTokenApi): fix handle errors
MDI74 Aug 8, 2025
1d3ac1f
fix: lint fix
MDI74 Aug 8, 2025
95db9f8
fix: lint fix
MDI74 Aug 8, 2025
9f2457d
chore: return all forms
MDI74 Aug 11, 2025
03bbdb3
fix: fix lint
MDI74 Aug 11, 2025
1bb4fe6
fix: fix lint
MDI74 Aug 11, 2025
154593d
fix(HeroBlockTechnology): fix button width
MDI74 Aug 11, 2025
fb217d1
feat: add Checkbox component
MDI74 Aug 11, 2025
c8b40d0
feat: add a checkbox for form so that you confirm your consent to the…
MDI74 Aug 11, 2025
f2cd12c
feat(Chekbox): add custom behavior for checkbox when pressing 'Enter'
MDI74 Aug 11, 2025
7ce9068
fix: add aria-label for checkbox
MDI74 Aug 11, 2025
6323c67
feat(Form): add focus to the submit button after completing captcha
MDI74 Aug 11, 2025
e8e6c07
refactor: remove duplicated code
MDI74 Aug 12, 2025
a3acabd
fix: fix smartCaptcha language
MDI74 Aug 12, 2025
15a5586
feat: add the form display only in Russia
MDI74 Aug 13, 2025
55887e6
docs: add providing-security-for-the-form ADR
MDI74 Aug 13, 2025
7792c9f
Merge branch 'develop' into feature/add-mail.ru-smtp-and-smartcaptcha…
MDI74 Aug 13, 2025
e0b2584
test(FormBlockRedesign): add form display depending on geolocation tests
MDI74 Aug 13, 2025
efc80e2
docs: update providing-security-for-the-form.md
MDI74 Aug 13, 2025
688da53
fix: fixed the empty description in the email message
MDI74 Aug 13, 2025
ddf39cc
test(FormBlockRedesign): add submit button state based on privacy pol…
MDI74 Aug 13, 2025
f1f23e1
feat: add disabling the button while the consent mark is not marked
MDI74 Aug 13, 2025
66a7e73
chore(FormBlock): update screenshots
MDI74 Aug 13, 2025
a7ec2c8
build: remove react-google-recaptcha
MDI74 Aug 13, 2025
223a8ff
build: remove @emailjs/browser
MDI74 Aug 13, 2025
66bb70f
fix: punctuation in ADR
katariniss Aug 20, 2025
e17032d
Merge branch 'develop' into feature/add-mail.ru-smtp-and-smartcaptcha…
MDI74 Aug 25, 2025
ad7e196
chore: uncommented form modal in header
MDI74 Aug 25, 2025
fab561d
fix: fix lint
MDI74 Aug 25, 2025
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
8 changes: 7 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ NEXT_PUBLIC_EMAIL_TEMPLATE_ID=template_cz5xqkf
NEXT_PUBLIC_EMAIL_PUBLIC_KEY=cmeaOeNpGPYjN8WzV
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=UA-171018032-1
NEXT_PUBLIC_YANDEX_METRIKA_ID=89913543
NEXT_PUBLIC_ARTICLE_URL=https://raw.githubusercontent.com/TourmalineCore/TourmalineCore.Articles/master/articles/
NEXT_PUBLIC_ARTICLE_URL=https://raw.githubusercontent.com/TourmalineCore/TourmalineCore.Articles/master/articles/
NEXT_PUBLIC_TARGET_EMAIL=targetEmail@tourmalinecore.com
MAILRU_EMAIL=test@tourmalinecore.com
MAILRU_PASSWORD=yourpassword
NEXT_PUBLIC_SMARTCAPTCHA_CLIENT_KEY=ysc1_ruZxAsOm5Z3ubTqoerfrehmutrhl0YsP7Tkg3r4mfMPB14652ea0
SMARTCAPTCHA_SERVER_KEY=ysc2_ruZxAsOm5Z3ubTqoerfrehmutrhl0YsP7Tkg3r4mfMPB14652ea0

7 changes: 6 additions & 1 deletion .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ NEXT_PUBLIC_EMAIL_TEMPLATE_ID=template_cz5xqkf
NEXT_PUBLIC_EMAIL_PUBLIC_KEY=cmeaOeNpGPYjN8WzV
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=UA-171018032-1
NEXT_PUBLIC_YANDEX_METRIKA_ID=89913543
NEXT_PUBLIC_ARTICLE_URL=https://raw.githubusercontent.com/TourmalineCore/TourmalineCore.Articles/master/articles/
NEXT_PUBLIC_ARTICLE_URL=https://raw.githubusercontent.com/TourmalineCore/TourmalineCore.Articles/master/articles/
NEXT_PUBLIC_TARGET_EMAIL=targetEmail@tourmalinecore.com
MAILRU_EMAIL=test@tourmalinecore.com
MAILRU_PASSWORD=yourpassword
NEXT_PUBLIC_SMARTCAPTCHA_CLIENT_KEY=ysc1_ruZxAsOm5Z3ubTqoerfrehmutrhl0YsP7Tkg3r4mfMPB14652ea0
SMARTCAPTCHA_SERVER_KEY=ysc2_ruZxAsOm5Z3ubTqoerfrehmutrhl0YsP7Tkg3r4mfMPB14652ea0
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions architecture decision records/providing-security-for-the-form.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Обеспечение безопасной обработки данных в формах при работе с персональными данными в России.

## Статус
Принято

## Контекст
Необходимо обеспечить безопасную обработку данных в формах при работе с персональными данными, в соответствии с требованиями законодательства и защитить систему от автоматизированных атак. Форма должна показываться только пользователям из России.

## Решение
### Обработка персональных данных
1. Необходимо явное соглашение на обработку персональных данных с проставлением галочки напротив текста
"Я даю согласие на обработку своих персональных данных ...".
2. Галочка на согласие не должна проставляться автоматически.
3. Форма не должна отправляться без проставленной галочки на согласие.
4. Присутствует ссылка на документ с политикой конфиденциальности и обработкой персональных данных.

Плюсы:
- Прозрачность и соответствие юридическим требованиям

- Четкое доказательство получения согласия

### Защита от автоматизированных атак (Yandex SmartCaptcha)
Архитектурное решение:

![alt text](./images/captcha-schema.png)

1. После успешного заполнения капчи клиент вызывает `/api/validateCaptcha` с параметром `token` который генерируется автоматически после заполнения капчи.
2. Внутри `/api/validateCaptcha` вызывается эндпоинт на стороне Yandex `https://smartcaptcha.yandexcloud.net/validate` с параметрами `token` и `secret` (`secret` можно получить в личном кабинете, его обязательно добавляем в `.env` т.к. это секрет) для валидации отправленного `token`.
3. После валидации вернется ответ, если `token` валидный, то вернется `{ status: 'ok' }`, иначе `{ "status": "failed" }`.

Особенности работы:
1. Токен одноразовый, поэтому если попробовать отправить несколько форм с одним токеном, мы получим ошибку с сообщением о том, что такой токен уже был использован.

Yandex SmartCaptcha имеет полноценный гайд по созданию и добавлению капчи на сайта, прочитать можно [тут](https://yandex.cloud/ru/docs/smartcaptcha/quickstart#node_1)

>В гайде по ссылке есть пример только для ванильного JS, в личном кабинете в сервисе Yandex SmartCaptcha есть пример кода для React. Для этого необходимо нажать на кнопку "Подключить" в верхнем правом углу.

![alt text](./images/location-of-the-connect-button.png)

Плюсы:
- Снижение автоматизированных атак ботов

Минусы:
- Пользователю приходится проделывать больше действий для отправки формы

## Отправка писем на почту
Используем mail.ru SMTP, более подробно про настройку можно прочитать [тут](https://help.mail.ru/mail/login/mailer/#setup).

Архитектурное решение:

![alt text](./images/send-email.png)

1. После заполнения формы и выполнения капчи клиент вызывает `/api/sendEmail`, который принимает следующие параметры:

```js
{
to: 'targetEmail',
subject: `yourSubject`,
message: `yourMessage`,
html: 'html',
}
```

2. Внутри этого эндпоинта используется библиотека [Nodemailer](https://nodemailer.com/), которая имеет методы для создания `transporter` со следующими параметра:

```js
{
host: `smtp.mail.ru`,
port: 465,
secure: true,
auth: {
user: yourEmail,
pass: yourServicePassword,
},
}
```

3. Вызывается метод `transporter.sendEmail()`, который отправляет сообщение на указанную почту (параметр `to`).


Плюсы:
- Бесплатно
- Прост в настройке

## Отображение формы только для пользователей из России
Из-за того, что в форме используются российские сервисы для капчи и отправки сообщений, ее можно показывать только пользователям из России.

Для этого используется стандартный инструмент в JS [Geolocation API](https://developer.mozilla.org/ru/docs/Web/API/Geolocation_API/Using_the_Geolocation_API). С помощью него можно получить координаты пользователя, но для того чтобы получить страну пользователя, эти координаты нужно геокодировать, для этого используется пакет [coordinate_to_country](https://www.npmjs.com/package/coordinate_to_country).
1 change: 1 addition & 0 deletions common/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './useDeviceSize';
export * from './useOnClickOutside';
export * from './usePath';
export * from './useTranslationNamespace';
export * from './useIsRussianCountry';
export * from './useOnScrollDirections';
16 changes: 16 additions & 0 deletions common/hooks/useIsRussianCountry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useState } from "react";
import { getCountry } from "../utils/getCountry";

export function useIsRussianCountry() {
const [country, setCountry] = useState<string | null>(null);

useEffect(() => {
const checkCountry = async () => {
setCountry(await getCountry());
};

checkCountry();
}, []);

return country === `RUS`;
}
17 changes: 17 additions & 0 deletions common/utils/getCountry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import lookup from "coordinate_to_country";

export async function getCountry(): Promise<string | null> {
if (navigator.geolocation) {
try {
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});

const country = await lookup(position.coords.latitude, position.coords.longitude);
return country[0];
} catch {
return null;
}
}
return null;
}
21 changes: 21 additions & 0 deletions components/Checkbox/Checkbox.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.checkbox {
margin: 0;
border: 2px solid $color-white;
border-radius: 2px;
width: 16px;
height: 16px;
background-color: $color-black;
cursor: pointer;
appearance: none;

&:checked {
background-image: url("../../public/images/checkbox-mark.svg");
background-position: center;
background-repeat: no-repeat;
}

@include desktop-xl {
width: 20px;
height: 20px;
}
}
23 changes: 23 additions & 0 deletions components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import clsx from "clsx";
import { DetailedHTMLProps, InputHTMLAttributes } from "react";

export function CheckBox({
className,
...props
}: {
className?: string;
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>) {
return (
<input
className={clsx(`checkbox`, className)}
type="checkbox"
onKeyDown={(e) => {
if (e.key === `Enter`) {
e.preventDefault();
e.target.checked = !e.target.checked;
}
}}
{...props}
/>
);
}
34 changes: 20 additions & 14 deletions components/Cta/Cta.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useBodyScrollHidden, usePath } from '../../common/hooks';
import { useBodyScrollHidden, useIsRussianCountry, usePath } from '../../common/hooks';
import { TechnologyPageAnchorLink } from '../../common/enums';
import { FormModal } from '../FormModal/FormModal';
import { PrimaryButton } from '../PrimaryButton/PrimaryButton';

export function Cta() {
const {
Expand All @@ -16,6 +17,8 @@ export function Cta() {

useBodyScrollHidden(isOpen);

const isCountryRus = useIsRussianCountry();

return (
<section
id={TechnologyPageAnchorLink.Cta}
Expand All @@ -24,19 +27,22 @@ export function Cta() {
<div className="container cta__wrapper">
<div className={`cta__inner cta__inner--${slicePathname}`}>
<h2 className="title-technology-type-1 cta__title">{t(`title`)}</h2>
<a
href="mailto:contact@tourmalinecore.com"
className={`cta__button cta__button--${slicePathname}`}
>
{t(`buttonText`)}
</a>
{/* Todo: uncomment after editing the form */}
{/* <PrimaryButton
onClick={() => setIsOpen(true)}
className={`cta__button cta__button--${slicePathname}`}
>
{t(`buttonText`)}
</PrimaryButton> */}
{isCountryRus ? (
<PrimaryButton
onClick={() => setIsOpen(true)}
className={`cta__button cta__button--${slicePathname}`}
>
{t(`buttonText`)}
</PrimaryButton>
) : (
<a
href="mailto:contact@tourmalinecore.com"
className={`cta__button cta__button--${slicePathname}`}
role="button"
>
{t(`buttonText`)}
</a>
)}
<div className="cta__image" />
</div>
</div>
Expand Down
19 changes: 0 additions & 19 deletions components/ExternalLink/ExternalLink.tsx

This file was deleted.

47 changes: 37 additions & 10 deletions components/Form/Form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,51 @@
}

&__footer {
margin-top: 40px;
position: relative;
margin-top: 32px;

@include tablet {
display: flex;
align-items: center;
}

@include desktop {
margin-top: 56px;
margin-top: 24px;
}
}

&__consent {
@include body-s;

display: flex;
margin-top: 32px;
color: $color-white-alpha-100;
gap: 8px;

@include desktop {
margin-top: 24px;
}

@include desktop-xl {
gap: 16px;
}
}

&__consent-checkbox {
flex-shrink: 0;
margin-top: 2px;
}

&__consent-link {
position: relative;
margin-left: 4px;
color: inherit;
text-underline-offset: 2px;
}

&__button {
margin-bottom: 24px;
cursor: pointer;

@include tablet {
flex: none;
Expand All @@ -38,14 +69,10 @@
}
}

&__approval {
font-size: 12px;
line-height: 1.3;
color: $color-white-alpha-200;

@include desktop-xl {
font-size: 14px;
}
&__captcha {
position: absolute;
left: 0;
top: 0;
}

&--zh {
Expand Down
Loading