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(''); diff --git a/controllers/LpController.php b/controllers/LpController.php index f8dae98..3524225 100644 --- a/controllers/LpController.php +++ b/controllers/LpController.php @@ -10,6 +10,7 @@ class LpController extends Controller public function __construct() { +App::error404(); self::$layout = 'layouts/lp.php'; self::addScript('/js/jquery-2.1.0.min.js'); self::addScript('/js/jquery-ui-1.10.4.custom.min.js'); diff --git a/models/User.php b/models/User.php index 5f07cb9..5cfa9a6 100644 --- a/models/User.php +++ b/models/User.php @@ -28,6 +28,31 @@ class User extends Model } } + function getByEmail($email) + { + $res = App::DB()->query('SELECT `id` FROM `' . $this->_tableName_ . '` WHERE `email` = ' . App::DB()->quote($email)); + if ($res) { + $res = $res->fetch(PDO::FETCH_ASSOC); + if ($res) { + return $this->getByPK((int) $res['id']); + } + } + return false; + } + + static function hashPassword($password) + { + return password_hash($password, PASSWORD_BCRYPT); + } + + function verifyPassword($password) + { + if (!$this->password) { + return false; + } + return password_verify($password, $this->password); + } + function getPointsCount() { return Point::model()->getCountByUser($this->id); diff --git a/public/js/map.js b/public/js/map.js index 5a71fb5..4673d0b 100644 --- a/public/js/map.js +++ b/public/js/map.js @@ -70,18 +70,19 @@ function init(lat, lng) { L.control.scale({imperial: false}).addTo(myMap); // layers['quest'] = L.tileLayer('https://otile1-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png', {attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox | Points data © WIKIPOINTS.RU', maxZoom: 18}); - layers['sputnik'] = L.tileLayer('https://{s}.sputnik.wikipoints.ru/{z}/{x}/{y}.png', {attribution: 'Map data © OpenStreetMap contributors, Sputnik, CC-BY-SA | Points data © WIKIPOINTS.RU', maxZoom: 18}); +// layers['sputnik'] = L.tileLayer('https://{s}.sputnik.wikipoints.ru/{z}/{x}/{y}.png', {attribution: 'Map data © OpenStreetMap contributors, Sputnik, CC-BY-SA | Points data © WIKIPOINTS.RU', maxZoom: 18}); +// layers['sputnik'] = L.tileLayer('https://{s}.tilessputnik.ru/{z}/{x}/{y}.png', {attribution: 'Map data © OpenStreetMap contributors, Sputnik, CC-BY-SA | Points data © WIKIPOINTS.RU', maxZoom: 18}); layers['osm'] = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA | Points data © WIKIPOINTS.RU', maxZoom: 18}); layers['topo'] = L.tileLayer('https://topo.wikipoints.ru/?z={z}&x={x}&y={y}&ver=0216', {attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Маршруты.ру | Points data © WIKIPOINTS.RU', maxZoom: 15}); - layers['google'] = new L.Google('HYBRID'); - layers['yandexMap'] = new L.Yandex(); - layers['yandex'] = new L.Yandex('hybrid'); +// layers['google'] = new L.Google('HYBRID'); +// layers['yandexMap'] = new L.Yandex(); +// layers['yandex'] = new L.Yandex('hybrid'); - if ($.cookie('mapLayer') && $.cookie('mapLayer') == 'quest') { - setLayer('sputnik'); + if ($.cookie('mapLayer') && ($.cookie('mapLayer') != 'osm' || $.cookie('mapLayer') != 'topo')) { + setLayer('osm'); } - setLayer($.cookie('mapLayer') ? $.cookie('mapLayer') : 'sputnik'); + setLayer($.cookie('mapLayer') ? $.cookie('mapLayer') : 'osm'); initCategories(); // getPoints внутри diff --git a/update.sh b/update.sh old mode 100755 new mode 100644 diff --git a/version b/version index 00dedf6..43e6d35 100644 --- a/version +++ b/version @@ -1 +1 @@ -abcde +205fb diff --git a/views/indexTemplate.php b/views/indexTemplate.php index 2567d0e..edac41d 100644 --- a/views/indexTemplate.php +++ b/views/indexTemplate.php @@ -27,16 +27,11 @@ if (App::$user) {
× - +

Фильтры и поиск

@@ -132,7 +127,7 @@ if (App::$user) {
- + Category::model()->getCategoriesForChoise()), true) : '' ?> $categories), true) ?> diff --git a/views/layouts/clear.php b/views/layouts/clear.php index 1c3a96a..315b221 100644 --- a/views/layouts/clear.php +++ b/views/layouts/clear.php @@ -49,7 +49,7 @@ - + \ No newline at end of file diff --git a/views/layouts/default.php b/views/layouts/default.php index 0b3adfd..32c1d21 100644 --- a/views/layouts/default.php +++ b/views/layouts/default.php @@ -60,7 +60,7 @@ - + \ No newline at end of file diff --git a/views/layouts/lp.php b/views/layouts/lp.php index e980d10..6def574 100644 --- a/views/layouts/lp.php +++ b/views/layouts/lp.php @@ -13,7 +13,7 @@ - + diff --git a/views/layouts/page.php b/views/layouts/page.php index 90e6730..3457b0a 100644 --- a/views/layouts/page.php +++ b/views/layouts/page.php @@ -14,7 +14,7 @@ - + diff --git a/views/layouts/print.php b/views/layouts/print.php index e3b9bbb..9858b01 100644 --- a/views/layouts/print.php +++ b/views/layouts/print.php @@ -19,7 +19,7 @@ - + diff --git a/views/modals/login.php b/views/modals/login.php index ef69e9c..20e86e3 100644 --- a/views/modals/login.php +++ b/views/modals/login.php @@ -1,4 +1,3 @@ - -
+ \ No newline at end of file diff --git a/views/modals/register.php b/views/modals/register.php new file mode 100644 index 0000000..1ba74f0 --- /dev/null +++ b/views/modals/register.php @@ -0,0 +1,43 @@ + \ No newline at end of file diff --git a/views/page/articles.php b/views/page/articles.php index 1673933..88c2295 100644 --- a/views/page/articles.php +++ b/views/page/articles.php @@ -55,20 +55,7 @@ $oldIndex = $pagesCount+1;
-
-
- - - - -
-
+
$randomPoint), true)?> diff --git a/views/page/darkFooter.php b/views/page/darkFooter.php index 70a0997..eed07df 100644 --- a/views/page/darkFooter.php +++ b/views/page/darkFooter.php @@ -7,7 +7,6 @@

