diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..8586898
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,204 @@
+# План работ: wikipoints.ru
+
+## Статус
+
+- Бэкап БД: есть (`poi.sql`, 18 таблиц, 868 строк)
+- Google Maps API key: **просрочен** (`AIzaSyDuWmguxO35oV9WGc6D8xUPvQBaUS4kt78`)
+- SMS.ru / Alarmer: ключи устарели, но не критично
+- uLogin: **выпилить**, заменить на вход по email + пароль
+- Подтверждение email: через **Resend** (добавить колонки `email`, `password`, `emailConfirmed`)
+
+---
+
+## 1. База данных
+
+### 1.1 Импортировать `poi.sql`
+```sql
+mysql -u root -p poi < poi.sql
+```
+
+### 1.2 Расширить таблицу `users`
+```sql
+ALTER TABLE users
+ ADD COLUMN `email` varchar(255) DEFAULT NULL AFTER `uid`,
+ ADD COLUMN `password` varchar(255) DEFAULT NULL AFTER `email`,
+ ADD COLUMN `emailConfirmed` tinyint(1) NOT NULL DEFAULT 0 AFTER `password`,
+ ADD COLUMN `confirmToken` varchar(64) DEFAULT NULL AFTER `emailConfirmed`,
+ ADD UNIQUE KEY `email` (`email`);
+```
+
+Старым пользователям (через uLogin) проставить `emailConfirmed = 1` — они не могут войти по паролю, но не теряют доступ. Новые регистрируются только через email+пароль.
+
+---
+
+## 2. Авторизация (выпил uLogin)
+
+### 2.1 Новый контроллер `AuthController` или методы в `IndexController`
+
+**actionRegister** — регистрация:
+- Поля: email, пароль (2 раза), ник
+- Валидация: email уникальный, пароль ≥ 6 символов
+- Хеш пароля: `password_hash($pass, PASSWORD_BCRYPT)`
+- Генерация `confirmToken = bin2hex(random_bytes(32))`
+- Отправка письма через Resend с ссылкой `/auth/confirm?token=...`
+- После успеха: флеш-сообщение «Проверьте почту»
+
+**actionLogin** — вход (заменить старый ulogin):
+- Форма: email + пароль
+- Поиск пользователя по email, проверка `password_verify()`
+- Если `emailConfirmed = 0` — «Подтвердите email»
+- Успех: `App::userLogin($user)`, редирект
+
+**actionLogout** — без изменений (уже работает)
+
+**actionConfirm** — подтверждение email:
+- Проверить `confirmToken`, проставить `emailConfirmed = 1`, очистить токен
+- Автоматический вход, редирект на главную
+
+**actionForgot** — восстановление пароля:
+- Форма email → поиск пользователя → Resend c токеном сброса
+- Новая колонка `resetToken` (или переиспользовать `confirmToken`)
+
+**actionReset** — сброс пароля:
+- Проверить токен, показать форму нового пароля
+
+### 2.2 Модель `User` — добавить методы
+
+```php
+function getByEmail($email) { ... }
+static function hashPassword($pass) { return password_hash($pass, PASSWORD_BCRYPT); }
+function verifyPassword($pass) { return password_verify($pass, $this->password); }
+```
+
+### 2.3 Файлы для удаления/изменения
+
+| Файл | Действие |
+|------|----------|
+| `views/modals/login.php` | Переписать целиком: убрать uLogin, вставить форму email+пароль + ссылку на регистрацию |
+| `controllers/IndexController.php` (actionLogin) | Удалить ulogin-код, заменить на email/пароль |
+| `views/layouts/*.php` (скрипт ulogin) | Убрать проверку `strpos($script, 'ulogin')` |
+| `public/js/app.js` | Проверить и убрать ulogin-вызовы |
+
+### 2.4 Шаблоны
+
+- `views/modals/register.php` — новый
+- `views/modals/login.php` — переписать
+- `views/auth/confirm.php` — новый
+- `views/auth/forgot.php` — новый
+- `views/auth/reset.php` — новый
+
+---
+
+## 3. Уведомления через Resend
+
+### 3.1 Установка (без composer — вручную)
+
+Скачать и положить в `classes/`:
+- `classes/Resend.php` — простой класс-обёртка для Resend API (POST https://api.resend.com/emails)
+- API-ключ через config.php: `'resendKey' => 're_...'`
+
+### 3.2 Отправка писем
+
+- Регистрация: подтверждение email (HTML-шаблон)
+- Восстановление пароля
+- (Опционально) уведомления о новых точках/комментариях
+
+---
+
+## 4. Google Maps API ключ
+
+### 4.1 Файлы для замены
+
+| Файл | Строка |
+|------|--------|
+| `controllers/JsonController.php:384,506` | `AIzaSyDuWmguxO35oV9WGc6D8xUPvQBaUS4kt78` → новый ключ |
+
+### 4.2 Вынести в config
+
+Добавить в `config.php`:
+```php
+'googleMapsKey' => 'НОВЫЙ_КЛЮЧ',
+```
+В коде:
+```php
+$key = App::getConfig('googleMapsKey');
+```
+
+---
+
+## 5. Конфигурация
+
+### 5.1 `config.php` — изменения
+
+```php
+return array(
+ 'title' => '...',
+ 'description' => '...',
+ 'DB' => array(
+ 'host' => 'localhost',
+ 'user' => 'poi',
+ 'password' => '...',
+ 'dbname' => 'poi',
+ ),
+ 'images' => [...],
+ 'version' => trim(file_get_contents(...)),
+ 'metrika' => false, // отключить, ключей нет
+ 'ganalytics' => false, // отключить
+ 'disableSMS' => true, // SMS.ru не работает
+ 'alarmerKey' => '', // не используется
+ 'protocol' => 'http', // пока http
+ 'googleMapsKey' => '...', // новый ключ
+ 'resendKey' => 're_...', // ключ Resend
+);
+```
+
+---
+
+## 6. PHP-совместимость (минимальные правки)
+
+| Проблема | Правка |
+|----------|--------|
+| `__autoload()` | Пока работает в PHP 7.4/8.x (deprecated, но не fatal). Совместимость не сломана |
+| `die;` → `die();` | Косметика, не блокирует |
+| PHP4-конструкторы | Проверить `Model.php` и `Controller.php` — если `Class() { parent::Class(); }` — заменить на `__construct()` |
+| `mysql_*` | Уже нет, везде PDO |
+
+После всех правок проверить:
+```bash
+php -l app.php
+php -l controllers/*.php
+php -l models/*.php
+php -l classes/*.php
+```
+
+---
+
+## 7. Порядок выполнения
+
+```
+ 1. config_template.php → скопировать, настроить БД, ключи
+ 2. Импортировать poi.sql, выполнить ALTER TABLE users
+ 3. Добавить класс Resend
+ 4. Расширить модель User (email, password, getByEmail)
+ 5. Создать AuthController (register, login, logout, confirm, forgot, reset)
+ 6. Переписать views/modals/login.php (форма email+pass)
+ 7. Создать views/modals/register.php
+ 8. Создать шаблоны confirm/forgot/reset
+ 9. Заменить Google Maps ключ в JsonController
+10. Вычистить uLogin из layouts
+11. Обновить config.php (resendKey, googleMapsKey, disableSMS, metrika/ganalytics)
+12. Проверить php -l
+13. Проверить в браузере
+```
+
+---
+
+## Не делаем (осознанно)
+
+- SQL-инъекции глобально — править только критичные места
+- XSS-защита — не трогаем legacy
+- Миграции — не пишем, всё через sql-файл
+- Тесты — не пишем
+- Composer — не добавляем
+- CSRF — не добавляем
+- Спрятать пароль БД в .env — в следующей итерации
diff --git a/controllers/IndexController.php b/controllers/IndexController.php
index 34a05f6..e2217a3 100644
--- a/controllers/IndexController.php
+++ b/controllers/IndexController.php
@@ -13,11 +13,11 @@ class IndexController extends Controller
self::addScript('/js/bootstrap.min.js');
self::addScript('/js/leaflet.js');
- self::addScript('https://maps.google.com/maps/api/js?v=3');
- self::addScript('/js/tile/Google.js');
+// self::addScript('https://maps.google.com/maps/api/js?v=3');
+// self::addScript('/js/tile/Google.js');
- self::addScript('https://api-maps.yandex.ru/2.0/?load=package.map&lang=ru-RU');
- self::addScript('/js/tile/Yandex.js');
+// self::addScript('https://api-maps.yandex.ru/2.0/?load=package.map&lang=ru-RU');
+// self::addScript('/js/tile/Yandex.js');
self::addScript('/js/app.js?ver=' . App::getConfig('version'));
self::addScript('/js/jquery.cookie.js');
@@ -108,34 +108,76 @@ class IndexController extends Controller
static function actionLogin()
{
- $source = file_get_contents('https://ulogin.ru/token.php?token=' . $_POST['token'] . '&host=' . $_SERVER['HTTP_HOST']);
- $uLoginUser = json_decode($source, true);
- $uLoginUser['identity'] = md5($uLoginUser['uid'] . $uLoginUser['network'] . '=P');
+ $email = isset($_POST['email']) ? trim($_POST['email']) : '';
+ $password = isset($_POST['password']) ? $_POST['password'] : '';
- $user = User::model()->getByUID($uLoginUser['identity']);
-
- if (!$user->id) {
- $user = $user->add(array(
- 'uid' => $uLoginUser['identity'],
- 'nick' => $uLoginUser['nickname'],
- 'name' => $uLoginUser['first_name'],
- 'profile' => $uLoginUser['profile'],
- 'network' => $uLoginUser['network'],
- 'photo' => $uLoginUser['photo_big'],
- ));
-
- if (!$user) {
- return false;
- }
+ if (!$email || !$password) {
+ $_SESSION['login_error'] = 'Заполните email и пароль.';
+ App::redirect(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '/');
}
- $target = App::getBasedir().'public/img/avatars/'.$user->id.'.jpg';
- Image::createAvatar($uLoginUser['photo_big'], $target);
+ $user = User::model()->getByEmail($email);
+
+ if (!$user || !$user->id || !$user->verifyPassword($password)) {
+ $_SESSION['login_error'] = 'Неверный email или пароль.';
+ App::redirect(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '/');
+ }
+
+ if (!$user->emailConfirmed) {
+ $_SESSION['login_error'] = 'Email не подтверждён. Проверьте почту.';
+ App::redirect(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '/');
+ }
App::userLogin($user);
+ unset($_SESSION['login_error']);
App::redirect(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '/');
}
+ static function actionRegister()
+ {
+ $email = isset($_POST['email']) ? trim($_POST['email']) : '';
+ $nick = isset($_POST['nick']) ? trim($_POST['nick']) : '';
+ $password = isset($_POST['password']) ? $_POST['password'] : '';
+ $password2 = isset($_POST['password2']) ? $_POST['password2'] : '';
+
+ $error = '';
+ if (!$email || !$nick || !$password) {
+ $error = 'Заполните все поля.';
+ } elseif ($password !== $password2) {
+ $error = 'Пароли не совпадают.';
+ } elseif (strlen($password) < 6) {
+ $error = 'Пароль должен быть не менее 6 символов.';
+ } elseif (User::model()->getByEmail($email)) {
+ $error = 'Этот email уже зарегистрирован.';
+ }
+
+ if ($error) {
+ $_SESSION['register_error'] = $error;
+ App::redirect(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '/');
+ }
+
+ $user = User::model()->add(array(
+ 'uid' => md5($email . time()),
+ 'email' => $email,
+ 'nick' => $nick,
+ 'name' => $nick,
+ 'password' => User::hashPassword($password),
+ 'profile' => '',
+ 'network' => 'email',
+ 'photo' => '',
+ 'emailConfirmed' => 0,
+ 'confirmToken' => bin2hex(random_bytes(32)),
+ ));
+
+ if (!$user) {
+ $_SESSION['register_error'] = 'Ошибка регистрации. Попробуйте позже.';
+ App::redirect('/');
+ }
+
+ $_SESSION['register_success'] = 'Регистрация прошла успешно! Подтвердите email, перейдя по ссылке в письме.';
+ App::redirect('/');
+ }
+
static function actionSitemap()
{
$xml = new SimpleXMLElement('