-
Настоящая политика обработки персональных данных (далее — Политика) определяет порядок обработки персональных данных пользователей сайта avtourist.ru.
+
Настоящая политика обработки персональных данных (далее — Политика) определяет порядок обработки персональных данных пользователей сайта avtourist-surgut.ru.
1. Общие положения
1.1. Обработка персональных данных осуществляется на основе принципов законности, справедливости и конфиденциальности.
@@ -533,6 +533,73 @@ import { SITE_URL } from '@constants';
box-shadow: 0 4px 12px rgba(206, 159, 64, 0.4);
}
+ /* Success message */
+ .success-message {
+ text-align: center;
+ padding: 2rem 1rem;
+ }
+
+ .success-icon {
+ width: 80px;
+ height: 80px;
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 1.5rem;
+ }
+
+ .success-icon svg {
+ color: white;
+ }
+
+ .success-message h3 {
+ color: #1e3050;
+ font-size: 1.5rem;
+ font-weight: 700;
+ margin: 0 0 1rem;
+ }
+
+ .success-message p {
+ color: #64748b;
+ font-size: 1rem;
+ line-height: 1.6;
+ margin: 0 0 0.75rem;
+ }
+
+ .success-message .hint {
+ color: #94a3b8;
+ font-size: 0.875rem;
+ }
+
+ .success-message + .auth-footer {
+ display: none;
+ }
+
+ .success-message .btn-submit {
+ display: block;
+ margin-top: 1.5rem;
+ text-decoration: none;
+ width: 100%;
+ box-sizing: border-box;
+ text-align: center;
+ background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%);
+ color: #ffffff;
+ border: none;
+ border-radius: 8px;
+ padding: 0.75rem;
+ font-size: 0.95rem;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ }
+
+ .success-message .btn-submit:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px rgba(206, 159, 64, 0.4);
+ }
+
/* Кнопка отправки */
.btn-submit {
background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%);
@@ -552,6 +619,12 @@ import { SITE_URL } from '@constants';
box-shadow: 0 8px 20px rgba(206, 159, 64, 0.4);
}
+ .btn-submit:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+ }
+
.btn-submit:active {
transform: translateY(0);
}
@@ -623,13 +696,11 @@ import { SITE_URL } from '@constants';
}
function validateFirstName(value: string): boolean {
- const regex = /^[а-яА-ЯёЁ\-]+$/;
- return regex.test(value);
+ return /^[а-яА-ЯёЁ\-]+$/.test(value);
}
function validateLastName(value: string): boolean {
- const regex = /^[а-яА-ЯёЁ\-]+$/;
- return regex.test(value);
+ return /^[а-яА-ЯёЁ\-]+$/.test(value);
}
function validateEmail(value: string): boolean {
@@ -650,6 +721,7 @@ import { SITE_URL } from '@constants';
document.querySelectorAll('.toggle-password').forEach(button => {
button.addEventListener('click', () => {
const targetId = button.getAttribute('data-target');
+ if (!targetId) return;
const input = document.getElementById(targetId) as HTMLInputElement;
if (input) {
const isPassword = input.type === 'password';
@@ -660,9 +732,10 @@ import { SITE_URL } from '@constants';
});
firstNameInput?.addEventListener('input', () => {
- const value = firstNameInput.value.replace(/[^а-яА-ЯёЁ\-]/g, '');
+ const value = firstNameInput.value.replace(/[^\w\u0400-\u04FF\-]/gi, '');
firstNameInput.value = value;
- if (firstNameInput.value && !validateFirstName(firstNameInput.value)) {
+ const trimmed = value ? value.trim() : '';
+ if (trimmed.length > 0 && !validateFirstName(trimmed)) {
showError(firstNameInput, 'Используйте только русские буквы');
} else {
clearError(firstNameInput);
@@ -670,9 +743,10 @@ import { SITE_URL } from '@constants';
});
lastNameInput?.addEventListener('input', () => {
- const value = lastNameInput.value.replace(/[^а-яА-ЯёЁ\-]/g, '');
+ const value = lastNameInput.value.replace(/[^\w\u0400-\u04FF\-]/gi, '');
lastNameInput.value = value;
- if (lastNameInput.value && !validateLastName(lastNameInput.value)) {
+ const trimmed = value ? value.trim() : '';
+ if (trimmed.length > 0 && !validateLastName(trimmed)) {
showError(lastNameInput, 'Используйте только русские буквы');
} else {
clearError(lastNameInput);
@@ -751,21 +825,27 @@ import { SITE_URL } from '@constants';
return;
}
- const name = formData.get('firstName') as string;
+ const firstName = formData.get('firstName') as string;
const lastName = formData.get('lastName') as string;
- const email = formData.get('email') as string;
+ const email = (formData.get('email') as string) || '';
const phone = formData.get('phone') as string;
const password = formData.get('password') as string;
const confirmPassword = formData.get('confirmPassword') as string;
let hasErrors = false;
- if (!validateFirstName(firstName)) {
+ if (!firstName || typeof firstName !== 'string' || !firstName.trim()) {
+ showError(firstNameInput, 'Введите имя');
+ hasErrors = true;
+ } else if (!validateFirstName(firstName)) {
showError(firstNameInput, 'Введите имя (только русские буквы)');
hasErrors = true;
}
- if (!validateLastName(lastName)) {
+ if (!lastName || typeof lastName !== 'string' || !lastName.trim()) {
+ showError(lastNameInput, 'Введите фамилию');
+ hasErrors = true;
+ } else if (!validateLastName(lastName)) {
showError(lastNameInput, 'Введите фамилию (только русские буквы)');
hasErrors = true;
}
@@ -794,7 +874,53 @@ import { SITE_URL } from '@constants';
return;
}
- console.log('Регистрация:', { firstName, lastName, email, phone, password });
+ // Отправка данных на сервер
+ const submitBtn = form.querySelector('.btn-submit') as HTMLButtonElement;
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Регистрация...';
+
+ try {
+ const response = await fetch('/api/auth/sign-up', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ firstName, lastName, email, phone, password }),
+ });
+
+ const result = await response.json();
+
+if (result.success) {
+ // Показываем success message и скрываем footer
+ form.innerHTML = `
+
+
+
Регистрация успешна!
+
На ваш email ${email || ''} отправлена ссылка для подтверждения регистрации.
+
Проверьте почту и перейдите по ссылке для активации аккаунта.
+
+ `;
+
+ // Скрываем footer с ссылкой на вход
+ const authFooter = document.querySelector('.auth-footer') as HTMLElement;
+ if (authFooter) {
+ authFooter.style.display = 'none';
+ }
+ } else {
+ showError(emailInput, result.error || 'Ошибка регистрации');
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Зарегистрироваться';
+ }
+ } catch (err) {
+ showError(emailInput, 'Ошибка соединения');
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Зарегистрироваться';
+ }
});
// Модальное окно политики
diff --git a/frontend/src/pages/auth/verify.astro b/frontend/src/pages/auth/verify.astro
new file mode 100644
index 0000000..a758d17
--- /dev/null
+++ b/frontend/src/pages/auth/verify.astro
@@ -0,0 +1,208 @@
+---
+import Layout from '@layouts/Layout.astro';
+import { SITE_URL } from '@constants';
+
+const success = Astro.url.searchParams.get('success');
+const error = Astro.url.searchParams.get('error');
+const token = Astro.url.searchParams.get('token');
+const userId = Astro.url.searchParams.get('userId');
+---
+
+
+
+
+
+
+
+
+
Подтверждение email...
+
+
+
+
+
+
Email подтверждён!
+
Ваш аккаунт успешно активирован. Теперь вы можете войти в личный кабинет.
+
Войти в аккаунт
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 85b4bf7..c5f39b8 100644
--- a/package.json
+++ b/package.json
@@ -8,15 +8,20 @@
"dev:backend": "cd backend && ./pocketbase.exe serve",
"dev:frontend": "cd frontend && bun dev",
"build:frontend": "cd frontend && bun build",
- "preview:frontend": "cd frontend && bun run preview"
+ "preview:frontend": "bun run preview",
+ "dev:mail": "maildev --web 1080 --smtp 1025",
+ "preview": "cd frontend && bun run preview"
},
"workspaces": [
"frontend"
],
"dependencies": {
+ "nodemailer": "^8.0.5",
"pocketbase": "^0.26.8"
},
"devDependencies": {
- "gray-matter": "^4.0.3"
+ "@types/nodemailer": "^8.0.0",
+ "gray-matter": "^4.0.3",
+ "maildev": "^2.2.1"
}
}
diff --git a/scripts/dev.js b/scripts/dev.js
index 807fd6b..57f6059 100644
--- a/scripts/dev.js
+++ b/scripts/dev.js
@@ -6,11 +6,23 @@ import path from "path";
console.log("🚀 Запуск серверов avtourist086...\n");
-// Запуск PocketBase
+// Запуск Maildev
+const maildev = spawn("maildev", ["--web", "1080", "--smtp", "1025"], {
+ stdio: "inherit",
+ shell: true,
+});
+
+// Запуск PocketBase с SMTP настройками
const backend = spawn("pocketbase.exe", ["serve"], {
cwd: path.join(process.cwd(), "backend"),
stdio: "inherit",
shell: true,
+ env: {
+ ...process.env,
+ PB_SMTP_HOST: "localhost",
+ PB_SMTP_PORT: "1025",
+ PB_SMTP_FROM: "noreply@avtourist-surgut.ru"
+ }
});
// Запуск Astro
@@ -23,6 +35,7 @@ const frontend = spawn("bun", ["dev"], {
// Обработка завершения
const cleanup = () => {
console.log("\n🛑 Остановка серверов...");
+ maildev.kill();
backend.kill();
frontend.kill();
process.exit(0);
@@ -32,6 +45,10 @@ process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
// Ожидание завершения процессов
+maildev.on("exit", (code) => {
+ console.log(`Maildev остановлен с кодом ${code}`);
+});
+
backend.on("exit", (code) => {
console.log(`Backend остановлен с кодом ${code}`);
cleanup();
@@ -43,6 +60,7 @@ frontend.on("exit", (code) => {
});
console.log("✅ Серверы запущены:\n");
+console.log(" Maildev (SMTP): http://localhost:1080");
console.log(" Backend (PocketBase): http://localhost:8090");
console.log(" Frontend (Astro): http://localhost:4321\n");
console.log("Нажмите Ctrl+C для остановки\n");