Исправление багов авторизации, корзины и админки

- Исправлено выпадающее меню профиля (hover-баг с margin-top)
- Исправлена авторизация: правильные пути к API (api/auth.php)
- Исправлены ссылки на админку (admin/index.php вместо admin_panel.php)
- Исправлены пути API корзины в catalog.php и checkout.php
- Добавлена форма добавления/редактирования товаров в админке
- Исправлены кнопки +/- в корзине (улучшена обработка AJAX)
- Исправлена регистрация: правильные пути и обработка boolean в PostgreSQL
- Добавлена миграция для назначения прав админа пользователю admin@mail.ru
- Удален тестовый блок 'Быстрый вход' для неавторизованных пользователей
- Улучшена обработка ошибок во всех API-эндпоинтах
This commit is contained in:
kirill.khorkov
2025-12-16 02:58:44 +03:00
parent 3f257120fa
commit 29b9aaac50
388 changed files with 15312 additions and 3220 deletions

View File

@@ -0,0 +1,73 @@
-- 001_initial_schema.sql
-- Создание базовых таблиц для AETERNA
-- Таблица пользователей
CREATE TABLE IF NOT EXISTS users (
user_id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
city VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
is_admin BOOLEAN DEFAULT FALSE
);
-- Таблица категорий
CREATE TABLE IF NOT EXISTS categories (
category_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
parent_id INTEGER REFERENCES categories(category_id) ON DELETE SET NULL,
description TEXT,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Таблица подкатегорий
CREATE TABLE IF NOT EXISTS subcategories (
subcategory_id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES categories(category_id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Таблица товаров
CREATE TABLE IF NOT EXISTS products (
product_id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES categories(category_id) ON DELETE SET NULL,
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
old_price DECIMAL(10, 2),
sku VARCHAR(50) UNIQUE,
stock_quantity INTEGER DEFAULT 0,
is_available BOOLEAN DEFAULT TRUE,
is_featured BOOLEAN DEFAULT FALSE,
rating DECIMAL(3, 2) DEFAULT 0,
review_count INTEGER DEFAULT 0,
image_url VARCHAR(500),
color VARCHAR(50),
material VARCHAR(100),
card_size VARCHAR(20) DEFAULT 'small',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Индексы для быстрого поиска
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category_id);
CREATE INDEX IF NOT EXISTS idx_products_available ON products(is_available);
CREATE INDEX IF NOT EXISTS idx_products_price ON products(price);
CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id);
CREATE INDEX IF NOT EXISTS idx_categories_active ON categories(is_active);

View File

@@ -0,0 +1,70 @@
-- 002_add_cart_orders.sql
-- Таблицы для корзины и заказов
-- Таблица корзины
CREATE TABLE IF NOT EXISTS cart (
cart_id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(product_id) ON DELETE CASCADE,
quantity INTEGER DEFAULT 1 CHECK (quantity > 0),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, product_id)
);
-- Таблица заказов
CREATE TABLE IF NOT EXISTS orders (
order_id SERIAL PRIMARY KEY,
order_number VARCHAR(50) UNIQUE NOT NULL,
user_id INTEGER REFERENCES users(user_id) ON DELETE SET NULL,
-- Контактная информация
customer_name VARCHAR(100) NOT NULL,
customer_email VARCHAR(255) NOT NULL,
customer_phone VARCHAR(20) NOT NULL,
-- Адрес доставки
delivery_address TEXT NOT NULL,
delivery_region VARCHAR(100),
postal_code VARCHAR(20),
-- Способы
delivery_method VARCHAR(50) DEFAULT 'courier',
payment_method VARCHAR(50) DEFAULT 'card',
-- Суммы
subtotal DECIMAL(10, 2) NOT NULL,
discount_amount DECIMAL(10, 2) DEFAULT 0,
delivery_price DECIMAL(10, 2) DEFAULT 0,
final_amount DECIMAL(10, 2) NOT NULL,
-- Промокод
promo_code VARCHAR(50),
-- Статус и даты
status VARCHAR(30) DEFAULT 'pending',
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
);
-- Таблица позиций заказа
CREATE TABLE IF NOT EXISTS order_items (
item_id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(order_id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(product_id) ON DELETE SET NULL,
product_name VARCHAR(200) NOT NULL,
product_price DECIMAL(10, 2) NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
total_price DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Индексы
CREATE INDEX IF NOT EXISTS idx_cart_user ON cart(user_id);
CREATE INDEX IF NOT EXISTS idx_orders_user ON orders(user_id);
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
CREATE INDEX IF NOT EXISTS idx_orders_created ON orders(created_at);
CREATE INDEX IF NOT EXISTS idx_order_items_order ON order_items(order_id);

View File

@@ -0,0 +1,57 @@
-- 003_add_product_fields.sql
-- Добавление дополнительных полей (если таблицы уже существуют)
-- Добавляем поля в products если их нет
DO $$
BEGIN
-- color
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'products' AND column_name = 'color') THEN
ALTER TABLE products ADD COLUMN color VARCHAR(50);
END IF;
-- material
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'products' AND column_name = 'material') THEN
ALTER TABLE products ADD COLUMN material VARCHAR(100);
END IF;
-- card_size
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'products' AND column_name = 'card_size') THEN
ALTER TABLE products ADD COLUMN card_size VARCHAR(20) DEFAULT 'small';
END IF;
END $$;
-- Добавляем поля в users если их нет
DO $$
BEGIN
-- city
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'city') THEN
ALTER TABLE users ADD COLUMN city VARCHAR(100);
END IF;
-- last_login
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'last_login') THEN
ALTER TABLE users ADD COLUMN last_login TIMESTAMP;
END IF;
END $$;
-- Добавляем поля в categories если их нет
DO $$
BEGIN
-- updated_at
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'categories' AND column_name = 'updated_at') THEN
ALTER TABLE categories ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
END IF;
-- created_at
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'categories' AND column_name = 'created_at') THEN
ALTER TABLE categories ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
END IF;
END $$;

