CRM для автошколы, часть 2

от автора

Первая часть: https://habr.com/ru/articles/883818/

Поговорим о текущем состоянии моей CRM, сравним с текущим релизом, на каком этапе сейчас код и какие планы. Разберем ключевые этапы. первый из них и один из самых важных:

Авторизация, сессии

В текущем релизе ПО не предусмотрена возможность одновременной работы пользователей с разных устройств. Куки хранится сразу в таблице с сотрудниками. Теперь, в новом релизе интегрирована поддержка нескольких сессии. Создана доп. таблица со след. параметрами: IP, cookie, время жизни. При любом запросе к веб странице — выполняется проверка необходимой записи в данной таблице. Так, как я говорил ранее о необходимости использовать ООП, происходит инициализация объекта Admin. В инициализации происходят 3 процедуры:

  1. Проверка значения куки в браузере

  2. Проверка данного ip адреса в черном списке

  3. Проверка сессии (checkAuth)

    function __construct(){          $this->cookie = $this->checkCookie();          $this->ban = $this->checkBan();         if($this->ban > 0) {             exit(http_response_code(403));         }          if($this->cookie == 0) {             $url = explode('/',$_SERVER['REQUEST_URI']);             header('Location:/?url='.$url[1]);             exit;         }      }      function checkBan(){          global $connect;         $result = $connect->query("SELECT `id` FROM `***` WHERE ip = ?",             [ip()],             's');         $row = $connect->fetch_array($result);         if(sizeof($row) > 0){                          $connect->query("INSERT INTO `***` (`time`,`type`,`message`,`ip`,`example`) VALUES (?, ?, ?, ?, ?)",             [time(), 'ACCESS', 'BANLIST', ip(), 'TRY ACCESS FROM BANLIST'],             'issss');         }         return sizeof($row);      }      function checkCookie(){          if((empty($_COOKIE['tmpsid'])) OR (!isset($_COOKIE['tmpsid'])) OR (strlen($_COOKIE['tmpsid']) != 32)){              if((strlen($_COOKIE['tmpsid']) != 32) AND (!empty($_COOKIE['tmpsid']))){                  $this->ban('MODIFY COOKIE', $_COOKIE['tmpsid'], 'COOKIE `tmpsid`',);              }                      $hash = md5(ip().time());                      setcookie(                 'tmpsid',                 $hash,                  [                     'expires'=>time() + (86400 * 350),                     //'secure'=> true,                     'httponly'=> true,                     'samesite'=> 'Lax',                     'path'=>'/'                 ]             );              $menus = ['fullscrenn','hidden'];              if(empty($_COOKIE['menu']) OR !in_array($_COOKIE['menu'],$menus)){                 setcookie(                     'menu',                     'fullscreen',                      [                         'expires'=>time() + (86400 * 350),                         //'secure'=> true,                         'httponly'=> true,                         'samesite'=> 'Lax',                         'path'=>'/'                     ]                 );             }                      $url = explode('/',$_SERVER['REQUEST_URI']);             header('Location:/?url='.$url[1]);             exit;                  } else {              return 1;          }      }      function checkAuth(){          global $connect;          $result = $connect->query("SELECT t1.admin,t2.* FROM *** t1 LEFT JOIN *** t2 ON t1.admin = t2.id WHERE `ip` = ? AND `cookie` = ? AND `time_end` >= ?",             [ip(), $_COOKIE['tmpsid'], time()],             'ssi');             $row = $connect->fetch_array($result);              if(sizeof($row) == 1){              $this->admin = $row[0];              if((!empty($_SESSION['admin'])) || (isset($_SESSION['admin']))){                  $this->check_session = 1;                 if($row[0]['admin'] == $_SESSION['admin']){                     $this->check_session = 1;                     return 1;                 } else {                     $this->check_session = 0;                     $this->logout();                     return 0;                 };              } else {                  $_SESSION['admin'] = $row[0]['admin'];                 $this->check_session = 1;                 return 1;              }          } else {              $this->check_session = 0;             return 0;          }      } 

Добавлено условие: если длинна cookie не равна 32 символам, то данный ip адрес блокируется и при любом запросе, crm будет отвечать 403. Далее думаю о ограничении неудачных попытках авторизации — сейчас не работал над этим, но к завершению проекта — обязательно допишу. Так же — открыт вопрос о скорости запросах. Ближе к завершению, так же как и попытки неудачной авторизации — буду думать в этом направлении о количестве и времени между запросами. К примеру, если между предидущем запросе прошло не более 1-2 секунды — кидать ip в бан лист.

Структура проекта

Ключевая директория — public_html, которая содержит в себе все директории и файлы, принимающие в себя запросы пользователя. Так же в данной папке содержится файл ajax.php , который вызывает файлы из папки engine/ajax — это различные окна редактирования, списки и обработчики запросов к БД.

require dirname($_SERVER['DOCUMENT_ROOT']).'/engine/include.php';  $filename = dirname($_SERVER['DOCUMENT_ROOT']).'/engine/'.$_GET['file'];  $files = [     'Массив файлов, доступных для вызова'  ];  $admin = new Admin;  $auth = $admin->checkAuth();  if((($auth == 1) AND ($_GET['file'] != 'ajax/auth.php') OR ($_GET['file'] == 'ajax/auth.php'))){      if(in_array($_GET['file'], $files)){          if(file_exists($filename)){                  if(in_array($_GET['file'], $files)){                      if($filename != 'ajax/auth.php' AND $auth == 1){                          require $filename;                      } elseif($filename = 'ajax/auth.php' AND $auth == 0) {                          require $filename;                      } else {                                          exit(http_response_code(403));                      }                  }                      } else {                  exit(http_response_code(403));              }          } else {                   echo 'nofile';         //$admin->ban('AJAX', 'NO_FILE', $_GET['file']);         //$admin->ban();          }  } else {      exit(http_response_code(403));  }

Для безопасности — файл может вызывать скрипты, которые находятся в массиве с наименованиями файлов. В случае, если будет вызван иной файл — ip отправляется в бан лист. Ну и проверка сесси при вызове ajax. Соответственно со стороны JS , если в ответе прилетает 403 — пользователя перенаправляет на страницу с авторизацией.

Текущий релиз: в предыдущем релизе поддержка только 1 сессии пользователя. Соответственно так же, при каждом обращении к страницам — выполнялась проверка сессии и перенаправление на авторизации в случае за неимение ее в базе. jQuery не был никак интегрирован, по этому — проверка выполнялась сразу в загружаемом файле .php.

Обработчики

Думаю, разбирать селекты из обработчиков — смысла нету, поговорим сразу о INSERT и UPDATE. Решил не делать для каждого INSERT или UPDATE отдельные обработки — а собрать их в кучу. Все наименование полей и таблиц собираются из формы, а так же: тип данных, поле с уникальным значеним, обязательно поле к заполнению. Это все собирается в json и отправляется в обработчики update.php или insert.php. Те в свою очередь выдают ответ. К примеру, если поле должно быть уникальным — перед добавлением записи или ее обновлением — выполняется запрос с введенными данными. Опять же, проверяя авторизацию пользователя.

<div class="darkmode-window-header">         <h3>Добавить новую учебную группу</h3>         <a onclick="modalClose()">&times;</a>     </div>     <div class="darkmode-window-body">         <form id="add">         <input type="hidden" name="table" value="1"> // Ключ массива с таблицами         <input type="hidden" name="admin" value="123" data-type="i">         <div class="darkmode-window-input"><span><b>Основная информация</b></span><span><hr></span></div>         <div class="darkmode-window-input">             <span>Школа</span>             <span>                 <select name="school" data-type="i"><option value="2">ЧОУ ДПО «Автошкола Максимум»</option><option value="5">ЧОУ ДПО «Автошкола Максимум»(Филиалы)</option></select>             </span>         </div>         <div class="darkmode-window-input">             <span>Наименование</span>             <span><input type="text" name="title" data-empty="false" data-unikey="true" data-type="s"></span>         </div>          </div>     <div class="darkmode-window-footer">         <button class="btn" onclick="add()"><i class="fa fa-plus"></i> Добавить</button>         </form>     </div>

К примеру — наименование. data-empty=»false» — указывает на то, что поле не должно быть пустым. data-unikey=»true» — указывает на то, что поле должно иметь уникальное значение. data-type=»s» — тип данных в базе «строка». Но для сложных записей в таблицах, где с нескольких input формируется одно значение — ввожу data-field и по нему уже задаю произвольную обработку.

Текущий релиз: Имеется реализация открывания окон, но все окна добавления записей в бд — были уже загружены на странице соответствующим файлом. А js только задавал ему свойство — display. Так же, этот же файл html кодом и содержал бэк часть с обработкой этих запросов.

Контент

К примеру, разберем запрос к списку учебных групп: /groups/ . Запрос идет к файлу index.php, который находится в данной директории. Он в свою очередь подгружает необходимые инклуды для работы ПО: подключение к бд, объекты, файл с функциями, autoload.php

require dirname($_SERVER['DOCUMENT_ROOT']).'/engine/include.php';  $admin = new Admin;  if($admin->checkAuth() == 1){      if($admin->check_session == 0){          header('Location:/');         exit;              } else {          require 'header.php';         require dirname($_SERVER['DOCUMENT_ROOT']).'/engine/template/main.php';          if(isset($_REQUEST['id'])){ ?>                      <script>                              let form = {};                 getItem('ajax/groups/item.php',<?php echo $_REQUEST['id']; ?>);                          document.querySelector('.panel-tools').addEventListener('change', function(event) {                              if (event.target.tagName === 'SELECT') {                         getItem('ajax/groups/item.php',<?php echo $_REQUEST['id']; ?>);                     }                          });                          </script>                      <?php } else { ?>                      <script>                          let form = {};                 getList('ajax/groups/list.php');                          document.querySelector('.panel-tools').addEventListener('change', function(event) {                              if (event.target.tagName === 'SELECT') {                         getList('ajax/groups/list.php');                     }                          });                      </script>                  <?php }              }  } else {      $url = explode('/',$_SERVER['REQUEST_URI']);     header('Location:/?url='.$url[1]);     exit;  }

После подключения необходимых для работы файлов — инициализация объекта Admin -> проверка сессии, и подключение файла header.php, который находится вместе с index.php. Данный файл хранит в себе html код, который будет отображен в шаблоне, title страницы, запрос к базе, если указан ID в запросе:

if(isset($_REQUEST['id'])){      $_REQUEST['id'] = preg_replace('/[^\d]/', '', $_REQUEST['id']);      $result = $connect->query("SELECT t1.title AS group_title, t2.title FROM auto_users_groups t1 JOIN auto_categories t2 ON t1.category = t2.id  WHERE t1.id = ? LIMIT 1",         [$_REQUEST['id']],'i');      $group = $connect->fetch($result);      $title = 'Группа: '.$group['group_title'].', категория: '.$group['title'].' | '.$_ENV['HOME'];      $content = '<div class="schedule-header">                 <h3>                     <i class="fa fa-users"></i>                      Учебная группа '.$group['group_title'].', Категория обучения: '.$group['title'].'                 </h3>             </div>             <div class="panel-tools">                 <a onclick="modal(\'add-user-group\','.$_REQUEST['id'].')" class="btn"><i class="fa fa-user-plus"></i> Ученик(и)</a>                 <a onclick="modal(\'edit-group\','.$_REQUEST['id'].')" class="btn empty"><i class="fa fa-pencil"></i> Редактировать</a>                 <div style="position:relative;">                     <div class="drop-down-list" data-link="docs_all">                         <a href="/src/docs/order_start_b.php?group=550" style="margin-bottom:2px;"><i class="fa fa-print"></i> Приказ о создании группы</a>                         <a href="/src/docs/protocol.php?group=550" style="margin-bottom:2px;"><i class="fa fa-print"></i> Протокол группы</a>                         <a href="/src/docs/journal.php?group=550" style="margin-bottom:2px;"><i class="fa fa-print"></i> Журнал группы</a>                         <a href="/src/docs/order_2022.php?group=550" style="margin-bottom:2px;"><i class="fa fa-print"></i> Приказ группы 2022</a>                         <a href="/src/docs/order_finish_b.php?group=550" style="margin-bottom:2px;"><i class="fa fa-print"></i> Приказ об отчислении</a>                     </div>                 </div>                 <a onclick="dropdown(\'docs_all\')" class="btn empty drop">                     <i class="fa fa-file-text-o"></i> Документы <i class="fa fa-caret-down"></i>                 </a>                 <input type="text" name="search-list" id="search-f" onkeyup="tableSearch(\'contract-list\',\'search-f\')" placeholder="Поиск по таблице">                 <label style="flex-grow:1;"></label>                 <label>                     Статус ученика                     <select name="block">                         <option value="" selected>- Все</option>                         <option value="1">Заблокирован</option>                         <option value="0">Не заблокирован</option>                     </select>                 </label>                 <label>                     Документы                     <select name="docs">                         <option value="" selected>- </option>                         <option value="problems">Неактуальные</option>                         <option value="false">Отсутсвует</option>                         <option value="true">В порядке</option>                     </select>                 </label>                 <label>                     Баланс                     <select name="balance">                         <option value="" selected>- Все</option>                         <option value="false">Должники</option>                         <option value="problems">Баланс минус</option>                        </select>                 </label>             </div>             <div class="table-scrolled-x">             </div>';  } else {      $title = 'Учебные группы | '.$_ENV['HOME'];      $content = '<div class="schedule-header">                 <h3>                     <i class="fa fa-users"></i>                      Список учебных групп                 </h3>             </div>             <div class="panel-tools">                 <a onclick="modal(\'add-group\')" class="btn"><i class="fa fa-user-plus"></i> Новая группа</a>                 <input type="text" name="search-list" placeholder="Поиск по таблице">                 <label style="flex-grow:1;"></label>                 <label>                     Категория                     <select name="category">                         <option value="">- Все</option>                         <option value="1">A</option>                         <option value="2">B</option>                         <option value="3">C</option>                         <option value="4">D</option>                     </select>                 </label>                 <label>                     Актуальность                     <select name="date">                         <option value="">- Все</option>                         <option value="true" selected>Актуальные</option>                         <option value="false">Неактуальные</option>                     </select>                 </label>             </div>             <div class="table-scrolled-x">             </div>';  }

И так же в index.php — подключается файл с основным html содержимым страницы (шаблон). Все это собирается в 1 кучу и получается контент. Да, возможно метод не самый оптимальный и в комментариях будет куча учителей — и это хорошо. На этапе разработки будет полезно почитать для себя какую-то информацию, и правильно ее применить на фронте работ.

Текущий релиз: 2 фала в директории, которая открывается пользователем. index.php — содержит в себе шаблон и переменную $content, в которую вносит html код и логику из файла в той же директории — functions.php. Фрон и бэк в куче.

На какой стадии и какой план

Стадия — сырой скилет. Ключевые моменты реализованы — сейчас рутина с формированием таблиц, окон редактирования и т.д. Пока занимаюсь этим. После перейду к детальным связкой учеников — договоров, договоров — прочим (графиком вождения, финансовым операциям и т.д.).

Я не считаю, себя крутым разработчиком и не беру за данную работу 6-ти значную сумму, и понимаю то, что есть много вещей и нюансов, которые я не знаю. Все мы учимся. Иногда обучение происходин на своих ошибках, иногда на чужих. Но это код, и он работает — сделать лучше можно в любой момент(сел, открыл. переписал, перезаписал). На любого программиста — всегда найдется программист по лучше. Прошу сильно не критиковать, а подсказать совет — всегда пожалуйста!


ссылка на оригинал статьи https://habr.com/ru/articles/883940/