Первая инициализация

This commit is contained in:
RedHood 2020-11-03 22:37:14 +10:00
commit d66f02294a
56 changed files with 38010 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
vendor/
cache/

41
codeception.yml Normal file
View File

@ -0,0 +1,41 @@
# suite config
suites:
acceptance:
actor: AcceptanceTester
path: .
modules:
enabled:
- WebDriver:
url: http://git.hood.su/mirzaev/task_manager/view
host: 'Mirzaev:211041a7-665c-44e6-86e1-4719c7df4f2c@ondemand.saucelabs.com'
port: 80
browser: chrome
capabilities:
platform: 'Windows 10'
- \Helper\Acceptance
# add Codeception\Step\Retry trait to AcceptanceTester to enable retries
step_decorators:
- Codeception\Step\ConditionalAssertion
- Codeception\Step\TryTo
- Codeception\Step\Retry
extensions:
enabled: [Codeception\Extension\RunFailed]
params:
- env
gherkin: []
# additional paths
paths:
tests: mirzaev/beejee/tests
output: mirzaev/beejee/tests/_output
data: mirzaev/beejee/tests/_data
support: mirzaev/beejee/tests/_support
envs: mirzaev/beejee/tests/_envs
settings:
shuffle: false
lint: true

40
composer.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "mirzaev/beejee",
"description": "Test BeeJee",
"type": "project",
"license": "AGPL-3.0-or-later",
"homepage": "https://git.hood.su/mirzaev/beejee",
"authors": [
{
"name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "red@hood.su",
"role": "Developer"
}
],
"require": {
"php": ">=7.4.0",
"ext-PDO": "^7.4",
"twbs/bootstrap": "^4.5",
"twig/twig": "^3.1"
},
"require-dev": {
"codeception/codeception": "^4.1",
"codeception/module-webdriver": "^1.0.0"
},
"autoload": {
"psr-4": {
"mirzaev\\beejee\\": "mirzaev/beejee/system"
}
},
"autoload-dev": {
"psr-4": {
"mirzaev\\beejee\\tests\\": "mirzaev/beejee/tests"
}
},
"scripts": {
"post-update-cmd": [
"cp -R vendor/twbs/bootstrap/dist/css mirzaev/beejee/system/public/css/bootstrap",
"cp -R vendor/twbs/bootstrap/dist/js mirzaev/beejee/system/public/js/bootstrap"
]
}
}

