@mashka818/exam-de-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/exam-guides/COMMENTS_GUIDE.md +53 -0
- package/exam-guides/EXAM_COMMANDS.txt +47 -0
- package/exam-guides/GUIDE_PAGES.md +529 -0
- package/exam-guides/NPM_PACKAGE.md +206 -0
- package/exam-guides/README.md +40 -0
- package/exam-guides/RESPONSIVE.md +224 -0
- package/exam-guides/TECH_STACK.txt +142 -0
- package/exam-guides/THEME_BANQUETAM_NET.md +106 -0
- package/exam-guides/commented-code/README.txt +5 -0
- package/exam-guides/commented-code/client/index.html +14 -0
- package/exam-guides/commented-code/client/package-lock.json +2298 -0
- package/exam-guides/commented-code/client/package.json +21 -0
- package/exam-guides/commented-code/client/public/images/README.txt +26 -0
- package/exam-guides/commented-code/client/public/images/about-cleaning.svg +4 -0
- package/exam-guides/commented-code/client/public/images/admin-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/empty-requests.svg +1 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-1.svg +4 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-2.svg +4 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-3.svg +4 -0
- package/exam-guides/commented-code/client/public/images/home-hero.svg +4 -0
- package/exam-guides/commented-code/client/public/images/login-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/logo.svg +4 -0
- package/exam-guides/commented-code/client/public/images/new-request-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/register-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/requests-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/slide-1.svg +6 -0
- package/exam-guides/commented-code/client/public/images/slide-2.svg +5 -0
- package/exam-guides/commented-code/client/public/images/slide-3.svg +5 -0
- package/exam-guides/commented-code/client/src/App.jsx +72 -0
- package/exam-guides/commented-code/client/src/api.js +71 -0
- package/exam-guides/commented-code/client/src/components/FormField.jsx +25 -0
- package/exam-guides/commented-code/client/src/components/Layout.jsx +83 -0
- package/exam-guides/commented-code/client/src/components/PageImage.jsx +38 -0
- package/exam-guides/commented-code/client/src/components/ProtectedRoute.jsx +35 -0
- package/exam-guides/commented-code/client/src/components/UserNav.jsx +33 -0
- package/exam-guides/commented-code/client/src/components/landing/HeroSlider.jsx +103 -0
- package/exam-guides/commented-code/client/src/components/landing/LandingLayout.jsx +76 -0
- package/exam-guides/commented-code/client/src/components/landing/SiteFooter.jsx +74 -0
- package/exam-guides/commented-code/client/src/config/images.js +104 -0
- package/exam-guides/commented-code/client/src/constants/services.js +19 -0
- package/exam-guides/commented-code/client/src/context/AuthContext.jsx +72 -0
- package/exam-guides/commented-code/client/src/index.css +73 -0
- package/exam-guides/commented-code/client/src/main.jsx +28 -0
- package/exam-guides/commented-code/client/src/pages/AdminPage.jsx +151 -0
- package/exam-guides/commented-code/client/src/pages/LandingPage.jsx +131 -0
- package/exam-guides/commented-code/client/src/pages/LoginPage.jsx +81 -0
- package/exam-guides/commented-code/client/src/pages/RegisterPage.jsx +117 -0
- package/exam-guides/commented-code/client/src/pages/RequestFormPage.jsx +196 -0
- package/exam-guides/commented-code/client/src/pages/RequestsPage.jsx +112 -0
- package/exam-guides/commented-code/client/src/utils/validation.js +71 -0
- package/exam-guides/commented-code/client/vite.config.js +31 -0
- package/exam-guides/commented-code/server/db/init.js +67 -0
- package/exam-guides/commented-code/server/db/pool.js +23 -0
- package/exam-guides/commented-code/server/db/schema.sql +53 -0
- package/exam-guides/commented-code/server/db/seed.sql +15 -0
- package/exam-guides/commented-code/server/index.js +45 -0
- package/exam-guides/commented-code/server/middleware/auth.js +38 -0
- package/exam-guides/commented-code/server/package-lock.json +1084 -0
- package/exam-guides/commented-code/server/package.json +17 -0
- package/exam-guides/commented-code/server/routes/admin.js +96 -0
- package/exam-guides/commented-code/server/routes/auth.js +128 -0
- package/exam-guides/commented-code/server/routes/requests.js +115 -0
- package/exam-guides/commented-code/server/routes/services.js +31 -0
- package/exam-guides/commented-code/server/utils/validation.js +81 -0
- package/exam-guides/exam-starter/README.txt +22 -0
- package/exam-guides/exam-starter/package.json +13 -0
- package/exam-guides//320/243/320/224/320/220/320/233/320/230/320/242/320/254-/320/237/320/225/320/240/320/225/320/224-/320/241/320/224/320/220/320/247/320/225/320/231.txt +9 -0
- package/exam-project/README.md +16 -0
- package/exam-project/client/index.html +14 -0
- package/exam-project/client/package-lock.json +2298 -0
- package/exam-project/client/package.json +21 -0
- package/exam-project/client/public/images/README.txt +26 -0
- package/exam-project/client/public/images/about-cleaning.svg +4 -0
- package/exam-project/client/public/images/admin-banner.svg +1 -0
- package/exam-project/client/public/images/empty-requests.svg +1 -0
- package/exam-project/client/public/images/footer-photo-1.svg +4 -0
- package/exam-project/client/public/images/footer-photo-2.svg +4 -0
- package/exam-project/client/public/images/footer-photo-3.svg +4 -0
- package/exam-project/client/public/images/home-hero.svg +4 -0
- package/exam-project/client/public/images/login-banner.svg +1 -0
- package/exam-project/client/public/images/logo.svg +4 -0
- package/exam-project/client/public/images/new-request-banner.svg +1 -0
- package/exam-project/client/public/images/register-banner.svg +1 -0
- package/exam-project/client/public/images/requests-banner.svg +1 -0
- package/exam-project/client/public/images/slide-1.svg +6 -0
- package/exam-project/client/public/images/slide-2.svg +5 -0
- package/exam-project/client/public/images/slide-3.svg +5 -0
- package/exam-project/client/src/App.jsx +52 -0
- package/exam-project/client/src/api.js +50 -0
- package/exam-project/client/src/components/FormField.jsx +11 -0
- package/exam-project/client/src/components/Layout.jsx +61 -0
- package/exam-project/client/src/components/PageImage.jsx +22 -0
- package/exam-project/client/src/components/ProtectedRoute.jsx +20 -0
- package/exam-project/client/src/components/UserNav.jsx +20 -0
- package/exam-project/client/src/components/landing/HeroSlider.jsx +89 -0
- package/exam-project/client/src/components/landing/LandingLayout.jsx +61 -0
- package/exam-project/client/src/components/landing/SiteFooter.jsx +61 -0
- package/exam-project/client/src/config/images.js +83 -0
- package/exam-project/client/src/constants/services.js +7 -0
- package/exam-project/client/src/context/AuthContext.jsx +55 -0
- package/exam-project/client/src/index.css +54 -0
- package/exam-project/client/src/main.jsx +17 -0
- package/exam-project/client/src/pages/AdminPage.jsx +128 -0
- package/exam-project/client/src/pages/LandingPage.jsx +115 -0
- package/exam-project/client/src/pages/LoginPage.jsx +63 -0
- package/exam-project/client/src/pages/RegisterPage.jsx +97 -0
- package/exam-project/client/src/pages/RequestFormPage.jsx +178 -0
- package/exam-project/client/src/pages/RequestsPage.jsx +94 -0
- package/exam-project/client/src/utils/validation.js +54 -0
- package/exam-project/client/vite.config.js +17 -0
- package/exam-project/package.json +18 -0
- package/exam-project/scripts/init-project.js +86 -0
- package/exam-project/scripts/prepack.js +27 -0
- package/exam-project/scripts/unpack-template.js +105 -0
- package/exam-project/server/db/init.js +50 -0
- package/exam-project/server/db/pool.js +11 -0
- package/exam-project/server/db/schema.sql +41 -0
- package/exam-project/server/db/seed.sql +12 -0
- package/exam-project/server/index.js +29 -0
- package/exam-project/server/middleware/auth.js +24 -0
- package/exam-project/server/package-lock.json +1084 -0
- package/exam-project/server/package.json +17 -0
- package/exam-project/server/routes/admin.js +76 -0
- package/exam-project/server/routes/auth.js +109 -0
- package/exam-project/server/routes/requests.js +99 -0
- package/exam-project/server/routes/services.js +18 -0
- package/exam-project/server/utils/validation.js +63 -0
- package/package.json +25 -0
- package/scripts/init-project.js +86 -0
- package/scripts/prepack.js +27 -0
- package/scripts/unpack-template.js +105 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ВАЛИДАЦИЯ НА КЛИЕНТЕ (мгновенные ошибки на форме)
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* Должна совпадать с server/utils/validation.js
|
|
6
|
+
* ЗАМЕНИТЕ регулярки и сообщения, если в задании другие правила.
|
|
7
|
+
* БАНКЕТАМ.НЕТ: см. server/utils/validation.js (логин, пароль 8, дата ДД.ММ.ГГГГ).
|
|
8
|
+
*
|
|
9
|
+
* ВАЖНО: любое изменение здесь — продублировать в server/utils/validation.js
|
|
10
|
+
* Иначе форма пройдёт на клиенте, но сервер вернёт 400.
|
|
11
|
+
*
|
|
12
|
+
* validateRegistration — RegisterPage submit
|
|
13
|
+
* validateRequestForm — RequestFormPage submit
|
|
14
|
+
* formatPhoneInput — onChange телефона (маска +7(999)-999-99-99)
|
|
15
|
+
*
|
|
16
|
+
* GUIDE_PAGES.md §7.1 | THEME_BANQUETAM_NET.md
|
|
17
|
+
* =============================================================================
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Телефон строго в формате задания — менять вместе с подсказкой на форме */
|
|
21
|
+
export const PHONE_REGEX = /^\+7\(\d{3}\)-\d{3}-\d{2}-\d{2}$/;
|
|
22
|
+
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
23
|
+
export const FIO_REGEX = /^[А-Яа-яЁё\s-]+$/;
|
|
24
|
+
|
|
25
|
+
/** Регистрация — поля users */
|
|
26
|
+
export function validateRegistration(form) {
|
|
27
|
+
const errors = {};
|
|
28
|
+
if (!form.login?.trim()) errors.login = 'Логин обязателен';
|
|
29
|
+
if (!form.password) errors.password = 'Пароль обязателен';
|
|
30
|
+
else if (form.password.length < 6) errors.password = 'Минимум 6 символов'; // БАНКЕТАМ.НЕТ: 8
|
|
31
|
+
if (!form.fullName?.trim()) errors.fullName = 'ФИО обязательно';
|
|
32
|
+
else if (!FIO_REGEX.test(form.fullName.trim())) errors.fullName = 'Только кириллица и пробелы';
|
|
33
|
+
if (!form.phone?.trim()) errors.phone = 'Телефон обязателен';
|
|
34
|
+
else if (!PHONE_REGEX.test(form.phone.trim())) errors.phone = 'Формат: +7(XXX)-XXX-XX-XX';
|
|
35
|
+
if (!form.email?.trim()) errors.email = 'Email обязателен';
|
|
36
|
+
else if (!EMAIL_REGEX.test(form.email.trim())) errors.email = 'Некорректный email';
|
|
37
|
+
return errors;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Новая заявка — поля requests */
|
|
41
|
+
export function validateRequestForm(form) {
|
|
42
|
+
const errors = {};
|
|
43
|
+
if (!form.address?.trim()) errors.address = 'Адрес обязателен';
|
|
44
|
+
if (!form.contactPhone?.trim()) errors.contactPhone = 'Телефон обязателен';
|
|
45
|
+
else if (!PHONE_REGEX.test(form.contactPhone.trim()))
|
|
46
|
+
errors.contactPhone = 'Формат: +7(XXX)-XXX-XX-XX';
|
|
47
|
+
if (!form.scheduledAt) errors.scheduledAt = 'Укажите дату и время';
|
|
48
|
+
if (!form.paymentType) errors.paymentType = 'Выберите оплату';
|
|
49
|
+
if (form.isCustomService) {
|
|
50
|
+
if (!form.customService?.trim()) errors.customService = 'Опишите услугу';
|
|
51
|
+
} else if (!form.serviceTypeId) {
|
|
52
|
+
errors.serviceTypeId = 'Выберите услугу';
|
|
53
|
+
}
|
|
54
|
+
return errors;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Маска телефона при вводе — можно оставить для любой темы с тем же форматом */
|
|
58
|
+
export function formatPhoneInput(value) {
|
|
59
|
+
const digits = value.replace(/\D/g, '').slice(0, 11);
|
|
60
|
+
if (!digits.length) return '';
|
|
61
|
+
let d = digits;
|
|
62
|
+
if (d[0] === '8') d = '7' + d.slice(1);
|
|
63
|
+
if (d[0] !== '7') d = '7' + d;
|
|
64
|
+
const p = d.slice(1);
|
|
65
|
+
let out = '+7';
|
|
66
|
+
if (p.length > 0) out += `(${p.slice(0, 3)}`;
|
|
67
|
+
if (p.length >= 3) out += `)-${p.slice(3, 6)}`;
|
|
68
|
+
if (p.length >= 6) out += `-${p.slice(6, 8)}`;
|
|
69
|
+
if (p.length >= 8) out += `-${p.slice(8, 10)}`;
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* =============================================================================
|
|
7
|
+
* vite.config.js — СБОРКА И DEV-СЕРВЕР ФРОНТА
|
|
8
|
+
* =============================================================================
|
|
9
|
+
* port: 5173 — адрес сайта http://localhost:5173
|
|
10
|
+
* proxy /api → http://localhost:3001 — поэтому в api.js пишем fetch('/api/...')
|
|
11
|
+
*
|
|
12
|
+
* Картинки: всё из client/public/ отдаётся как /имя-файла (папка images → /images/...)
|
|
13
|
+
*
|
|
14
|
+
* Если сменили PORT в server/.env (например 3002) — поменяйте target ниже.
|
|
15
|
+
* production build: npm run build → папка client/dist
|
|
16
|
+
*
|
|
17
|
+
* GUIDE_PAGES.md §3.1
|
|
18
|
+
* =============================================================================
|
|
19
|
+
*/
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
plugins: [react(), tailwindcss()],
|
|
22
|
+
server: {
|
|
23
|
+
port: 5173,
|
|
24
|
+
proxy: {
|
|
25
|
+
'/api': {
|
|
26
|
+
target: 'http://localhost:3001',
|
|
27
|
+
changeOrigin: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ИНИЦИАЛИЗАЦИЯ БД: npm run db:init
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* Читает schema.sql + seed.sql, создаёт админа adminka/password
|
|
6
|
+
* ЗАМЕНИТЕ: login/password в seedAdmin(), ФИО и email админа
|
|
7
|
+
*
|
|
8
|
+
* КОМАНДА: из корня проекта npm run db:init
|
|
9
|
+
* 1) Читает schema.sql (таблицы)
|
|
10
|
+
* 2) Читает seed.sql (услуги)
|
|
11
|
+
* 3) seedAdmin() — UPSERT админа (ON CONFLICT login)
|
|
12
|
+
*
|
|
13
|
+
* Перед запуском: CREATE DATABASE в psql + DATABASE_URL в server/.env
|
|
14
|
+
* БАНКЕТАМ.НЕТ: login Admin26, password Demo20 в seedAdmin()
|
|
15
|
+
*
|
|
16
|
+
* README.md | GUIDE_PAGES.md §8.5
|
|
17
|
+
* =============================================================================
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import bcrypt from 'bcryptjs';
|
|
23
|
+
import dotenv from 'dotenv';
|
|
24
|
+
import { pool } from './pool.js';
|
|
25
|
+
|
|
26
|
+
dotenv.config();
|
|
27
|
+
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
async function runSqlFile(filename) {
|
|
31
|
+
const sql = fs.readFileSync(path.join(__dirname, filename), 'utf8');
|
|
32
|
+
await pool.query(sql);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function seedAdmin() {
|
|
36
|
+
// --- Учётка админа по заданию ДЭ (п.5) — менять здесь ---
|
|
37
|
+
// БАНКЕТАМ.НЕТ: const login = 'Admin26'; const password = 'Demo20';
|
|
38
|
+
const login = 'adminka';
|
|
39
|
+
const password = 'password';
|
|
40
|
+
const hash = await bcrypt.hash(password, 10);
|
|
41
|
+
|
|
42
|
+
await pool.query(
|
|
43
|
+
`INSERT INTO users (login, password_hash, full_name, phone, email, role)
|
|
44
|
+
VALUES ($1, $2, $3, $4, $5, 'admin')
|
|
45
|
+
ON CONFLICT (login) DO UPDATE SET
|
|
46
|
+
password_hash = EXCLUDED.password_hash,
|
|
47
|
+
role = 'admin'`,
|
|
48
|
+
[login, hash, 'Администратор системы', '+7(000)-000-00-00', 'admin@moynesam.local'] // БАНКЕТАМ.НЕТ: email admin@banketam.net
|
|
49
|
+
);
|
|
50
|
+
console.log('Админ: логин adminka, пароль password'); // БАНКЕТАМ.НЕТ: Admin26 / Demo20
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
try {
|
|
55
|
+
await runSqlFile('schema.sql');
|
|
56
|
+
await runSqlFile('seed.sql');
|
|
57
|
+
await seedAdmin();
|
|
58
|
+
console.log('База данных готова.');
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('Ошибка инициализации:', err.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
} finally {
|
|
63
|
+
await pool.end();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* pool.js — ПОДКЛЮЧЕНИЕ К PostgreSQL
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* Менять ТОЛЬКО server/.env → DATABASE_URL
|
|
6
|
+
* postgresql://ЛОГИН:ПАРОЛЬ@localhost:5432/ИМЯ_БАЗЫ
|
|
7
|
+
*
|
|
8
|
+
* БАНКЕТАМ.НЕТ: CREATE DATABASE banketam_net; и /banketam_net в URL
|
|
9
|
+
* Импорт pool везде: import { pool } from '../db/pool.js';
|
|
10
|
+
*
|
|
11
|
+
* GUIDE_PAGES.md §8 | README.md (разбор строки подключения)
|
|
12
|
+
* =============================================================================
|
|
13
|
+
*/
|
|
14
|
+
import pg from 'pg';
|
|
15
|
+
import dotenv from 'dotenv';
|
|
16
|
+
|
|
17
|
+
dotenv.config();
|
|
18
|
+
|
|
19
|
+
const { Pool } = pg;
|
|
20
|
+
|
|
21
|
+
export const pool = new Pool({
|
|
22
|
+
connectionString: process.env.DATABASE_URL,
|
|
23
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- СХЕМА БД — при смене темы переименуйте БД и таблицы по смыслу предметной области
|
|
3
|
+
-- Тема сейчас: клининг «Мой Не Сам» → заявки на уборку (requests)
|
|
4
|
+
-- Другая тема: заявки → orders / bookings / applications
|
|
5
|
+
-- =============================================================================
|
|
6
|
+
-- БАНКЕТАМ.НЕТ: users — без изменений структуры (ФИО, телефон, email, login, password).
|
|
7
|
+
-- БАНКЕТАМ.НЕТ: service_types → помещения (зал, ресторан, веранды).
|
|
8
|
+
-- БАНКЕТАМ.НЕТ: requests → бронирования; address можно оставить или переименовать в comment_only.
|
|
9
|
+
-- БАНКЕТАМ.НЕТ: scheduled_at → дата начала банкета.
|
|
10
|
+
-- БАНКЕТАМ.НЕТ: статусы CHECK → 'new','in_progress','completed' (без cancelled при необходимости).
|
|
11
|
+
-- БАНКЕТАМ.НЕТ: Модуль 3 — ДОБАВИТЬ таблицу reviews (request_id, user_id, text, rating, created_at).
|
|
12
|
+
-- =============================================================================
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
15
|
+
id SERIAL PRIMARY KEY,
|
|
16
|
+
login VARCHAR(64) NOT NULL UNIQUE,
|
|
17
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
18
|
+
full_name VARCHAR(255) NOT NULL,
|
|
19
|
+
phone VARCHAR(32) NOT NULL,
|
|
20
|
+
email VARCHAR(255) NOT NULL,
|
|
21
|
+
role VARCHAR(20) NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- Справочник видов услуг — при другой теме замените названия и список в seed
|
|
26
|
+
-- БАНКЕТАМ.НЕТ: по смыслу «помещения», таблицу можно не переименовывать (меньше правок в SQL)
|
|
27
|
+
CREATE TABLE IF NOT EXISTS service_types (
|
|
28
|
+
id SERIAL PRIMARY KEY,
|
|
29
|
+
name VARCHAR(255) NOT NULL UNIQUE
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Заявки заказчика — ядро предметной области
|
|
33
|
+
-- Поля: address (адрес/комментарий), scheduled_at (дата услуги), payment_type cash|card
|
|
34
|
+
-- service_type_id ИЛИ custom_service — «иная услуга»; БАНКЕТАМ.НЕТ: только service_type_id
|
|
35
|
+
-- status: new → админ меняет на in_progress / completed (подписи в requests.js STATUS_LABELS)
|
|
36
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
37
|
+
id SERIAL PRIMARY KEY,
|
|
38
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
39
|
+
address TEXT NOT NULL,
|
|
40
|
+
contact_phone VARCHAR(32) NOT NULL,
|
|
41
|
+
service_type_id INTEGER REFERENCES service_types(id),
|
|
42
|
+
custom_service TEXT,
|
|
43
|
+
scheduled_at TIMESTAMPTZ NOT NULL,
|
|
44
|
+
payment_type VARCHAR(20) NOT NULL CHECK (payment_type IN ('cash', 'card')),
|
|
45
|
+
status VARCHAR(20) NOT NULL DEFAULT 'new'
|
|
46
|
+
CHECK (status IN ('new', 'in_progress', 'completed', 'cancelled')),
|
|
47
|
+
cancel_reason TEXT, -- БАНКЕТАМ.НЕТ: поле можно не использовать (в задании нет «отмены»)
|
|
48
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
49
|
+
-- БАНКЕТАМ.НЕТ: ALTER TABLE requests ADD COLUMN review_text TEXT; — отзыв после смены статуса админом
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- SEED — начальные записи в service_types (справочник для формы п.4)
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
-- Выполняется из init.js при npm run db:init
|
|
5
|
+
-- ON CONFLICT DO NOTHING — повторный init не дублирует строки
|
|
6
|
+
--
|
|
7
|
+
-- Синхронизируйте с client/src/constants/services.js (лендинг)
|
|
8
|
+
-- БАНКЕТАМ.НЕТ: замените VALUES на четыре помещения:
|
|
9
|
+
-- ('Банкетный зал'), ('Ресторан'), ('Летняя веранда'), ('Закрытая веранда')
|
|
10
|
+
INSERT INTO service_types (name) VALUES
|
|
11
|
+
('Общий клининг'),
|
|
12
|
+
('Генеральная уборка'),
|
|
13
|
+
('Послестроительная уборка'),
|
|
14
|
+
('Химчистка ковров и мебели')
|
|
15
|
+
ON CONFLICT (name) DO NOTHING;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* EXPRESS — точка входа сервера
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* PORT, DATABASE_URL, JWT_SECRET — в server/.env
|
|
6
|
+
* Префикс /api обычно не меняют. Роуты подключаются из папки routes/
|
|
7
|
+
* БАНКЕТАМ.НЕТ: DATABASE_URL в .env — имя БД например banketam_net
|
|
8
|
+
*
|
|
9
|
+
* ЗАПУСК: npm run dev (корень) или node index.js из server/
|
|
10
|
+
* Порядок: dotenv → cors → express.json() → роуты → listen(PORT)
|
|
11
|
+
*
|
|
12
|
+
* Новый роут: import + app.use('/api/xxx', xxxRoutes)
|
|
13
|
+
* Health-check: GET /api/health → { ok: true } — проверка что API жив
|
|
14
|
+
*
|
|
15
|
+
* GUIDE_PAGES.md §8 | README.md
|
|
16
|
+
* =============================================================================
|
|
17
|
+
*/
|
|
18
|
+
import express from 'express';
|
|
19
|
+
import cors from 'cors';
|
|
20
|
+
import dotenv from 'dotenv';
|
|
21
|
+
|
|
22
|
+
import authRoutes from './routes/auth.js';
|
|
23
|
+
import servicesRoutes from './routes/services.js';
|
|
24
|
+
import requestsRoutes from './routes/requests.js';
|
|
25
|
+
import adminRoutes from './routes/admin.js';
|
|
26
|
+
|
|
27
|
+
dotenv.config();
|
|
28
|
+
|
|
29
|
+
const app = express();
|
|
30
|
+
const PORT = process.env.PORT || 3001;
|
|
31
|
+
|
|
32
|
+
app.use(cors({ origin: true, credentials: true }));
|
|
33
|
+
app.use(express.json());
|
|
34
|
+
|
|
35
|
+
// --- Подключение API (при смене темы файлы можно переименовать, пути оставить) ---
|
|
36
|
+
app.use('/api/auth', authRoutes);
|
|
37
|
+
app.use('/api/services', servicesRoutes);
|
|
38
|
+
app.use('/api/requests', requestsRoutes);
|
|
39
|
+
app.use('/api/admin', adminRoutes);
|
|
40
|
+
|
|
41
|
+
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
|
42
|
+
|
|
43
|
+
app.listen(PORT, () => {
|
|
44
|
+
console.log(`API: http://localhost:${PORT}`);
|
|
45
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* JWT middleware
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* JWT_SECRET — server/.env
|
|
6
|
+
* role в токене: user | admin — из таблицы users
|
|
7
|
+
*
|
|
8
|
+
* authRequired — вешать на роуты /requests, /admin (заголовок Authorization: Bearer ...)
|
|
9
|
+
* adminRequired — только для /api/admin/* (роль admin после входа Admin26)
|
|
10
|
+
*
|
|
11
|
+
* На фронте: ProtectedRoute role="user" | "admin" (client/src/components/ProtectedRoute.jsx)
|
|
12
|
+
*
|
|
13
|
+
* JWT_SECRET в server/.env — обязательно задать перед экзаменом
|
|
14
|
+
* =============================================================================
|
|
15
|
+
*/
|
|
16
|
+
import jwt from 'jsonwebtoken';
|
|
17
|
+
|
|
18
|
+
export function authRequired(req, res, next) {
|
|
19
|
+
const header = req.headers.authorization;
|
|
20
|
+
if (!header?.startsWith('Bearer ')) {
|
|
21
|
+
return res.status(401).json({ message: 'Требуется авторизация' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const token = header.slice(7);
|
|
26
|
+
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
|
27
|
+
next();
|
|
28
|
+
} catch {
|
|
29
|
+
return res.status(401).json({ message: 'Сессия истекла, войдите снова' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function adminRequired(req, res, next) {
|
|
34
|
+
if (req.user?.role !== 'admin') {
|
|
35
|
+
return res.status(403).json({ message: 'Доступ только для администратора' });
|
|
36
|
+
}
|
|
37
|
+
next();
|
|
38
|
+
}
|