View File

@@ -0,0 +1,50 @@
-- 004_grant_admin_to_admin_mail.sql
-- Миграция: Назначение прав администратора пользователю admin@mail.ru
-- Обновляем пользователя admin@mail.ru, давая ему права администратора
UPDATE users
SET is_admin = TRUE,
is_active = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE email = 'admin@mail.ru';
-- Проверяем результат
DO $$
DECLARE
updated_count INTEGER;
user_info RECORD;
BEGIN
GET DIAGNOSTICS updated_count = ROW_COUNT;
IF updated_count > 0 THEN
-- Получаем информацию об обновленном пользователе
SELECT user_id, email, full_name, is_admin, is_active
INTO user_info
FROM users
WHERE email = 'admin@mail.ru';
RAISE NOTICE 'Пользователь % (ID: %) успешно получил права администратора',
user_info.email, user_info.user_id;
RAISE NOTICE 'ФИО: %, Админ: %, Активен: %',
user_info.full_name, user_info.is_admin, user_info.is_active;
ELSE
-- Если пользователь не найден, создаем его с правами админа
INSERT INTO users (email, password_hash, full_name, phone, city, is_admin, is_active)
VALUES (
'admin@mail.ru',
'$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', -- admin123
'Администратор',
'+79129991223',
'Москва',
TRUE,
TRUE
)
ON CONFLICT (email) DO UPDATE
SET is_admin = TRUE,
is_active = TRUE,
updated_at = CURRENT_TIMESTAMP;
RAISE NOTICE 'Пользователь admin@mail.ru создан/обновлен с правами администратора';
END IF;
END $$;

View File