3868
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\controllers;
use mirzaev\beejee\controllers\controller;
/**
* Контроллер аутентификации
*
* @package mirzaev\beejee\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class authController extends controller
{
/**
* Аутентификация
*
* @param array $params
*
* @return array|null
*/
public function auth(array $params = null): ?array
{
$login = $params['login'] ?? $_COOKIE['login'];
$password = $params['password'] ?? $_COOKIE['password'];
if (isset($login, $password)) {
return $this->model->auth($login, $password);
}
return null;
}
/**
* Деаутентификация
*
* @param array $params
*
* @return void
*/
public function deauth(array $params = null): void
{
session_start();
// Выборочное удаление параметров сессии
unset($_SESSION['id']);
unset($_SESSION['admin']);
// session_unset();
// session_destroy();
}
/**
* Сгенерировать представление HTML-сайдбара
*
* @param array $params
*
* @return string|null
*/
public function genSidebarPanel(array $params = null): ?string
{
// Инициализация пользователя
$user = empty($this->auth()) ? false : true;
session_start();
// Инициализация админ-прав
$admin = $_SESSION['admin'];
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'auth' . DIRECTORY_SEPARATOR . 'auth_sidebar.html', ['user' => $user, 'admin' => $admin]);
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\controllers;
use mirzaev\beejee\core,
mirzaev\beejee\models\model;
use Twig\Loader\FilesystemLoader,
Twig\Environment as view;
use Exception;
/**
* Контроллер
*
* @package mirzaev\beejee\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
class controller
{
/**
* @var model $model Модель
*/
protected model $model;
/**
* @var view $view Шаблонизатор представления
*/
protected view $view;
/**
* Конструктор
*
* @return void
*/
public function __construct()
{
// Установка значения по умолчанию для модели (если будет найдена)
$this->__get('model');
// Установка значения по умолчанию для шаблонизатора представлений
$this->__get('view');
}
/**
* Отрисовка шаблона
*
* @param string $route Маршрут
*/
public function view(string $route)
{
// Чтение представления по шаблону пути: "/views/[controller]/index
// Никаких слоёв и шаблонизаторов
// Не стал в ядре записывать путь до шаблонов
if (file_exists($view = core::path() . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $route . DIRECTORY_SEPARATOR . 'index.html')) {
include $view;
}
}
/**
* Записать свойство
*
* @param mixed $name Название
* @param mixed $value Значение
*
* @return void
*/
public function __set($name, $value): void
{
if ($name === 'model') {
if (!isset($this->model)) {
$this->model = $value;
return;
} else {
throw new Exception('Запрещено переопределять модель');
}
} else if ($name === 'view') {
if (!isset($this->view)) {
$this->view = $value;
return;
} else {
throw new Exception('Запрещено переопределять шаблонизатор представления');
}
}
throw new Exception('Свойство не найдено: ' . $name);
}
/**
* Прочитать свойство
*
* @param mixed $name Название
*
* @return mixed
*/
public function __get($name)
{
if ($name === 'model') {
if (isset($this->model)) {
// Если модель найдена
return $this->model;
} else {
// Инициализация класса модели
$model = preg_replace('/' . core::controllerPostfix() . '$/i', '', basename(get_class($this))) . core::modelPostfix();
// Иначе
if (class_exists($model_class = core::namespace() . '\\models\\' . $model)) {
// Если найдена одноимённая с контроллером модель (без постфикса)
return $this->model = new $model_class;
}
return;
}
} else if ($name === 'view') {
if (isset($this->view)) {
// Если модель найдена
return $this->view;
} else {
$path = core::path() . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'views';
$loader = new FilesystemLoader($path);
return $this->view = (new view($loader, [
// 'cache' => $path . DIRECTORY_SEPARATOR . 'cache',
]));
}
}
throw new Exception('Свойство не найдено: ' . $name);
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*
* @return mixed
*/
public function __isset(string $name)
{
if ($name === 'model') {
return isset($this->model);
} else if ($name === 'view') {
return isset($this->view);
}
throw new Exception('Свойство не найдено: ' . $name);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\controllers;
use mirzaev\beejee\controllers\controller;
/**
* Контроллер основной страницы
*
* @package mirzaev\beejee\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class errorsController extends controller
{
/**
* Ответ 404
*
* @return void
*/
public function error404(): void
{
echo '404 Not Fount';
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\controllers;
use mirzaev\beejee\controllers\controller,
mirzaev\beejee\controllers\tasksController;
use PDO;
/**
* Контроллер основной страницы
*
* @package mirzaev\beejee\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class mainController extends controller
{
public function index(array $params)
{
$tasks = new tasksController;
// Нормализация
$page['current'] = filter_var($params['page'], FILTER_SANITIZE_NUMBER_INT);
$page['sort'] = filter_var($params['page'], FILTER_SANITIZE_STRING);
// Инициализация страниц заданий
$page['count'] = $tasks->count();
$page['current'] = filter_var($params['page'], FILTER_VALIDATE_INT, ['options' => ['default' => 1]]);
$page['previous'] = $page['current'] > 1 ? $page['current'] - 1 : 1;
$page['next'] = (int) $page['current'] < (int) $page['count'] ? $page['current'] + 1 : $page['current'];
// Инициализация сортировки
$sort = empty($params['sort']) ? 'id' : $params['sort'];
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'main' . DIRECTORY_SEPARATOR . 'index.html', ['page' => $page, 'sort' => $sort]);
}
}

View File

@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\controllers;
use mirzaev\beejee\controllers\controller;
/**
* Контроллер основной страницы
*
* @package mirzaev\beejee\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class tasksController extends controller
{
/**
* Записать в базу данных
*
* @param array $params
*
* @return string|null
*/
public function create(array $params): ?string
{
// Инициализация параметров
$name = $params['name'];
$email = $params['email'];
$task = $params['task'];
session_start();
if (!isset($_SESSION['task_create_last']) || (isset($_SESSION['task_create_last']) && $_SESSION['task_create_last'] < time() - 5)) {
// Если это первый вызов или последний вызов был более 5 секунд назад
// Запись задания
$this->model->create($name, $email, $task);
$_SESSION['task_create_last'] = time();
return null;
}
return 'Следующее задание можно создать через ' . (5 - (time() - $_SESSION['task_create_last'])) . ' секунд';
}
/**
* Прочитать из базы данных
*
* @param array $params
*
* @return array
*/
public function read(array $params): array
{
// Нормализация
$page['current'] = filter_var(filter_var($params['page'], FILTER_SANITIZE_NUMBER_INT), FILTER_VALIDATE_INT, ['options' => ['default' => 1]]);
$page['sort'] = filter_var($params['sort'], FILTER_SANITIZE_STRING);
// Инициализация параметров
$limit = 3;
$sort = empty($params['sort']) ? 'id' : $params['sort'];
$page = ($params['page'] - 1) * $limit;
// Чтение заданий
return $this->model->read($page, $limit, $sort);
}
/**
* Обновить в базе данных
*
* @param array $params
*
* @return void
*/
public function update(array $params): void
{
session_start();
// Нормализация
$page['id'] = filter_var(filter_var($params['id'], FILTER_SANITIZE_NUMBER_INT), FILTER_VALIDATE_INT, ['options' => ['default' => 1]]);
$page['name'] = filter_var($params['name'], FILTER_SANITIZE_STRING);
$page['email'] = filter_var($params['email'], FILTER_SANITIZE_EMAIL);
$page['task'] = filter_var($params['task'], FILTER_SANITIZE_STRING);
// Инициализация параметров
$id = (int) $params['id'];
$name = $params['name'];
$email = $params['email'];
$task = $params['task'];
$completed = $_SESSION['admin'] == 1 ? $params['completed'] : null;
// Запись задания
$this->model->update($id, $name, $email, $task, $completed);
}
/**
* Удалить из базы данных
*
* @param array $params
*
* @return void
*/
public function delete(array $params): void
{
// Инициализация параметров
$id = (int) $params['id'];
// ВНИМАНИЕ: Сессию можно подобрать брутфорсом (либо ещё как-нибудь узнать)
// Я бы не стал так делать на нормальном проекте - писал привязку к IP, например
session_start();
if ($_SESSION['admin'] == 1) {
// Если пользователь является админстратором
$this->model->delete($id);
}
}
/**
* Сгенерировать представление HTML-листа заданий
*
* @param array $params
*
* @return string
*/
public function genList(array $params): string
{
// Инициализация заданий
$tasks = $this->read($params);
// Инициализация админ-прав
session_start();
$admin = $_SESSION['admin'];
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'tasks' . DIRECTORY_SEPARATOR . 'list.html', ['tasks' => $tasks, 'admin' => $admin]);
}
/**
* Подсчитать количество заданий
*
* @return int
*/
public function count(): int
{
return $this->model->count();
}
}

View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee;
use mirzaev\beejee\router;
use PDO,
PDOException;
use Exception;
/**
* Ядро
*
* Простая реализация ядра, для пары мелочей
*
* @package mirzaev\beejee
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class core
{
/**
* @var PDO $db Соединение с базой данных
*/
private static PDO $db;
/**
* @var router $router Маршрутизатор
*/
private static router $router;
/**
* @var string $path Корневая директория
*/
private static string $path;
/**
* @var string $namespace Пространство имён
*/
private static string $namespace;
/**
* @var string $postfix_controller Постфикс контроллеров
*/
private static string $postfix_controller = 'Controller';
/**
* @var string $postfix_model Постфикс моделей
*/
private static string $postfix_model = 'Model';
/**
* Конструктор
*
* @param string $db
* @param string $login
* @param string $password
* @param router|null $router
*
* @param router $router Маршрутизатор
*/
public function __construct(string $db = 'mysql:dbname=db;host=127.0.0.1', string $login = '', string $password = '', router $router = null)
{
// Инициализация маршрутизатора
self::$router = $router ?? new router;
// Инициализация корневого пространства имён
self::$namespace = __NAMESPACE__;
try {
// Инициализация PDO
self::$db = new PDO($db, $login, $password);
} catch (PDOException $e) {
throw new Exception('Проблемы при соединении с базой данных: ' . $e->getMessage(), $e->getCode());
}
// Обработка запроса
self::$router::handle();
}
/**
* Деструктор
*
*/
public function __destruct()
{
// Закрытие соединения
}
/**
* Прочитать/записать корневую директорию
*
* @var string|null $path Путь
*
* @return string
*/
public static function path(string $path = null): string
{
return self::$path = (string) ($path ?? self::$path);
}
/**
* Прочитать/записать соединение с базой данных
*
* @var PDO|null $db Соединение с базой данных
*
* @return PDO
*/
public static function db(PDO $db = null): PDO
{
return self::$db = $db ?? self::$db;
}
/**
* Прочитать постфикс контроллеров
*
* @return string|null
*/
public static function controllerPostfix(): ?string
{
return self::$postfix_controller;
}
/**
* Прочитать постфикс моделей
*
* @return string|null
*/
public static function modelPostfix(): ?string
{
return self::$postfix_model;
}
/**
* Прочитать пространство имён
*
* @return string|null
*/
public static function namespace(): ?string
{
return self::$namespace;
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\models;
use mirzaev\beejee\models\model;
use PDO;
/**
* Модель регистрации, аутентификации и авторизации
*
* @package mirzaev\beejee\models
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class authModel extends model
{
/**
* Аутентификация
*
* @param string $login Входной
* @param string $password Пароль
*
* @return array|null
*/
public function auth(string $login, string $password): ?array
{
$user = $this->search($login);
if (empty($user)) {
return null;
}
if (password_verify($password, $user['password'])) {
// Если пароли совпадают
// Инициализация сессии
session_start();
$_SESSION['id'] = $user['id'];
$_SESSION['admin'] = $user['admin'];
// Инициализация cookies
setcookie("login", $login, ['expires' => time() + 50000, 'SameSite' => 'Strict']);
setcookie("password", $password, ['expires' => time() + 50000, 'SameSite' => 'Strict']);
return $user;
}
}
/**
* Поиск пользователя по входному псевдониму
*
* @param string $login Входной
*
* @return array|null
*/
private function search(string $login): ?array
{
// Инициализация
$request = $this->db->prepare("SELECT * FROM `users` WHERE `login` = :login LIMIT 1");
// Параметры
$params = [
':login' => $login,
];
// Отправка
$request->execute($params);
return (array) $request->fetch(PDO::FETCH_ASSOC);
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\models;
use mirzaev\beejee\core;
use Exception,
PDO;
/**
* Модель
*
* @package mirzaev\beejee\models
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
class model
{
/**
* @var PDO $db Соединение с базой данных
*/
protected PDO $db;
/**
* Конструктор
*
* @param PDO|null $db Соединение с базой данных
*/
public function __construct(PDO $db = null)
{
$this->db = $db ?? core::db();
}
/**
* Записать свойство
*
* @param mixed $name Название
* @param mixed $value Значение
*
* @return void
*/
public function __set($name, $value): void
{
if ($name === 'db') {
if (!isset($this->db)) {
$this->db = $value;
return;
} else {
throw new Exception('Запрещено переопределять соединение с базой данных');
}
}
throw new Exception('Свойство не найдено: ' . $name);
}
/**
* Прочитать свойство
*
* @param mixed $name Название
*
* @return mixed
*/
public function __get($name)
{
if ($name === 'db') {
return $this->db;
}
throw new Exception('Свойство не найдено: ' . $name);
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*
* @return mixed
*/
public function __isset(string $name)
{
if ($name === 'db') {
return isset($this->db);
}
throw new Exception('Свойство не найдено: ' . $name);
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee\models;
use mirzaev\beejee\models\model;
use PDO;
/**
* Модель заданий
*
* @package mirzaev\beejee\models
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class tasksModel extends model
{
/**
* Создать запись
*
* @param string $name Имя
* @param string $email Почта
* @param string $task Задание
*
* @return array
*/
public function create(string $name, string $email, string $task): array
{
// Инициализация
$request = $this->db->prepare('INSERT INTO `tasks` (`name`, `email`, `task`) VALUES (:name, :email, :task)');
// Параметры
$request->bindValue(':name', $name, PDO::PARAM_STR);
$request->bindValue(':email', $email, PDO::PARAM_STR);
$request->bindValue(':task', $task, PDO::PARAM_STR);
// Отправка
$request->execute();
return (array) $request->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Прочитать запись
*
* @param int $page Страница
* @param int $limit Количество заданий
* @param string $sort Сортировка
*
* @return array
*/
public function read(int $page, int $limit, string $sort): array
{
// Нормализация сортировки по белому списку
if ($sort === 'id' || $sort === 'name' || $sort === 'email' || $sort === 'task') {
// Инициализация
$request = $this->db->prepare('SELECT * FROM `tasks` ORDER BY `' . $sort . '` LIMIT :page, :limit');
// Параметры
$request->bindValue(':page', $page, PDO::PARAM_INT);
$request->bindValue(':limit', $limit, PDO::PARAM_INT);
// Отправка
$request->execute();
return (array) $request->fetchAll(PDO::FETCH_ASSOC);
}
return [];
}
/**
* Обновить запись
*
* @param int $name Идентификатор
* @param string $name Имя
* @param string $email Почта
* @param string $task Задание
* @param string $task Статус о завершении
*
* @return array
*/
public function update(int $id, string $name = null, string $email = null, string $task = null, string $completed = null): array
{
// Инициализация строки запроса
$request = 'UPDATE `tasks` SET';
// Проверка на то, что уже стоит какое-то значение
// Нужно для того, чтобы ставить запятые
$comma = false;
if (!is_null($name)) {
// Имя
$request .= '`name` = :name';
$params[':name'] = $name;
$comma = true;
}
if (!is_null($email)) {
// Почта
$request .= ($comma ? ', ' : '') . '`email` = :email';
$params[':email'] = $email;
$comma = true;
}
if (!is_null($task)) {
// Задание
$request .= ($comma ? ', ' : '') . '`task` = :task';
$params[':task'] = $task;
$comma = true;
}
if (!is_null($completed) && $completed == 1) {
// Статус завершения
$request .= ($comma ? ', ' : '') . '`completed` = NOT `completed`';
}
$request .= ' WHERE `id` = :id';
$params[':id'] = $id;
echo $request;
// Инициализация запроса
$request = $this->db->prepare($request);
// Отправка
$request->execute($params);
return (array) $request->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Удалить запись
*
* @param int $name Идентификатор
*
* @return bool
*/
public function delete(int $id): bool
{
// Инициализация запроса
$request = 'DELETE FROM `tasks` WHERE `id` = :id';
// Параметры
$params = [
':id' => $id
];
// Запрос
return (bool) $this->db->prepare($request)->execute($params);
}
/**
* Подсчитать количество заданий
*
* @return int
*/
public function count(): int
{
// Запрос к базе данных
$request = ceil($this->db->query('SELECT count(*) as count FROM `tasks`')->fetch()['count'] / 3);
return (int) (empty($request) ? 1 : $request);
}
}

View File

@ -0,0 +1,109 @@
# ----------------------------
# Host config
# ----------------------------
server {
listen %ip%:%httpport% default;
listen %ip%:%httpsport% ssl http2 default;
server_name catalog.loc %aliases%;
root '%hostdir%';
limit_conn addr 64;
autoindex off;
index index.php index.html index.htm;
ssl_certificate '%sprogdir%/userdata/config/cert_files/server.crt';
ssl_certificate_key '%sprogdir%/userdata/config/cert_files/server.key';
# ssl_trusted_certificate '';
# Force HTTPS
# add_header Strict-Transport-Security 'max-age=2592000' always;
# if ($scheme ~* ^(?!https).*$) {
# return 301 https://$host$request_uri;
# }
# Force www.site.com => site.com
# if ($host ~* ^www\.(.+)$) {
# return 301 $scheme://$1$request_uri;
# }
# Disable access to backup/config/command/log files
# if ($uri ~* ^.+\.(?:bak|co?nf|in[ci]|log|orig|sh|sql|tar|sql|t?gz|cmd|bat)$) {
# return 404;
# }
# Disable access to hidden files/folders
if ($uri ~* /\.(?!well-known)) {
return 404;
}
# Disable MIME sniffing
add_header X-Content-Type-Options 'nosniff' always;
location ~* ^.+\.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv|svgz?|ttf|ttc|otf|eot|woff2?)$ {
expires 1d;
access_log off;
}
location / {
# Force index.php routing (if not found)
try_files $uri $uri/ /index.php$is_args$args;
# Force index.php routing (all requests)
# rewrite ^/(.*)$ /index.php?/$1 last;
location ~ \.php$ {
try_files $fastcgi_script_name =404;
# limit_conn addr 16;
# limit_req zone=flood burst=32 nodelay;
# add_header X-Frame-Options 'SAMEORIGIN' always;
# add_header Referrer-Policy 'no-referrer-when-downgrade' always;
# CSP syntax: <host-source> <scheme-source>(http: https: data: mediastream: blob: filesystem:) 'self' 'unsafe-inline' 'unsafe-eval' 'none'
# Content-Security-Policy-Report-Only (report-uri https://site.com/csp/)
# add_header Content-Security-Policy "default-src 'self'; connect-src 'self'; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; object-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
fastcgi_pass backend;
include '%sprogdir%/userdata/config/nginx_fastcgi_params.txt';
}
}
# Service configuration (do not edit!)
# ----------------------------
location /openserver/ {
root '%sprogdir%/modules/system/html';
autoindex off;
index index.php index.html index.htm;
%allow%allow all;
allow 127.0.0.0/8;
allow ::1/128;
allow %ips%;
deny all;
location ~* ^/openserver/.+\.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv|svgz?|ttf|ttc|otf|eot|woff2?)$ {
expires 1d;
access_log off;
}
location /openserver/server-status {
stub_status on;
}
location ~ ^/openserver/.*\.php$ {
try_files $fastcgi_script_name =404;
fastcgi_index index.php;
fastcgi_pass backend;
include '%sprogdir%/userdata/config/nginx_fastcgi_params.txt';
}
}
# End service configuration
# ----------------------------
}
# ----------------------------
# End host config
# ----------------------------

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,326 @@
/*!
* Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors
* Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([class]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg {
overflow: hidden;
vertical-align: middle;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
[role="button"] {
cursor: pointer;
}
select {
word-wrap: normal;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors
* Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
.task_delete_buttons {
color: rgb(202, 92, 92);
}
.task_delete_buttons:hover {
color: rgb(165, 67, 67);
}
.task_buttons:hover {
cursor: pointer;
}
.task_button_update:focus, .task_button_update {
box-shadow: none;
outline: none;
border: none;
background: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee;
use mirzaev\beejee\core,
mirzaev\beejee\router;
// Автозагрузка
require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
// Инициализация корневой директории
core::path(__DIR__);
// Запись маршрутов
router::create('/', 'main', 'index');
router::create('task/create', 'tasks', 'create', 'POST');
router::create('task/update', 'tasks', 'update', 'POST');
router::create('task/delete', 'tasks', 'delete', 'POST');
router::create('tasks', 'tasks', 'genList');
router::create('tasks/count', 'tasks', 'count');
router::create('auth', 'auth', 'auth', 'POST');
router::create('deauth', 'auth', 'deauth', 'POST');
router::create('auth', 'auth', 'genSidebarPanel', 'GET');
router::create('reg', 'auth', 'reg', 'POST');
new Core('mysql:dbname=beejee;host=127.0.0.1', 'root', 'root');

View File

@ -0,0 +1,56 @@
function auth(button) {
button.removeAttribute('onclick');
let login = document.getElementById('auth_sidebar_login').value;
let password = document.getElementById('auth_sidebar_password').value;
let body = 'login=' + encodeURIComponent(login) + '&password=' + encodeURIComponent(password);
let http = new XMLHttpRequest();
http.open('POST', '/auth');
http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
task_read();
auth_html();
}
}
http.send(body);
}
function auth_html(auth) {
let http = new XMLHttpRequest();
http.open('GET', '/auth');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
document.getElementById('sidebar').innerHTML = this.responseText;
}
}
http.send(null);
}
function deauth(button) {
button.removeAttribute('onclick');
delete_cookie('login');
delete_cookie('password');
delete_cookie('admin');
let http = new XMLHttpRequest();
http.open('POST', '/deauth');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
task_read();
auth_html();
}
}
http.send(null);
}
function delete_cookie(name) {
let date = new Date();
date.setTime(date.getTime() - 1);
document.cookie = name += "=; expires=" + date.toGMTString();
}
auth_html();

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,200 @@
function task_create(form) {
let name = document.getElementById('task_create_panel_name').value;
let email = document.getElementById('task_create_panel_email').value;
let task = document.getElementById('task_create_panel_task').value;
let body = 'name=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email) + '&task=' + encodeURIComponent(task);
let http = new XMLHttpRequest();
http.open('POST', '/task/create');
http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
if (this.responseText !== '') {
document.getElementById('task_create_warning').innerHTML = this.responseText;
if (document.getElementById('task_create_warning').hasAttribute('style')) {
document.getElementById('task_create_warning').removeAttribute('style');
setTimeout(function () {
document.getElementById('task_create_warning').setAttribute('style', 'display: none;');
}, 3000);
}
} else {
if (document.getElementById('task_create_success').hasAttribute('style')) {
document.getElementById('task_create_success').removeAttribute('style');
setTimeout(function () {
document.getElementById('task_create_success').setAttribute('style', 'display: none;');
}, 3000);
}
task_read();
}
}
}
http.send(body);
}
function task_read(page) {
document.getElementById('button_forward').removeAttribute('onclick');
document.getElementById('button_back').removeAttribute('onclick');
if (typeof page === 'undefined') {
page = document.getElementById('page_сurrent').innerHTML;
if (typeof page === 'undefined') {
page = 1;
}
}
let http = new XMLHttpRequest();
http.open('GET', '/tasks/count');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
let pages_count;
document.getElementById('pages_сount').innerHTML = pages_count = this.responseText;
if (typeof pages_count === 'undefined') {
document.getElementById('pages_сount').innerHTML = pages_count = page;
}
task_page_buttons(page, pages_count);
task_read_request(page);
}
}
http.send(null);
}
function task_read_request(page) {
const url = new URL(window.location);
url.searchParams.set('page', page);
history.pushState(null, null, url);
document.getElementById('page_сurrent').innerHTML = page;
let http = new XMLHttpRequest();
http.open('GET', '/tasks' + url.search)
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
document.getElementById('tasks').innerHTML = this.responseText;
}
}
http.send(null);
}
function task_update(element) {
let id = element.getAttribute('id');
let name = element.children[1].innerHTML;
let email = element.children[2].innerHTML;
let task = element.children[3].innerHTML;
document.getElementById('form_task_create').setAttribute('onsubmit', 'task_update_send(document.getElementById(' + id + ')); return false;')
let update_button_task = document.getElementById('update_button_task_' + id);
update_button_task.parentNode.removeChild(update_button_task);
element.children[1].innerHTML = '<input type="text" class="form-control-sm">';
element.children[2].innerHTML = '<input type="email" type="text" class="form-control-sm">';
element.children[3].innerHTML = '<input type="text" class="form-control-sm">';
element.children[1].children[0].value = name;
element.children[2].children[0].value = email;
element.children[3].children[0].value = task;
element.children[4].innerHTML = '<button type="submit" class="p-0 task_button_update"><i class="fas fa-check mr-2 text-success"></i></button>' + element.children[4].innerHTML;
}
function task_update_send(element) {
let id = element.getAttribute('id');
let name = element.children[1].children[0].value;
let email = element.children[2].children[0].value;
let task = element.children[3].children[0].value;
let body = 'id=' + encodeURIComponent(id) + '&name=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email) + '&task=' + encodeURIComponent(task);
let http = new XMLHttpRequest();
http.open('POST', '/task/update');
http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
task_read();
}
}
http.send(body);
}
function task_change_status(element) {
let id = element.getAttribute('id');
let body = 'id=' + encodeURIComponent(id) + '&completed=1';
let http = new XMLHttpRequest();
http.open('POST', '/task/update');
http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
task_read();
}
}
http.send(body);
}
function task_delete(element) {
let id = element.getAttribute('id');
let body = 'id=' + encodeURIComponent(id);
let http = new XMLHttpRequest();
http.open('POST', '/task/delete');
http.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && http.status === 200) {
task_read();
}
}
http.send(body);
}
function task_sort(element) {
if (element.selectedIndex) {
const url = new URL(window.location);
url.searchParams.set('sort', element.value);
history.pushState(null, null, url);
task_read();
}
}
function task_page_buttons(current, count) {
let previous;
if (current > 1) {
previous = parseInt(current) - 1;
} else {
previous = current;
}
let next;
if (current < count) {
next = parseInt(current) + 1;
} else {
next = current;
}
if (current > count) {
current = parseInt(count) - 1;
}
if (current < 1) {
current = 1;
}
document.getElementById('button_forward').setAttribute('onclick', 'task_read(' + next + ')');
document.getElementById('button_back').setAttribute('onclick', 'task_read(' + previous + ')');
}
document.forms.tasks_create_panel.addEventListener('submit', function (event) {
if (document.forms.tasks_create_panel.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
} else {
task_create(document.forms.tasks_create_panel);
}
document.forms.tasks_create_panel.classList.add('was-validated');
}, false);
task_read();

View File

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace mirzaev\beejee;
use mirzaev\beejee\core;
/**
* Маршрутизатор
*
* @package mirzaev\beejee
* @author Arsen Mirzaev Tatyano-Muradovich <red@hood.su>
*/
final class router
{
/**
* @var array $router Маршруты
*/
public static array $routes = [];
/**
* Новый маршрут
*
* @param string $route Маршрут
* @param string $controller Контроллер
* @param string|null $method Метод
* @param string|null $type Тип
* @param string|null $model Модель
*
* @return void
*/
public static function create(string $route, string $controller, string $method = null, string $type = 'GET', string $model = null): void
{
if (is_null($model)) {
$model = $controller;
}
self::$routes[$route][$type] = [
// Инициализация контроллера с постфиксом
'controller' => preg_match('/' . core::controllerPostfix() . '$/i', $controller) ? $controller : $controller . core::controllerPostfix(),
'model' => preg_match('/' . core::modelPostfix() . '$/i', $model) ? $model : $model . core::modelPostfix(),
'method' => $method ?? '__construct'
];
}
/**
* Обработка маршрута
*
* @param string $route Маршрут
* @param string $controller Контроллер
*
* @return void
*/
public static function handle(string $uri = null): void
{
// Если не передан URI, то взять из данных веб-сервера
$uri = $uri ?? $_SERVER['REQUEST_URI'] ?? '';
// Инициализация URL
$url = parse_url($uri, PHP_URL_PATH);
// Сортировка массива маршрутов от большего ключа к меньшему
krsort(self::$routes);
foreach (self::$routes as $key => $value) {
// Если не записан "/" в начале, то записать
$route_name = preg_replace('/^([^\/])/', '/$1', $key);
if (mb_stripos($route_name, $url, 0, "UTF-8") === 0 && mb_strlen($route_name, 'UTF-8') <= mb_strlen($url, 'UTF-8')) {
// Если найден маршрут, а так же его длина не меньше длины запрошенного URL
$route = $value[$_SERVER["REQUEST_METHOD"] ?? 'GET'];
break;
}
}
if (!empty($route)) {
// Если маршрут найден
if (class_exists($controller_class = core::namespace() . '\\controllers\\' . $route['controller'])) {
// Если найден класс-контроллер маршрута
$controller = new $controller_class;
if (empty($response = $controller->{$route['method']}($_REQUEST))) {
// Если не получен ответ после обработки контроллера
// Удаление постфикса для поиска директории
$dir = preg_replace('/' . core::controllerPostfix() . '$/i', '', $route['controller']);
// Отрисовка шаблона по умолчанию
$response = $controller->view($dir);
}
echo $response;
return;
}
}
echo self::error();
}
private static function error(): ?string
{
if (
class_exists($class = core::namespace() . '\\controllers\\errors' . core::controllerPostfix()) &&
method_exists($class, $method = 'error404')
) {
// Если существует контроллер ошибок и метод-обработчик ответа 404,
// то вызвать обработку ответа 404
return (new $class(basename($class)))->$method();
} else {
// Никаких исключений не вызывать, отдать пустую страницу
// Либо можно, но отображать в зависимости от включенного дебаг режима
return null;
}
}
}

View File

@ -0,0 +1,44 @@
{% if not user %}
<form id="sidebar_auth_panel" class="row mt-4 mt-md-0 p-3 bg-white" onsubmit="auth(this); return false;">
<div class="form-group w-100">
<input id="auth_sidebar_login" class="col form-control" type="text" name="login" placeholder="Логин" required>
</div>
<div class="form-group w-100">
<input id="auth_sidebar_password" type="password" class="col form-control" name="password" placeholder="Пароль"
required>
</div>
<div class="form-group col p-0 mb-0">
<button id="sidebar_auth_button" type="submit" class="col btn btn-primary" data-toggle="button">Войти</button>
<!-- <button type="submit" class="col mt-2 btn btn-success">Зарегистрироваться</button> -->
</div>
</form>
{% else %}
<div id="sidebar_auth_panel" class="row mt-4 mt-md-0 p-3 bg-white">
<div class="col">
<div class="row">
<div class="col p-0">
<div class="w-50 px-5 px-md-0 mx-auto">
<img class="avatar img-fluid rounded-circle" src="/img/avatar.webp" alt="Аватар">
</div>
</div>
</div>
<div class="row">
<div class="col p-0">
<h4 class="mt-3 text-center">Имя Фамилия</h4>
</div>
</div>
<div class="row">
<div class="col text-center p-0">
<p class="mt-3"><b>Статус:</b>
{% if admin == 1 %}
администратор
{% else %}
пользователь
{% endif %}</p>
<button type="submit" class="col mt-2 btn btn-dark" onclick="deauth(this)"
data-toggle="button">Выход</button>
</div>
</div>
</div>
</div>
{% endif %}

View File

@ -0,0 +1,113 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet">
<link href="/css/main.css" rel="stylesheet">
<title>Задачи</title>
</head>
<body class="bg-dark">
<div class="container">
<div class="row mt-sm-4 mt-md-5">
<div class="col-md mr-md-3">
<div class="row p-4 bg-white">
<div class="col">
<div class="row p-3">
<h3 class="col mb-4 mb-sm-2 tasks_list_title">Список задач</h3>
<select class="col-sm-3 py-2 py-sm-0" id="select_sort" onchange="task_sort(this)">
<option class="d-none" selected>Сортировка</option>
{% if sort == 'id' %}
<option value="id" selected>Идентификатор</option>
{% else %}
<option value="id">Идентификатор</option>
{% endif %}
{% if sort == 'name' %}
<option value="name" selected>Имя</option>
{% else %}
<option value="name">Имя</option>
{% endif %}
{% if sort == 'email' %}
<option value="email" selected>Почта</option>
{% else %}
<option value="email">Почта</option>
{% endif %}
{% if sort == 'task' %}
<option value="task" selected>Задание</option>
{% else %}
<option value="task">Задание</option>
{% endif %}
</select>
</div>
<form id="form_task_create" onsubmit="return false;">
<table id="tasks" class="table table-hover table-responsive-sm table">
<tbody id="tasks">
</tbody>
</table>
</form>
<div class="row mt-4">
<a class="col-sm-3 col-md-2 ml-auto text-center btn btn-secondary" id="button_back"
onclick="task_read({{ page.previous }})" role="button">Назад</a>
<p class="col-2 ml-auto my-2 text-right" id="page_сurrent">{{ page.current }}</p>
<p class="my-sm-auto my-2 text-center">/</p>
<p class="col-2 mr-auto my-2 text-left" id="pages_сount">{{ page.count }}</p>
<a class="col-sm-3 col-md-2 mr-auto text-center btn btn-secondary" id="button_forward"
onclick="task_read({{ page.next }})" role="button">Вперёд</a>
</div>
</div>
</div>
<div class="row mt-4 p-4 bg-white">
<div class="col p-0">
<p id="task_create_warning" class="bg-warning mb-3 py-2 px-3 rounded" style="display: none;">
</p>
<p id="task_create_success" class="bg-success text-white mb-3 py-2 px-3 rounded" style="display: none;">
Задание успешно добавлено!
</p>
<form id="tasks_create_panel" class="row px-3 needs-validation" onsubmit="return false;"
novalidate>
<div class="col-sm-2 p-0 mr-sm-3 mb-3 mb-sm-0">
<input type="text" class="form-control" id="task_create_panel_name" placeholder="Имя"
name="name" required>
<div class="invalid-feedback">
Укажите имя
</div>
</div>
<div class="col-sm-3 p-0 mr-sm-3 mb-3 mb-sm-0">
<input type="email" class="form-control" id="task_create_panel_email"
placeholder="Почта" name="email" required>
<div class="invalid-feedback">
Неверный тип почты
</div>
</div>
<div class="col-sm p-0 mr-sm-3 mb-3 mb-sm-0">
<input type="text" class="form-control" id="task_create_panel_task"
placeholder="Задание" name="task" required>
<div class="invalid-feedback">
Укажите задание
</div>
</div>
<div class="col-sm-3 col-md-3 col-lg-2 p-0">
<button type="submit" class="btn btn-success w-100"
data-toggle="button">Добавить</button>
</div>
</form>
</div>
</div>
</div>
<div id="sidebar" class="col-md-3">
</div>
</div>
</div>
<script type="text/javascript" src="/js/auth.js"></script>
<script type="text/javascript" src="/js/tasks.js"></script>
<script src="https://kit.fontawesome.com/d7e922c226.js" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="/js/bootstrap/bootstrap.min.js"></script>
<script type="text/javascript" src="/js/bootstrap/forms_validator.js"></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
{% for task in tasks %}
<tr id="{{ task.id }}">
<td class="text-left">
{% if admin == 1 %}
{% if task.completed == 1 %}
<i class="far fa-check-square ml-1 text-dark task_buttons task_status"
onclick="task_change_status(this.closest('tr'))"></i>
{% else %}
<i class="far fa-square ml-1 text-dark task_buttons task_status" onclick="task_change_status(this.closest('tr'))"></i>
{% endif %}
{% else %}
{% if task.completed == 1 %}
<i class="far fa-check-square ml-1 text-dark task_status"></i>
{% else %}
<i class="far fa-square ml-1 text-dark task_status"></i>
{% endif %}
{% endif %}
</td>
<td class="task_name">{{ task.name }}</td>
<td class="task_email">{{ task.email }}</td>
<td class="task_task">{{ task.task }}</td>
<td class="text-right">
{% if admin == 1 %}
<i id="update_button_task_{{ task.id }}" class="fas fa-pen mr-1 task_buttons" onclick="task_update(this.closest('tr'))"></i>
<i class="fas fa-trash ml-1 task_delete_buttons task_buttons" onclick="task_delete(this.closest('tr'))"></i>
{% endif %}
</td>
</tr>
{% endfor %}

1
mirzaev/beejee/tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__output/

View File

@ -0,0 +1,146 @@
<?php
class MainCest
{
public function _before(AcceptanceTester $I)
{
$I->amOnPage('/?');
}
/**
* Проверка существования списка заданий
*
* @param AcceptanceTester $I
* @return void
*/
public function tasksListExists(AcceptanceTester $I)
{
// Cписок заданий
$I->waitForElement('#tasks');
// Строка с колонкой статуса
$I->waitForElement('//table[@id="tasks"]/tbody/tr/td/i[contains(@class, "task_status")]');
// Строка с колонкой имени
$I->waitForElement('//table[@id="tasks"]/tbody/tr/td[@class="task_name"]');
// Строка с колонкой почты
$I->waitForElement('//table[@id="tasks"]/tbody/tr/td[@class="task_email"]');
// Строка с колонкой задания
$I->waitForElement('//table[@id="tasks"]/tbody/tr/td[@class="task_task"]');
// Строк три
// $I->seeNumberOfElements('//table[@id="tasks"]/tbody/tr', 3);
}
/**
* Проверка существования панели создания задания
*
* @param AcceptanceTester $I
* @return void
*/
public function tasksCreatePanelExists(AcceptanceTester $I)
{
// Панель создания заданий
$I->waitForElement('#tasks_create_panel');
}
/**
* Проверка существования панели авторизации
*
* @param AcceptanceTester $I
* @return void
*/
public function authPanelExists(AcceptanceTester $I)
{
// Панель авторизации
$I->waitForElement('#sidebar_auth_panel');
// Первая строка с колонкой задания
$I->waitForElement('//button[@id="sidebar_auth_button"]');
}
/**
* Проверка вывода ошибки при создании задания без данных
*
* @param AcceptanceTester $I
* @return void
*/
public function createTaskWithoutData(AcceptanceTester $I)
{
// Отправка формы
$I->click('Добавить');
// Проверка вывода ошибок
$I->see('Укажите имя');
$I->see('Неверный тип почты');
$I->see('Укажите задание');
}
/**
* Проверка вывода ошибки при введении неверного email
*
* @param AcceptanceTester $I
* @return void
*/
public function createTaskWithWrongEmail(AcceptanceTester $I)
{
// Заполнение полей
$I->fillField(['name' => 'name'], 'test');
$I->fillField(['name' => 'email'], 'test');
$I->fillField(['name' => 'task'], 'test job');
// Отправка формы
$I->click('Добавить');
// Проверка вывода ошибок
$I->dontSee('Укажите имя');
$I->see('Неверный тип почты');
$I->dontSee('Укажите задание');
}
/**
* Проверка вывода ошибки при введении неверного email
*
* @param AcceptanceTester $I
* @return void
*/
public function createTask(AcceptanceTester $I)
{
// Заполнение полей
$I->fillField(['name' => 'name'], 'test');
$I->fillField(['name' => 'email'], 'test@test.test');
$I->fillField(['name' => 'task'], 'test job');
// Проверка вывода ошибок
$I->dontSee('Укажите имя');
$I->dontSee('Неверный тип почты');
$I->dontSee('Укажите задание');
// Отправка формы
$I->click('Добавить');
$I->waitForText('Задание успешно добавлено!');
$page = $I->grabTextFrom('//p[@id="pages_сount"]');
$I->openNewTab($I->executeJS("return location.protocol + '//' + location.host + location.pathname") . '?page=' . $page . '&sort=id');
$I->waitForElement('//p[@id="page_сurrent"]');
$current_page = $I->grabTextFrom('//p[@id="page_сurrent"]');
// Проверка на то, что открыта последняя страница
if ($page != $current_page) {
throw new Exception('Не удалось открыть последнюю страницу');
}
// Строка с колонкой имени
echo $name = $I->grabTextFrom('//table[@id="tasks"]/tbody/tr/td[@class="task_name"]');
// Строка с колонкой почты
echo $email = $I->grabTextFrom('//table[@id="tasks"]/tbody/tr/td[@class="task_email"]');
// Строка с колонкой задания
echo $task = $I->grabTextFrom('//table[@id="tasks"]/tbody/tr/td[@class="task_task"]');
}
}

View File

View File

View File

@ -0,0 +1 @@
<html><head></head><body></body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,6 @@
mirzaev\beejee\tests\MainCest.php:tasksListExists
mirzaev\beejee\tests\MainCest.php:tasksCreatePanelExists
mirzaev\beejee\tests\MainCest.php:authPanelExists
mirzaev\beejee\tests\MainCest.php:createTaskWithoutData
mirzaev\beejee\tests\MainCest.php:createTaskWithWrongEmail
mirzaev\beejee\tests\MainCest.php:createTask

View File

@ -0,0 +1,26 @@
<?php
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method void pause()
*
* @SuppressWarnings(PHPMD)
*/
class AcceptanceTester extends \Codeception\Actor
{
use _generated\AcceptanceTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,10 @@
<?php
namespace Helper;
// here you can define custom actions
// all public methods declared in helper class will be available in $I
class Acceptance extends \Codeception\Module
{
}

File diff suppressed because it is too large Load Diff