Контакты

- Телефон: +7 (904) 610-6141
EMail: info@wikipoints.ru
diff --git a/views/page/points.php b/views/page/points.php index 8a9df1a..f0d2413 100644 --- a/views/page/points.php +++ b/views/page/points.php @@ -51,16 +51,6 @@ $oldIndex = $pagesCount+1; } ?>

- - - -

@@ -104,20 +94,6 @@ $oldIndex = $pagesCount+1;
-
-
- - - - -
-

$randomPoint), true)?>
diff --git a/views/point.php b/views/point.php index 620609d..0c7bf58 100644 --- a/views/point.php +++ b/views/point.php @@ -45,17 +45,6 @@ if ($pointsAirport && $usersAirport && $pointsAirport->id != $usersAirport->id){ 4IG - - - - -
@@ -141,7 +130,7 @@ if ($pointsAirport && $usersAirport && $pointsAirport->id != $usersAirport->id){ [ authorUser->nick ?> - authorUser->getPointsCount() ?> ] isAdmin == 1): ?> - [ источник: source ?> ] + [ источник: source, 0, 40) ?>… ]

@@ -237,21 +226,6 @@ if ($pointsAirport && $usersAirport && $pointsAirport->id != $usersAirport->id){ - -
-
- - - - -
-
diff --git a/views/topNavigation.php b/views/topNavigation.php index 68f1270..6b2d5d2 100644 --- a/views/topNavigation.php +++ b/views/topNavigation.php @@ -90,4 +90,4 @@ if (App::$user && App::$user->id) {
- +