@@ -0,0 +1,94 @@
<?php
/**
* Быстрый скрипт для назначения прав администратора пользователю admin@mail.ru
* Запуск: php migrations/grant_admin.php
*/
require_once __DIR__ . '/../config/database.php';
echo "===========================================\n";
echo " Назначение прав администратора\n";
echo "===========================================\n\n";
try {
$db = Database::getInstance()->getConnection();
echo "[OK] Подключение к базе данных успешно\n\n";
$email = 'admin@mail.ru';
// Проверяем, существует ли пользователь
$checkStmt = $db->prepare("SELECT user_id, email, full_name, is_admin, is_active FROM users WHERE email = ?");
$checkStmt->execute([$email]);
$user = $checkStmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
echo "[INFO] Найден пользователь:\n";
echo " Email: {$user['email']}\n";
echo " ФИО: {$user['full_name']}\n";
echo " Админ: " . ($user['is_admin'] ? 'ДА' : 'НЕТ') . "\n";
echo " Активен: " . ($user['is_active'] ? 'ДА' : 'НЕТ') . "\n\n";
if ($user['is_admin']) {
echo "[INFO] Пользователь уже имеет права администратора\n";
} else {
// Обновляем права
$updateStmt = $db->prepare("
UPDATE users
SET is_admin = TRUE,
is_active = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE email = ?
");
$updateStmt->execute([$email]);
echo "[SUCCESS] Права администратора успешно назначены!\n";
}
} else {
echo "[WARN] Пользователь с email $email не найден\n";
echo "[INFO] Создаю нового пользователя с правами администратора...\n";
// Создаем пользователя с правами админа
// Пароль по умолчанию: admin123
$password_hash = password_hash('admin123', PASSWORD_DEFAULT);
$insertStmt = $db->prepare("
INSERT INTO users (email, password_hash, full_name, phone, city, is_admin, is_active)
VALUES (?, ?, ?, ?, ?, CAST(? AS boolean), TRUE)
RETURNING user_id
");
$insertStmt->execute([
$email,
$password_hash,
'Администратор',
'+79129991223',
'Москва',
'true'
]);
$user_id = $insertStmt->fetchColumn();
echo "[SUCCESS] Пользователь создан с ID: $user_id\n";
echo "[INFO] Email: $email\n";
echo "[INFO] Пароль по умолчанию: admin123\n";
echo "[WARN] Рекомендуется сменить пароль после первого входа!\n";
}
// Проверяем результат
$verifyStmt = $db->prepare("SELECT user_id, email, full_name, is_admin, is_active FROM users WHERE email = ?");
$verifyStmt->execute([$email]);
$finalUser = $verifyStmt->fetch(PDO::FETCH_ASSOC);
echo "\n===========================================\n";
echo " Итоговый статус:\n";
echo "===========================================\n";
echo " Email: {$finalUser['email']}\n";
echo " ФИО: {$finalUser['full_name']}\n";
echo " Админ: " . ($finalUser['is_admin'] ? 'ДА ✓' : 'НЕТ ✗') . "\n";
echo " Активен: " . ($finalUser['is_active'] ? 'ДА ✓' : 'НЕТ ✗') . "\n";
echo "===========================================\n";
} catch (PDOException $e) {
echo "[ERROR] Ошибка: " . $e->getMessage() . "\n";
exit(1);
}

109
migrations/migrate.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
/**
* Простой раннер миграций для PostgreSQL
* Запуск: php migrations/migrate.php
*/
// Подключаем конфиг базы данных
require_once __DIR__ . '/../config/database.php';
echo "===========================================\n";
echo " AETERNA - Система миграций базы данных\n";
echo "===========================================\n\n";
try {
$db = Database::getInstance()->getConnection();
echo "[OK] Подключение к базе данных успешно\n\n";
// 1. Создаем таблицу для отслеживания миграций
$db->exec("
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) NOT NULL UNIQUE,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
echo "[OK] Таблица migrations готова\n";
// 2. Получаем список уже примененных миграций
$stmt = $db->query("SELECT filename FROM migrations ORDER BY filename");
$applied = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo "[INFO] Уже применено миграций: " . count($applied) . "\n\n";
// 3. Сканируем папку на SQL-файлы
$migrationFiles = glob(__DIR__ . '/*.sql');
sort($migrationFiles);
$newMigrations = 0;
foreach ($migrationFiles as $file) {
$filename = basename($file);
// Пропускаем seed_data.sql - он запускается отдельно
if ($filename === 'seed_data.sql') {
continue;
}
// Проверяем, была ли миграция уже применена
if (in_array($filename, $applied)) {
echo "[SKIP] $filename (уже применена)\n";
continue;
}
// Применяем миграцию
echo "[RUN] Применяю $filename... ";
$sql = file_get_contents($file);
try {
$db->exec($sql);
// Записываем в таблицу миграций
$stmt = $db->prepare("INSERT INTO migrations (filename) VALUES (?)");
$stmt->execute([$filename]);
echo "OK\n";
$newMigrations++;
} catch (PDOException $e) {
echo "ОШИБКА!\n";
echo " Причина: " . $e->getMessage() . "\n";
echo "\n[!] Миграция остановлена из-за ошибки\n";
exit(1);
}
}
echo "\n-------------------------------------------\n";
if ($newMigrations > 0) {
echo "[SUCCESS] Применено новых миграций: $newMigrations\n";
} else {
echo "[INFO] Все миграции уже применены\n";
}
// 4. Спрашиваем про seed_data
$seedFile = __DIR__ . '/seed_data.sql';
if (file_exists($seedFile)) {
echo "\n[?] Хотите загрузить начальные данные (seed_data.sql)?\n";
echo " Запустите: php migrations/migrate.php --seed\n";
if (isset($argv[1]) && $argv[1] === '--seed') {
echo "\n[RUN] Загружаю seed_data.sql... ";
try {
$sql = file_get_contents($seedFile);
$db->exec($sql);
echo "OK\n";
} catch (PDOException $e) {
echo "ОШИБКА: " . $e->getMessage() . "\n";
}
}
}
echo "\n===========================================\n";
echo " Миграции завершены!\n";
echo "===========================================\n";
} catch (PDOException $e) {
echo "[ERROR] Ошибка подключения к БД: " . $e->getMessage() . "\n";
exit(1);
}

65
migrations/seed_data.sql Normal file
View File

@@ -0,0 +1,65 @@
-- seed_data.sql
-- Начальные данные для AETERNA
-- Администратор (пароль: admin123)
INSERT INTO users (email, password_hash, full_name, phone, city, is_admin, is_active)
VALUES (
'admin@aeterna.ru',
'$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', -- admin123
'Администратор AETERNA',
'+79129991223',
'Москва',
TRUE,
TRUE
) ON CONFLICT (email) DO NOTHING;
-- Тестовый пользователь (пароль: user123)
INSERT INTO users (email, password_hash, full_name, phone, city, is_admin, is_active)
VALUES (
'user@test.com',
'$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', -- user123
'Тестовый Пользователь',
'+79111234567',
'Санкт-Петербург',
FALSE,
TRUE
) ON CONFLICT (email) DO NOTHING;
-- Категории
INSERT INTO categories (name, slug, description, sort_order, is_active) VALUES
('Диваны', 'divany', 'Прямые и угловые диваны для гостиной', 1, TRUE),
('Кресла', 'kresla', 'Кресла для гостиной и офиса', 2, TRUE),
('Кровати', 'krovati', 'Односпальные и двуспальные кровати', 3, TRUE),
('Столы', 'stoly', 'Обеденные и рабочие столы', 4, TRUE),
('Стулья', 'stulya', 'Стулья для кухни и офиса', 5, TRUE),
('Светильники', 'svetilniki', 'Торшеры, люстры и настольные лампы', 6, TRUE)
ON CONFLICT (slug) DO NOTHING;
-- Товары
INSERT INTO products (category_id, name, slug, description, price, old_price, sku, stock_quantity, is_available, image_url, color, material, card_size) VALUES
(1, 'Светильник MINNIGHT', 'svetilnik-minnight', 'Настольный светильник в современном стиле', 7999, 9999, 'LAMP-MIN-001', 15, TRUE, 'img2/1_2.png', 'Черный', 'Металл', 'small'),
(3, 'Кровать MODER', 'krovat-moder', 'Двуспальная кровать с мягким изголовьем', 45999, 55999, 'BED-MOD-001', 5, TRUE, 'img2/3_3.png', 'Серый', 'Дерево/Ткань', 'large'),
(6, 'Торшер MARCIA', 'torsher-marcia', 'Напольный торшер с регулируемой высотой', 11999, 14999, 'LAMP-MAR-001', 8, TRUE, 'img2/2_2.png', 'Золотой', 'Металл', 'tall'),
(6, 'Светильник POLET', 'svetilnik-polet', 'Подвесной светильник для гостиной', 5499, NULL, 'LAMP-POL-001', 20, TRUE, 'img2/4.jpg', 'Белый', 'Стекло', 'wide'),
(4, 'Стол NORD', 'stol-nord', 'Обеденный стол в скандинавском стиле', 23999, 28999, 'TABLE-NOR-001', 7, TRUE, 'img2/5_5.png', 'Натуральный', 'Дерево', 'small1'),
(1, 'Диван ROYALTY', 'divan-royalty', 'Роскошный угловой диван с велюровой обивкой', 78999, 95999, 'SOFA-ROY-001', 3, TRUE, 'img2/6_6.png', 'Зеленый', 'Велюр', 'wide2'),
(2, 'Кресло MINIMAL', 'kreslo-minimal', 'Кресло в минималистичном стиле', 29999, 35999, 'ARM-MIN-001', 10, TRUE, 'img2/7_7.png', 'Бежевый', 'Ткань', 'wide3'),
(4, 'Стол LONKI', 'stol-lonki', 'Журнальный столик с мраморной столешницей', 34999, NULL, 'TABLE-LON-001', 12, TRUE, 'img2/8_8.png', 'Белый мрамор', 'Мрамор/Металл', 'wide2_1'),
(1, 'Диван HEMMINS', 'divan-hemmins', 'Большой модульный диван для всей семьи', 89999, 110000, 'SOFA-HEM-001', 2, TRUE, 'img2/9_9.png', 'Темно-серый', 'Ткань', 'full-width')
ON CONFLICT (slug) DO NOTHING;
-- Выводим статистику
DO $$
DECLARE
users_count INTEGER;
categories_count INTEGER;
products_count INTEGER;
BEGIN
SELECT COUNT(*) INTO users_count FROM users;
SELECT COUNT(*) INTO categories_count FROM categories;
SELECT COUNT(*) INTO products_count FROM products;
RAISE NOTICE 'Загружено: % пользователей, % категорий, % товаров',
users_count, categories_count, products_count;
END $$;