добавляю все изменения проекта на текущий момент

This commit is contained in:
vasya
2026-02-27 18:49:27 +03:00
parent e927910fda
commit 9c35f4e35e
303 changed files with 79434 additions and 2558 deletions
@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
/**
* Базовый класс для описания команд по расписанию
* Создан, в частности, чтобы предоставить универсальный функционал для всех команд, запускаемых по расписанию
*/
abstract class BaseScheduleCommand extends Command
{
/**
* Метод выполнения команды
*
* @param callable $execFunc функция с логикой выполнения команды
* @return void
*/
protected function executeCommand(callable $execFunc): void
{
try {
$execFunc();
} catch (\Throwable $th) {
$this->logExecErr($th);
}
}
/**
* Метод логирования ошибки выполнения скриптов по расписанию
*
* @param \Throwable $th
* @return void
*/
private function logExecErr(\Throwable $th): void
{
$context = [
//Скрипт откуда запустилась задача
'command' => static::class,
//Скрипт, в котором произошла ошибка
'file' => $th->getFile(),
'line' => $th->getLine(),
];
\Log::channel('schedule_err')->error($th->getMessage(), $context);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\TestData;
class testDataCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'data_base:test-data-command';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Тестовая команда';
/**
* Execute the console command.
*/
public function handle()
{
$testDataModel = new TestData;
$test = TestData::where('test_char', 'hellos')->get();
var_dump($test);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
//activity()->log('Look mum, I logged something');
#Гаврилов
//ПРОВЕРИТЬ, РАБОТАЕТ?
// $schedule->command('sanctum:prune-expired --hours=24')
// ->daily();
//->dailyAt('21:03');
// activity()
// ->byAnonymous()
// ->inLog('Shedule')
// ->event('clear_old_Sanctum_tokens');
$schedule->command('taxi:send-today-orders-mail')->dailyAt('08:00');
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Dto;
class ApiResponseDto
{
public function __construct(
public readonly ?string $message = null,
// public readonly ?int $statusCode = 200,
public readonly mixed $data = null,
public readonly mixed $meta = null,
)
{
// return new self($this->message, $this->statusCode, $this->data, $this->meta);
}
// public static function success()
// {
// return new self($message, $data, $statusCode = 200);
// }
// public static function error()
// {
// return new self($message, $data, $statusCode = 400);
// }
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Enums;
/**
* Бизнес действия, доступные для логирования через activity_log
* @author dgavrilov
*/
#Гаврилов
//ПОЛУЧИТСЯ ЛИ ПЕРЕДАТЬ НА ФРОНТ ЭТОТ ENUM
//ГАВРИЛОВ
//ИДЕНТИЧНЫЙ СПРАВОЧНИК УКАЗАН НА СТОРОНЕ react в компоненте ENTITYHISTORY. ЕСТЬ ВОЗМОЖНОСТЬ ОБЪЕДИНИТЬ ИХ?
enum LogBusinessEvents: string
{
case Create = 'create';
case Edit = 'edit';
case Archive = 'archive';
case Restore = 'restore';
case Delete = 'delete';
case Cancel = 'cancel';
public function title(): string
{
return match($this)
{
self::Create => 'создание',
self::Edit => 'редактирование',
self::Archive => 'архивация',
self::Restore => 'восстановление',
self::Delete => 'удаление',
self::Cancel => 'отмена',
};
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
use App\Services\RedisNotificationService;
class RedisNotifications extends Facade
{
protected static function getFacadeAccessor()
{
return \App\Services\RedisNotificationService::class;
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
/**
* Фасад для доступа к user сервису без инъекции зависимостей
*
* @see \App\Services\UserService
*/
class UserContext extends Facade
{
protected static function getFacadeAccessor()
{
return \App\Services\UserService::class;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\AccessModel;
class AccessListController extends Controller
{
public function getAccess($id = null)
{
$accessListModel = new AccessModel();
$accessListData = $accessListModel::where(['access_id' => $id])->select('access_id')->get();
if ($id) {
echo '<pre>'; var_dump($accessListData->toArray()); echo'</pre>';
} else {
var_dump($accessListModel::all());
}
}
public function postAccess(Request $rqst)
{
$accessListModel = new AccessModel();
$accessListModel->role = $rqst['role'];
$accessListModel->title = $rqst['title'];
$accessListModel->save();
}
public function delAccess($id)
{
$accessListModel = new AccessModel();
//$accessListModel::where(['access_id' => $id])->delete();
$accessListModel::destroy($id);
}
}
@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Spatie\Activitylog\Models\Activity;
// use App\Services\ModuleService;
use App\Services\UserService;
use App\Facades\UserContext;
use App\Services\ApiResponder;
use App\Dto\ApiResponseDto;
use App\Enums\LogBusinessAction;
use App\Models\User;
use Illuminate\Http\JsonResponse;
class AppHistoryController extends Controller
{
public function __construct(protected ApiResponder $apiResponder)
{}
public function getAppHistory($appName, $subjectId): JsonResponse
{
#Гаврилов
//ПОКА НЕ РЕАЛИЗОВЫВАЮ ОГРАНИЧЕНИЕ ДОСТУПА К ИСТОРИИ ПО РОЛЯМ (РОЛЬ ИСТОЧНИКА ЗАПРОСА НЕ ЗАПРАШИВАЕТСЯ И НЕ ВАЛИДИРУЕТСЯ, ТОЛЬКО НАЛИЧИЕ ДОСТУПА К ПРИЛОЖЕНИЮ)
if (array_key_exists($appName, UserContext::getUserAppPermissions()) === false) {
$this->apiResponder->setDto(new ApiResponseDto('Отсутствует доступ к ресурсу'));
return response()->json($this->apiResponder->error(), 403);
}
$entityHistory = Activity::where('log_name', $appName)
->where('subject_id', $subjectId)
->get()
->toArray();
$this->apiResponder->setDto(new ApiResponseDto(null, $this->formatHistory($entityHistory)));
//echo '<pre>'; var_dump($this->apiResponder->success()); echo'</pre>';
//return $this->apiResponder->success();
// return response()->json();
return $this->apiResponder->success();
// die();
// if (empty($entityHistory)) {
// $this->apiResponder->setDto(new ApiResponseDto());
// return response()->json($this->apiResponder->error());
// }
// $this->apiResponder->setDto(new ApiResponseDto('', ));
// return response()->json($this->apiResponder->error());
// die();
// //$appName = $moduleName ?? (new ModuleService())->getModuleName();
// //echo '<pre>'; var_dump(UserService->getUserAppPermissions()); echo'</pre>';
// echo '<pre>'; var_dump($rqst->subject_id); echo'</pre>';
// echo '<pre>'; var_dump($appName); echo'</pre>';
// if (!$appName) {
// #Гаврилов
// //ВОЗВРАЩАЕМ ОШИБКУ, ЕСЛИ ИСТОРИЯ НЕ НАЙДЕНА. ИЛИ НА УРОВНЕ ФРОНТА ПРОВЕРЯЕМ ПУСТАЯ ЛИ ИСТОРИЯ И ВЫВОДИМ ВСПЛЫВАЮЩЕЕ ОКНО что "ИСТОРИЯ ОТСУТСТВУЕТ"
// return '';
// }
// return '';
// // $entityHistory = Activity::where('log_name', 'Taxi')
// // ->where('subject_id', $entityId)
// // ->get();
// // return $entityHistory;
}
private function formatHistory($historyData)
{
$formattedHistory = [];
foreach ($historyData as $historyAction) {
$historyChange = [];
#Гаврилов
//ДОЛЖНО ЛОГИРОВАТЬСЯ ID АУТЕНТИФИЦИРОВАННОГО ПОЛЬЗОВАТЕЛЯ. сЕЙЧАС ЗАПИСЫВАЕМ КОЛХОЗНО В PROPERTIES.
//ПРОВЕРЬ ЧТО ДЕЛАТЬ, ЕСЛИ ПОЛЬЗОВАТЕЛЬ - API?
$historyChange['changeAction'] = $historyAction['business_event'];
$user = User::find($historyAction['causer_id']);
$login = $user->login;
$historyChange['changeAuthor'] = $login;
$actionDate = new \DateTime($historyAction['created_at']);
$historyChange['changeDate'] = $actionDate->format('d.m.Y');
$historyChange['changeTime'] = $actionDate->format('H:i:s');
$historyChange['changeDetails'] = json_encode($historyAction['properties']['attributes'], JSON_UNESCAPED_UNICODE);
$formattedHistory[] = $historyChange;
}
//ГАВРИЛОВ. СОЗДАЙ СЕРВИС. В НЕМ РЕАЛ3ИЗУЙ МЕТОДЫ НАЗНАЧЕНИЯ ЗНАЧЕНИЙ КАЖДОМУ СВОЙСТВУ ОБЪЕКТА ИСТОРИИ . ИЗ КОНТРОЛЛЕРА СОЗДАВАЙ ЭКЗЕМПЛЯР КЛАССА СЕРВИСА. В КОНТРОЛЛЕРЕ СЕРВИСА ПО ОЧЕРЕДИ ВЫЗЫВАЙ ВСЕ МЕТОДЫ НАЗНАЧЕНИЯ СВОЙСТВ ИСТОРИИ.
return $formattedHistory;
//changeAction: string, //Совершенное действие: создание, удаление, редактирование, архивирование и т.д.
// changeAuthor: string, //Автор изменения: логин, сервисная УЗ
// changeDate: Date //Объект даты изменения. В объекте будет либо время (тогда показываем), либо время будет отсутствовать (тогда не показываем) //что будем передавать? объект new Date или библиотечный объект для работы с датами?
// changeTime?: string //Время изменений. Передавать не нужно, на этабе сборки компонента будет формироваться на основе пропса changeDate
// changeDetails?: HistoryEntityElDetails[]
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\AuthorizationService;
use App\Facades\UserContext;
/**
* Контроллер авторизации
*/
class AuthorizationController extends Controller
{
public function __construct(AuthorizationService $authorizationService)
{
}
public function getUserRole($moduleName)
{
$userPermissions = UserContext::getUserAppPermissions();
//Проверяем есть ли у пользователя в принципе доступ к приложению
if (array_key_exists($moduleName, $userPermissions) !== false) {
return response()->json(['userRole' => $userPermissions[$moduleName]], 403);
} else {
return response()->json(['message' => 'Приложение недоступно'], 403);
}
}
}
+6 -2
View File
@@ -2,7 +2,11 @@
namespace App\Http\Controllers;
abstract class Controller
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
//
use AuthorizesRequests, ValidatesRequests;
}
+248
View File
@@ -0,0 +1,248 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use LdapRecord\Models\ActiveDirectory\User as LdapUserInfo;
use App\Models\User;
use App\Facades\UserContext;
use App\Services\AuthorizationService;
use App\Services\ApiResponder;
use App\Dto\ApiResponseDto;
class LoginController extends Controller
{
#Гаврилов
//КОГДА ПЕРЕЙДЕМ НА ГРУППЫ, ПОМЕНЯТЬ НА АДМИНСКУЮ ГРУППУ? ИЛИ ОСТАВИТЬ РАССЫЛКУ?
//ПЕРЕНЕСИ В .ENV И ВНИЗУ ПО КОДУ ГДЕ ОБРАЩАЕШЬСЯ К ЭТОМУ СВОЙСТВУ КЛАССА ПЕРЕПИШИ НА ПОЛУЧЕНИЕ СВОЙСТВА ИЗ ПЕРЕМЕННОЙ ОКРУЖЕНИЯ
/**
* @var string почтовая рассылка, куда входят админы
*/
private $adminGroup = '# Magic_admins';
/**
* @var array массив групп, которые не должны участвовать в авторизации пользователя и поэтому могут не храниться
*/
#Гаврилов
//ИСПОЛЬЗУЙ МАССИВ НИЖЕ, ЧТОБЫ УДАЛЯТЬ ГРУППЫ И НЕ ХРАНИТЬ ИХ В ПРОФИЛЕ ПОЛЬЗОВАТЕЛЯ. ИЛИ ЗАБИТЬ ХЕР И ХРАНИТЬ ВСЕ? ТОГДА УДАЛИ МАССИВ НИЖЕ
private $unnecessaryGroups = array(
'MCO.',
'ARSNOVA.',
'CHATBOT.',
'FOA_PROJECTS.',
'MAXOPTRA.',
'WEBSIGNER.',
'ECM.',
'BASIS.',
'MCO_DOCUMENT.',
'FACTOR.',
'BIP.',
'NKK.',
'CHATS.',
'IB_BSC.',
'DASHAAI.',
'VDI.',
'CTX',
'AP.AE.',
'Way4',
'sg.',
'AD.TEDDY',
'AP.APPV_',
'AP.BI_',
'AP.Citrix_',
'AP.CSD_',
'AP.EFK_',
'AP.FlexiCapture_',
'AP.HPSM.ACCESS.1',
'AP.HPSM.ACCESS.3',
'AP.HPSM.APPR_BR.Collection.',
'AP.HPSM.APPR_BR.CS.',
'AP.HPSM.ACCESS.CC',
'AP.HPSM.ACCESS.123',
'AP.HPSM.ACCESS.1.2',
'AP.HPSM.APPR',
'AP.OCP',
'AP.IBS_',
'AP.Intranet_',
'AP.Intranet.',
'AP.Jenkins_',
'AP.Kibana.',
'AP.LICA.',
'AP.MailSteam.',
'AP.MCO_',
'AP.MDW.',
'AP.POCHTA_',
'AP.PREPROD',
'AP.Prometheus',
'AP.RDS_',
'AP.SAS_',
'AP.Seguranzza_',
'AP.TEST.LICA.',
'AP.Test.MCO_',
'AP.TEST',
'App_',
'BTA_',
'Calculations ',
'Cards_',
'CC.',
'CCS_',
'Citrix',
'Collection_HR_',
'Collection_All',
'CS_',
'CSD_',
'DA_',
'DB.',
'DB_',
'Deny_',
'Diasoft ',
'Digital ',
'DOR_',
'ECM_',
'FS.',
'FW.',
'INFO_',
'MDC.',
'NRM_Collection',
'PFA.',
'RenTest_',
'ReportingGroup ',
'RS.',
'SG.',
'SP.',
'SRV',
'SRVFI09_',
'SSO_',
'test',
'TR_',
'WWW_',
'MailStream',
'SRVTST',
);
public function __construct(private AuthorizationService $authorizationService, private ApiResponder $apiResponder)
{
}
#Гаврилов
//ПЕРЕИМЕНУЙ КОНТРОЛЛЕР НА AUTHcoNTROLLER
/**
* Метод завершения пользовательской сессии
*
* @return void
*/
public function logout()
{
//Удаляем все sanctum токены пользователя. Удаление всех токенов приведет к выходу пользователя со всех устройств, где он был залогирован в Magic
User::where('login', UserContext::getUserLogin())->first()->tokens()->delete();
session()->invalidate();
#Гаврилов
//ВЫЗОВ СКРИПТА НА СТАРОМ МЭДЖИКЕ ДЛЯ УДАЛЕНИЯ КУК АУТЕНТИФИКАЦИИ (ЛОГИН И ГРУППЫ)
return redirect('/login');
}
#Гаврилов
//РАЗНЕСИ ЛОГИКУ МЕЖДУ МЕТОДАМИ, А ТО ПОКА ВСЯ ЛОГИКА В ОДНОМ МЕТОД DLAPCHECK
public function ldapCheckUser (Request $request)
{
if ($request->_auth_login) {
if (Auth::attempt([
'samaccountname' => $request->_auth_login,
'password' => $request->_auth_password,
])) {
$userGroups = $this->getUserGroups($request->_auth_login);
session()->put('_auth_login', $request->_auth_login);
session()->put('_auth_groups', $userGroups);
//Если пользователь зашел впервые - записываем его логин в таблицу users. Она нужна для корректного взаимодействия с пакетом Sanctum
$user = User::firstOrCreate(
['login' => $request->_auth_login],
);
//Удаляем все предыдущие sanctum токены пользователя
User::where('login', $request->_auth_login)->first()->tokens()->delete();
//Определяем на какую страницу нужно бросить пользователя после успешной аутентификации. По умолчанию кидаем в меню
$redirectUrl = session()->has('_auth_prev_page') ? session()->get('_auth_prev_page') : ('/menu');
$isAdminFlag = in_array($this->adminGroup, $userGroups);
//Кладем в сессию информацию о том является ли пользователь админом
session()->put('is_admin', $isAdminFlag);
//Устанавливаем в пользовательский сервис параметры пользователя
UserContext::setUserLogin($request->_auth_login);
UserContext::setUserADGroups($userGroups);
UserContext::setUserEmails($userGroups);
UserContext::setIsAdminFlag($isAdminFlag);
$userPermissions = $this->authorizationService->getUserAppPermissions();
UserContext::setUserAppPermissions($userPermissions);
//Генерим Sanctum токен, чтобы поместить его в куки при редиректе
$token = $user->createToken('sanctum-token', [
'permissions' => $userPermissions
//Устанавливаем время жизни sanctum токена синхронно с временем жизни сессии из конфига
], now()->addHours(config('app.session_life_time') / 60))->plainTextToken;
return redirect($redirectUrl)
->withCookie('sanctum_token', $token, 60 * 24, '/', null, true, true);
} else {
#Гаврилов
//СООБЩЕНИЕ ОБ ОШИБКЕ ПРИ НЕУДАЧНОЙ АУТЕНТИФИКАЦИИ
return redirect('/login');
}
}
}
/**
* Метод фонового обновления санктум токена при получении 401 ошибки в ответе api ендпоинта
*
* @param Request $request
* @return void
*/
public function silentRefreshUserSanctumToken(Request $request)
{
//Если сессия истекла - возвращаем 401 ошибку и редирект на /login в axios
if (!Auth::check()) {
$this->apiResponder->setDto(new ApiResponseDto(null, ['token_refresh' => false]));
return response()->json($this->apiResponder->error(), 401);
}
$token = $request->cookie('sanctum_token');
$accessToken = \Laravel\Sanctum\PersonalAccessToken::findToken($token);
//Если токен "протух" - продлеваем его на час
if (now()->diffInMinutes($accessToken->expires_at, false) < 1) {
$accessToken->update(
[
'expires_at' => now()->addHours(1)
]
);
$this->apiResponder->setDto(new ApiResponseDto(null, ['token_refresh' => true]));
return response()->json($this->apiResponder->success());
} else {
//Если токен еще "свежий" - значит причина 401 ошибки в чем-то другом. Не обновляем токен, просто возвращаем 401 ошибку и редирект на /login в axios
$this->apiResponder->setDto(new ApiResponseDto(null, ['token_refresh' => false]));
return response()->json($this->apiResponder->error(), 401);
}
}
/**
* Метод получает группы пользователя из ldap
*
* @param string $userLogin логин пользователя, чьи группы получаем
* @return array
*/
public function getUserGroups($userLogin)
{
$userGroups = [];
$ldapUser = LdapUserInfo::findBy('samaccountname', $userLogin);
$ldapUser->memberOf;
if (isset($ldapUser->memberOf)) {
foreach ($ldapUser->memberOf as $ldapGroupInfo) {
$CN_group = substr($ldapGroupInfo, 0, stripos($ldapGroupInfo, ","));
$groupName = str_replace(array('CN=', '\\'), array('', ''), $CN_group);
$clearGroupName = trim($groupName);
if ($clearGroupName && $clearGroupName == str_replace($this->unnecessaryGroups, '', $clearGroupName)) {
$userGroups[] = $clearGroupName;
}
}
}
return $userGroups;
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models;
class MenuController extends Controller
{
public function __construct()
{
$this->userLogin = 'dgavrilov';
}
/**
* Получение всех записей из таблицы с приложениями для меню
* TODO
* передавай флаг isActive и используй его для получения записей с приложениями меню архивных и не архивных
*
* @return mixed
*/
public function getApps()
{
//Приложения старого мэджик из скрипта Config_AD
$oldApps = $this->getOldApps();
#Гаврилов
//НОВЫЕ ПРИЛОЖЕНИЯ НЕ ИСПОЛЬЗУЕШЬ
//Приложения нового мэджик из базы данных
#Гаврилов
//отрисовывать только то, что может видеть пользователь
$newApps = Models\MagicApps::get()->toArray();
return response()->json($oldApps);
}
/**
* Метод получает конфиг со старыми приложениями Magic
*
* @return string
*/
public function getOldApps()
{
$oldAppConf = json_decode(file_get_contents('http://cs/magic/return_config.AD.php'));
return $oldAppConf;
}
/**
* Метод должен проверить есть ли избранные приложения у пользователя. Если есть, их необходимо обновить либо добавив, либо убрав то приложение, по которому кликнул пользователь. Если избранных приложений нет - вставить новую запись
*
* @return boolean
*/
public function updateUserFavApp(Request $rqst)
{
#Гаврилов
//ВНИЗУ ИСПОЛЬЗУЕТСЯ ЛОГИН DGAVRILOV, ПЕРЕПИШИ НА ЛОГИН ПОЛЬЗОВАТЕЛЯ MAGIC
$userFavApp = $this->getUserFavApp('dgavrilov');
$appName = $rqst->all()['appName'];
$newFavAppList = null;
//Если массив с избранными приложениями пустой - пользователь не добавил ни 1 приложения мэджик в избранное
if (!$userFavApp) {
$newFavAppList = $appName;
} else {
$currentFavApp = $userFavApp;
if (in_array($appName, $currentFavApp) !== false) {
$newFavAppList = implode(';', array_filter($currentFavApp, function($el) use($appName) {return $el !== $appName;}));
} else {
$newFavAppList = implode(';', array_merge($currentFavApp, [$appName]));
}
}
if ($newFavAppList) {
#Гаврилов
//ПРИ ВЫЗОВЕ ИЗ REACT НЕ ОБНОВЛЯЕТСЯ ПОЛЕ UPDATE_AT
$this->insertFavApp($newFavAppList);
//Если список приложений пуст - просто удаляем запись с избранными приложениями пользователя
} else {
$this->delAllUserFavApp('dgavrilov');
}
return $this->getUserFavApp('dgavrilov');
}
#Гаврилов
//try catch для отслеживания ошибок вставки записей в БД?
/**
* Метод вставки новой записи с избранными приложениями
*
* @return boolean
*/
public function insertFavApp($appName)
{
Models\UserFavApp::updateOrCreate(
['user_login' => $this->userLogin],
['fav_apps' => $appName]
);
return true;
}
/**
* Метод удаляет запись с избранными приложениями пользователя
*
* @param string $userLogin логин пользователя
* @return boolean
*/
public function delAllUserFavApp($userLogin)
{
Models\UserFavApp::where('user_login', $userLogin)->delete();
return true;
}
/**
* Метод получает избранные приложения пользователя
*
* @param string $userLogin логин пользователя
* @return array
*/
public function getUserFavApp(string $userLogin): array
{
#Гаврилов
//ЗДЕСЬ ВМЕСТО DGAVRILOV ДОЛЖНО БЫТЬ ОБРАЩЕНИЕ К КУКАСАМ, ЧТОБЫ БРАТЬ ЛОГИН ОТТУДА
$userFavApp = Models\UserFavApp::where('user_login', $userLogin)->first();
return $userFavApp ? explode(';', $userFavApp['fav_apps']) : [];
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
class TestController extends Controller
{
public function getRoles()
{
$roles = DB::connection('mysql')->table('arch_lk_access_list')->select(['*'])->get();
return view('roles', ['roles' => $roles]);
}
public function getAccess($id)
{
$roles = DB::connection('mysql')->table('arch_lk_access_list')->select(['*'])->where('access_id', '=', $id)->get();
$response = new Response(json_encode($roles[0]));
$response->header('Content-type', 'text/plain');
$response->header('Access-Control-Allow-Methods', 'POST');
return $response;
}
public function redirect()
{
// return redirect()->away('https://google.com');
//return redirect()->action([TestController::class, 'getAccess'], ['id' => 3]);
return redirect()->route('getAccessById', ['id' => 2]);
}
public function getParam(Request $rqst)
{
//echo '<pre>'; var_dump($rqst->cookie('test_cookie')); echo'</pre>';
//return response('test')->cookie('test_cookie', $rqst->id);
}
public function setRole(Request $rqst)
{
$lastInsert = DB::connection('mysql')->table('arch_lk_access_list')->insertGetId(['role' => $rqst->roleName, 'title' => $rqst->roleTitle]);
return redirect()->route('get_role');
}
public function delRole(Request $rqst)
{
$lastInsert = DB::connection('mysql')->table('arch_lk_access_list')->where('access_id', '=', $rqst->access_id)->delete();
return redirect()->route('get_role');
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\TestData;
class TestDataController extends Controller
{
public function insertNewData(Request $rqst)
{
$model = new TestData;
$model->test_int = $rqst->int;
$model->test_char = $rqst->char;
$model->save();
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Models\TestFormModel;
use Illuminate\Http\Request;
class TestFormController extends Controller
{
public function getForm()
{
return view('test_form');
}
public function setForm(Request $rqst)
{
$testTable = new TestFormModel;
$testTable->first_name = $rqst->first_name;
$testTable->last_name = $rqst->last_name;
$testTable->department_name = $rqst->department_name;
$testTable->save();
return redirect('/test_table');
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace App\Http;
use App\Http\Middleware\CheckUserAppAccess;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\AuthenticateMagic::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\App\Http\Middleware\EncryptCookies::class,
//\Illuminate\Session\Middleware\StartSession::class,
//\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
//\Illuminate\Routing\Middleware\SubstituteBindings::class,
//\Illuminate\Session\Middleware\AuthenticateSession::class, // Опционально
//\Illuminate\Auth\Middleware\Authenticate::class.':sanctum', // Глобальная аутентификация
//Кастомный посредник аутентификации, который наследует стандартному посреднику аутентификации с передачей guarda sanctum. Сначала в кастомном посреднике будет проведена аутентификация sanctum, если будет выброшена ошибка, она будет обработана кастомным посредником (с возвратом сообщения об ошибке и корректного статуса). Без этой реализации стандартный посредник аутентификации пытался редиректить на роут login, которого не должно быть при обращении к api ендпоинту
\App\Http\Middleware\AuthenticateMagicApi::class.':sanctum',
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
//Посредник проверки доступных ролей в приложении
'checkPermission' => \App\Http\Middleware\CheckUserPermission::class,
//Посредник проверки доступа к приложению
'checkAppAccess' => \App\Http\Middleware\CheckUserAppAccess::class,
];
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
use App\Facades\UserContext;
use App\Models\User;
use Illuminate\Support\Facades\Redis;
use Laravel\Sanctum\PersonalAccessToken;
/**
* Глобальный посредник аутентификации на платформе Magic для всех роутов платформы
*/
class AuthenticateMagic
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
//TODO ПРОВЕРИТЬ
//Если сессия не стартовала возможно пользователь сразу обращается к api ендпоинту (ПРОВЕРИТЬ)
if (session()->isStarted()) {
if (session()->has('_auth_login')) {
//гаврилов. получение токена
// $token = $request->user();
// $userId = User::where('login', $token->getAttributes()['samaccountname'][0])->get()->toArray()[0]['id'];
// $tokenExpires = PersonalAccessToken::where('tokenable_id', $userId)->get()->toArray()[0]['expires_at'];
// echo '<pre>'; var_dump(new \DateTime($tokenExpires)); echo'</pre>';
// echo '<pre>'; var_dump(PersonalAccessToken::where('tokenable_id', $userId)->get()->toArray()[0]['expires_at']); echo'</pre>';
//Если токен истекает менее через 60 минут, продлеваем его на 2 часа. Ситуации, что сессия протухла, а токен продолжает жить не может случиться, так как апи запросы с фронта отправляют куку аутентификации, которая проверяется при $this->authenticate. Если она протухла, возвратится 401 ошибку.
// if ($token->expires_at->diffInMinutes(now()) < 60) {
// $token->update(
// [
// 'expires_at' => now()->addHours(2)
// ]
// );
// }
UserContext::setUserLogin(session()->get('_auth_login'));
$userGroups = session()->get('_auth_groups');
UserContext::setUserADGroups($userGroups);
UserContext::setUserEmails($userGroups);
UserContext::setIsAdminFlag(session()->get('is_admin'));
//На этапе посредника мы не проводим повторное определение ролей пользователя, это было сделано в LoginController в процессе авторизации после успешной аутентификации. Здесь мы уже берем его доступы из таблицы с токенами
$user = User::where('login', UserContext::getUserLogin())->first();
UserContext::setUserId($user->id);
UserContext::setUserAppPermissions($user->tokens()->latest()->first()->abilities['permissions']);
#Гаврилов
//ЧТО ЗА КОМАНДА НИЖЕ?
Redis::setex('notifications', 60, 123);
return $next($request);
} else {
//Получаем адрес предыдущей страницы, на которую хотел попасть пользователь, чтобы направить его после успешной аутентификации на этот адрес
$prevPageUrl = explode('/', $_SERVER['REDIRECT_URL']);
//Удаляем из URL редиректа пустые сегменты и сегмент с названием приложения (оно подставляется при редиректе само)
unset($prevPageUrl[0], $prevPageUrl[1]);
//Кладем в сессию адрес страницы. только если это не страница выхода, иначе после аутентификации пользователя сразу выбросит
session()->put('_auth_prev_page', implode('/', $prevPageUrl) == 'logout' ? '/menu' : implode('/', $prevPageUrl));
return redirect('/login');
//редирект на страницу login с сообщением об ошибке
}
} else {
return redirect('/login');
//redirect на страницу login после которой точно сессия застартует, так как это webроут, а не api
}
// return $next($request);
}
}
@@ -0,0 +1,61 @@
<?php
/**
* Кастомный посредник для обработки неудачной аутентификации Sanctum при обращении к api ендпоинтам. Стандартный посредник аутентификации пытается редиректить на роут login, чего быть не должно при работе с api
* @author dgavrilov
*/
namespace app\Http\Middleware;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as BaseAuthenticate;
use Illuminate\Auth\AuthenticationException;
use App\Facades\UserContext;
class AuthenticateMagicApi extends BaseAuthenticate
{
public function handle($request, Closure $next, ...$guards)
{
// if ($request->is('api/silent_token_refresh')) {
// return $next($request);
// }
//Если пользователь в рамках сессии обращается к api ендпоинтам из приложения, он не всегда может установить заголовок с sanctum токеном (например, переходя по ссылкке <a href='.../api/delItem/id'>). В этом случае, проверяем куки и устанавливаем заголовок оттуда, так как при аутентификации пользовательский логин кладется в куки. Это позволяет нам не устанвливать заголовк в каждом fetch запросе на фронте
if ($request->is('api/*') && ($token = $request->cookie('sanctum_token'))) {
$request->headers->set("Authorization", "Bearer $token");
}
//Переопределяем поведение в случае возникновения ошибки при стандартной аутентификации. Стандартное поведение перебрасывает на страницу login, мы же возвращаем 401 ошибку, так как понимаем, что пользователь обратился по api
//DGAVRILOV. ПОВЕДЕНИЕ, ОПИСАННОЕ ВЫШЕ, ПРИВОДИТ К ОШИБКЕ. ЕСЛИ ПОЛЬЗОВАТЕЛЬ ОТПРАВИЛ API ЗАПРОС С ФРОНТА И ЕГО СЕССИЯ ПРОТУХЛА, ВОЗВРАЩАЕТСЯ 401 ОШИБКА ВО ВКЛАДКЕ NETWORK, НО ПОЛЬЗОВАТЕЛЯ НЕ ПЕРЕБРАСЫВАЕТ НА СТРАНИЦУ /LOGIN. ЕСЛИ ПОПРАВИТЬ ПОСРЕДНИК WEB АУТЕНТИФИКАЦИИ, ТО ЭТОЙ ПРОБЛЕМЫ НЕ БУДЕТ? БУДЕТ ПЕРЕБРАСЫВАТЬ НА СТРАНИЦУ LOGIN СО СТРАНИЦЫ, ОТКУДА СОВЕРЩАЛСЯ ВЫЗО API ЕНДПОИНТА? СКОРЕЕ ВСЕГО НЕТ, НУЖНО БУДЕТ ПРИ ВЫЗОВЕ API КАК-ТО ПЕРЕХВАТЫВАТЬ 401 ОШИБКУ И ОТПРАВЛЯТЬ НА СТРАНИЦУ /LOGIN
try {
//Стандартная аутентификация по санктум токену (проверяется срок жизни)
$this->authenticate($request, $guards);
//$token = $request->user()->currentAccessToken();
//Если токен истекает менее через 60 минут, продлеваем его на 2 часа. Ситуации, что сессия протухла, а токен продолжает жить не может случиться, так как апи запросы с фронта отправляют куку аутентификации, которая проверяется при $this->authenticate. Если она протухла, возвратится 401 ошибку.
// if ($token->expires_at->diffInMinutes(now()) < 60) {
// $token->update(
// [
// 'expires_at' => now()->addHours(2)
// ]
// );
// }
//После успешной аутентификации Sanctum обогащаем UserService параметрами пользователя (логин, роли приложения)
#Гаврилов
//ПОКА НЕ ДОБАВЛЯЮ ГРУППЫ AD B EMAILS, ТАК КАК В WEB КОНТУРЕ ОНИ ОПРЕДЕЛЯЮТСЯ ПРИ AD АУТЕНТИФИКАЦИИ И ПОЛУЧЕННЫЕ ЗНАЧЕНИЯ СКЛАДЫВАЮТСЯ В SESSION. В API КОНТУРЕ ДОСТУПА К СЕССИИ НЕТ - ПРОВОДИТЬ AD АУТЕНТИФИКАЦИЮ НЕ ПОЛУЧИТС БЕЗ ПАРОЛЯ ПОЛЬЗОВАТЕЛЯ, КОТОРЫЙ НИГДЕ НЕ ХРАНИТСЯ. ПОЭТОМУ, ЕСЛИ ПОНАДОБИТСЯ ДОСТАВАТЬ В API КОНТУРЕ ГРУППЫ ПОЛЬЗОВАТЕЛЯ, ПРИ AD АУТЕНТИФИКАЦИИ ПОНАДОБИТСЯ КЛАСТЬ AD ГРУППЫ ПОЛЬЗОВАТЕЛЯ И ЕГО EMAILS В ПОЛЕ ABILITIES ТАБЛИЦЫ PERSONAL ACCESS TOKENS ПО АНАЛОГИИ С PERISSIONS
#Гаврилов
//НУЖНО ЛИ ОБОГАЩАТЬ USERcoNTEXT НИЖЕ, ЕСЛИ ПРИ WEB АУТЕНТИФИКАЦИИ КОНТЕКСТ УЖЕ ДОЛЖЕН БЫЛ БЫТЬ ОБОГАЩЕН. вЕРОЯТНО, ЭТО НУЖНО ДЛЯ ОБРАБОТКИ ЧИСТО API ЗАПРОСОВ (БЕЗ ВХОДА В СИСТЕМУ), НО НАДО ПРОВЕРИТЬ
UserContext::setUserLogin($request->user()->login);
UserContext::setUserAppPermissions($request->user()->currentAccessToken()->abilities['permissions']);
} catch (AuthenticationException $auth) {
if ($request->is('api/*')) {
return response()->json([
#Гаврилов
//СКОРРЕКТИРУЙ ИНФОРМАЦИЮ О ВОЗВРАЩАЕМОЙ ОШИБКЕ?
'error' => 'Unauthenticated',
'message' => 'Invalid or missing authentication token'
], 401);
}
throw $auth;
}
return $next($request);
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Facades\UserContext;
/**
* Посредник проверки доступа пользователя к приложению
*/
class CheckUserAppAccess
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$moduleName = explode('/', $request->route()->getPrefix());
$moduleName = end($moduleName) ?? null;
if ($moduleName) {
if (array_key_exists($moduleName, UserContext::getUserAppPermissions()) !== false) {
return $next($request);
}
}
//Если ошибка при обращении к api ендпоинту
if ($request->expectsJson()) {
return response()->json(['message' => "Ошибка! Приложение недоступно"], 403);
//Если ошибка при обращении к web роуту
} else {
#Гаврилов
//ЕСЛИ ВЫЗЫВАЕТСЯ WEB РОУТ НЕ С ФРОНТА, ТО ПРОИСХОДИТ РЕДИРЕКТ БЕЗ УКАЗАНИЯ ТЕКСТА ОШИБКИ. нАПРИМЕР, ПРИ РЕДИРЕКТЕ НИЖЕ НА СТРАНИЦУ МЕНЮ, ПОЛЬЗОВАТЕЛЬ НЕ УВИДИТ НИКАКОГО ОПОВЕЩЕНИЯ ОБ ОШИБКЕ.
return redirect('/menu');
}
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Models\User;
use App\Facades\UserContext;
/**
* Посредник проверки доступа пользовательской роли к роуту или ендпоинту
*/
class CheckUserPermission
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string $requirement): Response
{
//Роли, которым доступен функционал
$accessRoles = explode(';', $requirement);
$userPermissions = UserContext::getUserAppPermissions();
//Всем роутам модуля добавляем префикс, поэтому можем ориентироваться на него, чтобы получить имя модуля, откуда пришел запрос
$moduleName = explode('/', $request->route()->getPrefix());
$moduleName = end($moduleName) ?? null;
if ($moduleName) {
if (array_key_exists($moduleName, UserContext::getUserAppPermissions()) !== false) {
if (in_array($userPermissions[$moduleName], $accessRoles)) {
return $next($request);
}
// else {
// return redirect('/menu');
// #Гаврилов
// //РЕДИРЕКТ НА СТРАНИЦУ 403
// }
}
// else {
// return redirect('/menu');
// #Гаврилов
// //РЕДИРЕКТ НА СТРАНИЦУ 403
// //redirect();
// }
}
// else {
// return redirect('/menu');
// #Гаврилов
// //КУДА РЕДИРЕКТИТЬ или что возвращать, если имя модуля не определено?
// }
//Если ошибка при обращении к api ендпоинту
if ($request->expectsJson()) {
return response()->json(['message' => "Ошибка! Функционал недоступен для вашей роли"], 403);
//Если ошибка при обращении к web роуту
} else {
#Гаврилов
//ЕСЛИ ВЫЗЫВАЕТСЯ WEB РОУТ НЕ С ФРОНТА, ТО ПРОИСХОДИТ РЕДИРЕКТ БЕЗ УКАЗАНИЯ ТЕКСТА ОШИБКИ. нАПРИМЕР, ПРИ РЕДИРЕКТЕ НИЖЕ НА СТРАНИЦУ МЕНЮ, ПОЛЬЗОВАТЕЛЬ НЕ УВИДИТ НИКАКОГО ОПОВЕЩЕНИЯ ОБ ОШИБКЕ.
return redirect('/menu');
}
#Гаврилов
//РЕДИРЕКТ НА СТРАНИЦУ 403
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
'/access/*',
'/access',
];
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Job;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;
/**
* Базовый класс для выполнения джобы
*/
class BaseJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Метод обработки ошибки при выполнении джобы
*
* @param \Throwable $th
* @return void
*/
public function failed(\Throwable $th): void
{
$context = [
//Скрипт откуда запустилась задача
'command' => static::class,
//Скрипт, в котором произошла ошибка
'file' => $th->getFile(),
'line' => $th->getLine(),
];
\Log::channel('job_err')->error($th->getMessage(), $context);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Mail;
/**
* Базовый класс объекта параметров отправляемого мэджиком писем
*/
class BaseMailerObj
{
public function __construct(
public array $to, //адресаты письма
public string $subject, //тема письма
public string $body, //основной текст письма
public string $appName, //кто будет в копии
public array $copy = [], //шапка письма, например, для заголовка
public ?string $header = '', //подвал письма, например для технической информации
public ?string $footer = '', //название приложения назмещается под названием платформы Magic в футере письма
public string $mailLayout = 'mail\mailer_default', //используемый blade шаблон для отправки
){
}
#Гаврилов
//УДАЛИ ЕСЛИ НЕ ПОНАДОБИТСЯ
// /**
// * Фабричный метод для создания экземпляра родительского класса
// *
// * @param [type] $to
// * @param [type] $subject
// * @param [type] $body
// * @param [type] $appName
// * @param array $copy
// * @param string|null $header
// * @param string|null $footer
// * @param string $mailLayout
// * @return void
// */
// public static function create($to, $subject, $body, $appName, array $copy = [], ?string $header = '', ?string $footer = '', string $mailLayout = 'mail\mailer_default')
// {
// return new self(
// $to,
// $subject,
// $body,
// $appName,
// $copy,
// $header,
// $footer,
// $mailLayout
// );
// }
}
+145
View File
@@ -0,0 +1,145 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use App\Mail\BaseMailerObj;
class Mailer extends Mailable
{
#Гаврилов
//ЗАЧЕМ ОНИ СЮДА ДОБАВИЛИСЬ? ТЕСТИРОВАЛ ОТПРАВКУ ОЧЕРЕДЕЙ? УДАЛИ ЕСЛИ НЕ НУЖНЫ ОТСЮДА И ИЗ USE
use Queueable, SerializesModels;
/**
* @var array данные для письма
*/
public $mailData;
/**
* @var string имя blade шаблона
*/
public $mailLayout;
/**
* @var string название приложения, которое отправляет сообщение
*/
public $appName;
/**
* Конструктор
*
* @param array $to - адресаты письма
* @param string $subject - тема письма
* @param string $body - основной текст письма
* @param array $copy - кто будет в копии
* @param string $header - шапка письма, например, для заголовка
* @param string $footer - подвал письма, например для технической информации
* @param string $appName название приложения назмещается под названием платформы Magic в футере письма
* @param string $mailLayout используемый blade шаблон для отправки
*/
public function __construct(BaseMailerObj $mailerObject)
{
//Преобразуем передаваемых адресатов, добавляя им логин
$this->mailData['to'] = $this->addDomain($mailerObject->to);
//$this->mailData['to'] = $to;
$this->mailData['copy'] = !empty($copy) ? $this->addDomain($mailerObject->copy) : [];
$this->mailData['subject'] = $mailerObject->subject;
$this->mailData['body'] = $mailerObject->body;
$this->mailData['header'] = $mailerObject->header;
$this->mailData['footer'] = $mailerObject->footer;
$this->checkTestEnv();
$this->appName = $mailerObject->appName;
$this->mailLayout = $mailerObject->mailLayout;
}
/**
* Метод корректирует свойства отправляемого письма, если среда разработки тестовая
*
* @return void
*/
private function checkTestEnv()
{
//Если тестировщик аутентифицирован, берется его логин из сессии, в противном случае берется закрепленный в глобальных конфигах тестовый адрес (который разработчик указывает для своего локального тестирования сам). Можно в будущем улучшить логику с определением тестового емейла, например, передавая к api ендпоинту заголовок с тестовым емейлом, но на старте пока так
// if (auth()->user()->login) {
if (session()->has('_auth_login')) {
$userLogin = session()->get('_auth_login');
} else {
$userLogin = config('app.mail_test_addressee');
}
// echo '<pre>'; var_dump($userLogin); echo'</pre>';
// $test = config('app.test_env');
// $test2 = config('app.mail_test_addressee');
// echo '<pre>'; var_dump($test); echo'</pre>';
// echo '<pre>'; var_dump($test2); echo'</pre>';
//}
// $userLogin = auth()->user()->login ? auth()->user()->login : (session()->has('_auth_login') ? session()->get('_auth_login')) : config('MAIL_TEST_ADRESSEE');
//Если работаем из под тестовой среды
if (config('app.test_env')) {
//Информация для тестирования будет добавлена в футер сообщения. В нее включаем информацию об адресатах и копии, если бы письмо отправлялось на проде
$testInfo = "На проде письмо отправится: " . implode(", ", $this->mailData['to']) . ". В копии: " . implode(", ", $this->mailData['copy']);
$this->mailData['footer'] .= $testInfo;
//Копию очищаем
$this->cc('');
//echo '<pre>'; var_dump(['dgavrilov@rencredit.ru']); echo'</pre>';
//echo '<pre>'; var_dump($this->addDomain([$userLogin])); echo'</pre>';
$this->to($this->addDomain([$userLogin]));
//$this->to(['dgavrilov@rencredit.ru']);
} else {
//ГАВРИЛОВ. ДОБАВЬ ДОМЕН К ЛОГИНАМ
$this->to($this->mailData['to']);
$this->to($this->mailData['copy']);
}
}
/**
* Хэлпер преобразует всех адресатов, добавляя им почтовый домен
*
* @param array $addresseesArr адресаты
* @return array
*/
private function addDomain(array $addresseesArr): array
{
$domain = config('mail.mail_domain');
return array_map(function($el) use($domain) {return $el . "@" . $domain;}, $addresseesArr);
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: $this->mailData['subject'],
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
// view: 'view.name',
view: $this->mailLayout,
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AccessModel extends Model
{
use HasFactory;
protected $connection = 'mysql';
protected $table = 'tm_lk_access_list';
public $timestamps = false;
protected $primaryKey = 'access_id';
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\MagicApps;
class AppRoles extends Model
{
protected $table = 'app_roles';
use HasFactory;
public function magicApp()
{
return $this->belongsTo(MagicApps::class, 'app_id', 'id');
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\AppRoles;
class MagicApps extends Model
{
protected $table = 'magic_apps';
use HasFactory;
public function appRoles()
{
return $this->hasMany(AppRoles::class, 'app_id', 'id')
->orderBy('role_priority', 'asc');
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models\OldMagicModels;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CcEmp extends Model
{
use HasFactory;
protected $table = 'cc_emp';
protected $connection = 'magic_old';
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TestData extends Model
{
use HasFactory;
protected $table = 'test_data';
protected $connection = 'mysql';
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TestFormModel extends Model
{
use HasFactory;
protected $connection = 'mysql';
}
+22 -20
View File
@@ -6,43 +6,45 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
* @var array<int, string>
*/
// protected $fillable = [
// 'name',
// 'email',
// 'password',
// ];
protected $fillable = [
'name',
'email',
'password',
'login',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
// protected $hidden = [
// 'password',
// 'remember_token',
// ];
protected $hidden = [];
/**
* Get the attributes that should be cast.
* The attributes that should be cast.
*
* @return array<string, string>
* @var array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserFavApp extends Model
{
use HasFactory;
public $incrementing = true;
public $timestamps = true;
protected $table = 'user_fav_app';
protected $connection = "mysql";
protected $fillable = ['user_login', 'fav_apps'];
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\ApiResponder;
class ApiResponderProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->bind(ApiResponder::class, function($app){
return new ApiResponder();
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}
+8 -2
View File
@@ -3,6 +3,9 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Spatie\Activitylog\Facades\CauserResolver;
use Illuminate\Support\Facades\View;
use App\Services\ModuleService;
class AppServiceProvider extends ServiceProvider
{
@@ -11,7 +14,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
//Регистрируем передачу во все blade шаблоны функционал сервиса по определению имени модуля из текущего роута
View::share('moduleName', app(ModuleService::class));
}
/**
@@ -19,6 +23,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
// Глобально отключаем определение causer для корректной работы пакета activity_log. По умолчанию пакет ожидает получить экземпляр модели Models\User для прописывания в таблицу activity_log значения causer_type и causer_id. Эти значения нельзя руками прописать при логировании. Никакие танцы с бубнами не помогали кроме строки ниже. Данные по инициатору изменения решил записывать в поле properties
//UPD: Решил отказаться, так как все равно для корректной работы аутентификации пришел к фиксации записи в модели users, для отображения истории бизнес-сущностей удобнее будет получать пользователя, совершившего действие, из отдельного поля в модели, а не парсить json из поля properties
// CauserResolver::resolveUsing(fn () => null);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
/**
* Провайдер для регистрации сервиса аутентификации
*/
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
//
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
//
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\AuthorizationService;
/**
* Провайдер для регистрации сервиса авторизации
*/
class AuthorizationServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->bind(AuthorizationService::class, function($app) {
return new AuthorizationService();
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*/
public function boot(): void
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Providers;
use App\Services\RedisNotificationService;
use Illuminate\Support\ServiceProvider;
/**
* Провайдер для работы с сервисом Redis для хранения нотификаций для отображения на фронте
*/
class RedisNotificationProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->bind(RedisNotificationService::class, function($app){
return new RedisNotificationService;
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to your application's "home" route.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/home';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\UserService;
/**
* Провайдер для регистрации сервиса работы с данными юзера (установка логина, групп, подмена значений в случае работы на тестовой среде)
*/
class UserServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton(UserService::class, function($app){
return new UserService;
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Services;
use App\Dto\ApiResponseDto;
use Illuminate\Http\JsonResponse;
class ApiResponder{
public $dto;
public function __construct(
)
{}
public function success(): JsonResponse
{
return $this->buildResponse();
}
public function error()
{
return $this->buildResponse();
}
public function setDto(ApiResponseDto $dto)
{
$this->dto = $dto;
}
// public function fromDTO(ApiResponseDto $dto)
// {
// return $dto;
// }
#Гаврилов
//СБОР СТАТИСТИКИ ПО ВЫЗЫВАЕМЫМ API ЕНДПОИНТАМ. ВНЕСТИ В TODO ПРОЕКТА
private function setStatistics()
{
}
#Гаврилов
//ДОБАВЬ ЛОГИРОВАНИЕ ОШИБОК В TRY CATCH В .LOG ФАЙЛ
private function buildResponse(): JsonResponse
{
//echo '<pre>'; var_dump((array)$this->dto); echo'</pre>';
//return (array) $this->dto;
return response()->json((array) $this->dto);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Services;
use App\Models\MagicApps;
use App\Facades\UserContext;
class AuthorizationService
{
private $appWithRoles;
/**
* Получаем приложения вместе с ролями
*
* @return array
*/
public function getUserAppPermissions(): array
{
$this->appWithRoles = MagicApps::with('appRoles')->get()->toArray();
//Итоговый массив с доступами пользователя к приложению и ролью
$userAppAccess = [];
foreach ($this->appWithRoles as $appData) {
//Определяем по какому массиву проверять доступ к приложению - почтовые рассылки или группы AD
if (empty($appData['access_groups_email'])) {
$appAccess = explode(';', $appData['access_groups_ad']);
$userAccessGroups = UserContext::getUserADGroups();
} else {
$appAccess = explode(';', $appData['access_groups_email']);
$userAccessGroups = UserContext::getUserEmails();
}
//Если пересечения групп доступа приложения и групп доступа пользователя отсутствуют, считаем, что доступа нет
if (empty(array_intersect($appAccess, $userAccessGroups))) {
continue;
}
//Если ролевая модель отсутствует для приложения как таковая, прописывам значение null
if (empty($appData['app_roles'])) {
$userAppAccess[$appData['app_name']] = null;
} else {
//Если ролевая модель существует, ставим значение false, которое будет заменено ролью пользователя. Сохранение false на выходе, в свою очередь, сигнализирует, что роль пользователя не определилась
$userAppAccess[$appData['app_name']] = false;
foreach ($appData['app_roles'] as $roleData) {
if($this->checkRoleAccess($appData['role_driver'], explode(';', $roleData['role_access']))) {
$userAppAccess[$appData['app_name']] = $roleData['app_role'];
//Все роли идут по приоритету важности, поэтому останавливаемся на самой важной и возвращаем ее, остальные роли не перебираем. Я пока не уверен как лучше организовать отображение функционала ролей в приложениях: весь нужный функционал "класть" в одну роль и отображать строго тот функционал, который относится к роли. Или распределять функционал между ролями и, если пользователь входит в несколько ролей, отображать весь функционал всех ролей, куда он может входить. В случае последнего варианта, нужно будет хранить все доступные роли пользователя, а не только самую важную по приортету.
//Также пока непонятно, реализовывать ли возможность переключаться между ролями на продакшн среде. Если реализовывать, так же придется хранить все роли, куда входит пользователь. Но в этом случае есть риски запутаться в логировании, например, если пользователь под ролью админ переключится на пользователя юзер и совершит от него действие
break;
}
}
}
}
return $userAppAccess;
}
/**
* Возвращаем результат проверки доступности роли
*
* @param string $roleAccessDriver название драйвера доступа роли
* @param array $roleData массив с перечисленными рассылками/логинами/группами, которым доступна роль
* @return bool
*/
public function checkRoleAccess(string $roleAccessDriver, array $roleData): bool
{
switch ($roleAccessDriver) {
case 'login':
return in_array(UserContext::getUserLogin(), $roleData);
break;
case 'email':
return !empty(array_intersect(UserContext::getUserEmails(), $roleData));
break;
case 'ADgroup':
return !empty(array_intersect(UserContext::getUserADGroups(), $roleData));
break;
}
}
//УДАЛИТЬ
// public function getUserAppRoles(array $userGroups)
// {
// $userEmails = session()->get('_auth_groups');
// $userLogin = session()->get('_auth_login');
// $magicApps = $this->getAppRoles();
// }
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**Сервис для определения модуля
@author dgavrilov
*/
namespace App\Services;
class ModuleService{
/**
* Получаем имя модуля из роута (так как в нем обязательно указывается префикс)
*
* @return string | null
*/
public function getModuleName(): string | null
{
$route = request()->route();
$routePrefix = null;
if ($route && $route->getPrefix()) {
$routePrefix = explode('/', $route->getPrefix())[1];
}
return $routePrefix;
}
/**
* Основываясь на имени модуля из роута получаем имя роута на русском (свойство name_ru), которое обязательное прописывается в конфиге модуля
*
* @return string | null
*/
public function getRuModuleName(): string | null
{
if ($module = $this->getModuleName()) {
return config("$module.name_ru", null);
} else {
return null;
}
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Services;
/**
* Базовый класс для работы с Redis в контексте модульной структуры платформы
*/
abstract class RedisBaseService
{
/**
* @var string базовое подключение к Redis. Руками не прописывается, устанавливается в конструкторе дочернего сервиса через недоступный из фасада метод protected connection(). Сделано это, чтобы разработчик не указывал вручную подключение каждый раз b при этом любой дочерний сервис был обязан прописывать подключение
*/
protected string $connection;
/**
* @var int базовый ttl для данных с ограниченным сроком жизни
*/
public int $ttl = 600;
/**
* @var string имя модуля/приложения откуда отправляются данные. Передается в инициилизирующем методе module(). Значение '' означает, что данные отправляются из корня проекта, не из модуля
*/
public string $moduleName;
/**
* Метод установки подключения к Redis. В какую БД кладем данные
*
* @param string $connection имя подключения
* @return void
*/
abstract protected function connection(string $connection): void;
/**
* Инициилизирующий метод фасада, принимающий аргумент в виде названия модуля, откуда планируется отправить данные.
*
* @param string $moduleName имя модуля, откуда передаются данные. Аргумент можно не передавать, если данные передаются из корня ларавель
* @return self
*/
public function module(string $moduleName = ''): self
{
$this->moduleName = $moduleName;
return $this;
}
/**
* Меняем ttl данных
*
* @param int $ttl время жизни записи с данными
* @return self
*/
public function ttl(int $ttl): self
{
$this->ttl = $ttl;
return $this;
}
/**
* Возвращает часть префикса ключа redis для хранения информации, отправленной из модуля
*
* @return string
*/
protected function getModuleRedisPrefix(): string
{
return "_" . $this->moduleName . ":";
}
/**
* Реализация размещения данных в Redis
*
* @return void
*/
abstract public function put(): void;
}
+99
View File
@@ -0,0 +1,99 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Redis;
use App\Facades\UserContext;
use Exception;
class RedisNotificationService extends RedisBaseService
{
protected string $connection = 'notifications';
/**
* @var array тексты уведомлений
*/
public array $notificationTexts = [];
/**
* @var array тексты нотификаций
*/
public array $notificationTypes = [];
public function __construct()
{
$this->connection('notifications');
}
protected function connection($connection): void
{
$this->connection = $connection;
//Предполагается, что время жизни должно быть небольшое, так уведомления размещаются перед самим редиректом для их демонстрации пользователю как можно скорее
$this->ttl = 180;
}
/**
* Метод возвращает id юзера, который является обязательной частью ключа в БД redis с нотификациями
*
* @return int
*/
private function identifyUser(): int
{
$userId = UserContext::getUserId();
return $userId;
}
/**
* Устанавливаем текст уведомлений для отправки в Redis
*
* @param array $notificationTexts массив текстов уведомлений
* @return self
*/
public function notifications(array $notificationTexts): self
{
$this->notificationTexts = $notificationTexts;
return $this;
}
/**
* Устанавливаем типы для передаваемых Redis уведомлений
*
* @param array $notificationTypes типы уведомлений
* @return self
*/
public function types(array $notificationTypes): self
{
//Если при передаче типов уведомлений не указаны тексты уведомлений, либо если количество текстов уведомлений не равно количеству типов уведомлений, возвращаем ошибку
if (empty($this->notificationTexts)) {
throw new Exception('Не переданы тексты уведомлений');
}
if (count($this->notificationTexts) !== count($notificationTypes)) {
throw new Exception('Количество переданных типов должно совпадать с количеством текстов уведомлений');
}
if ($incorrectTypes = array_diff($notificationTypes, config('app.FRONT_notification_types'))) {
throw new Exception('Переданные типы уведомлений ' . implode(', ', $incorrectTypes) . ' не существуют');
}
$this->notificationTypes = $notificationTypes;
return $this;
}
public function put(): void
{
$notificationsRedis = [];
if (empty($this->notificationTexts) || empty($this->notificationTypes)) {
throw new Exception('Отсутствуют обязательные параметры для отправки в Redis');
} else {
foreach ($this->notificationTexts as $key => $text) {
$notificationsRedis[] = ['text' => $text, 'type' => $this->notificationTypes[$key]];
}
}
Redis::connection($this->connection)->setex($this->getModuleRedisPrefix() . "notifications:user:" . $this->identifyUser(), $this->ttl, json_encode($notificationsRedis));
}
}
+198
View File
@@ -0,0 +1,198 @@
<?php
namespace App\Services;
use Exception;
/**
* Сервис работы с данными юзера (установка/получение логина, групп, подмена значений в случае работы на тестовой среде)
*/
class UserService
{
/**
* @var string пользовательский логин
*/
public string $userLogin;
/**
* @var array пользовательские группы AD
*/
public array $userADGroups;
/**
* @var array почтовые рассылки куда входит пользователь
*/
public array $userEmails;
/**
* @var boolean является ли пользователь админом приложения Magic
*/
public bool $isAdmin;
/**
* @var array доступы пользователя к приложениям
*/
public array $userAppPermissions;
/**
* @var int идентификатор пользователя из таблицы users
*/
public int $userId;
/**
* Установка пользовательского логина
*
* @param string $login логин для подмены превоначального значения, взятого из сессии
* @return void
*/
public function setUserLogin(string $login): void
{
$this->userLogin = $login;
}
/**
* Установка пользовательского логина
*
* @param array $appRoles доступы пользователя к приложениям Magic
* @return void
*/
public function setUserAppPermissions(array $appRoles): void
{
$this->userAppPermissions = $appRoles;
}
/**
* Установка пользовательского идентификатора из таблицы users
*
* @param int $userId идентификатор пользователя
* @return void
*/
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
/**
* Установка групп AD пользователя
*
* @param array $userGroups все группы из AD где состоит пользователь (emails, AD и т.д.)
* @return void
*/
public function setUserADGroups(array $userGroups): void
{
$this->userADGroups = array_filter($userGroups, function($el){return substr($el, 0, 1) !== '#';});
}
/**
* Установка почтовых рассылок куда входит пользователь
*
* @param array $userGroups все групы пользователя AD, в которых он состоит (почтовые ящики, AD и т.д.)
* @return void
*/
public function setUserEmails(array $userGroups): void
{
$this->userEmails = array_filter($userGroups, function($el){return substr($el, 0, 1) === '#';});
}
/**
* Устанавливаем флаг является ли пользователь админом приложения Magic
*
* @param boolean $flag
* @return void
*/
public function setIsAdminFlag(bool $flag): void
{
$this->isAdmin = $flag;
}
/**
* Добавление группы AD в массив групп пользователя
*
* @param string $group группа для добавления в массив установленных при аутентификации групп AD пользователя
* @return void
*/
public function addUserADGroup(string $group): void
{
$this->userADGroups[] = $group;
}
/**
* Добавление email в массив емейлов пользователя
*
* @param string $email почтовая рассылка для добавления в массив установленных при аутентификации почтовых ящиков пользователя
* @return void
*/
public function addUserEmail(string $email): void
{
$this->userEmails[] = $email;
}
/**
* Возвращаем доступы пользователя к приложениям Magic
*
* @return array
*/
public function getUserAppPermissions(): array
{
return $this->userAppPermissions;
}
/**
* Получаем пользовательский логин
*
* @return string
*/
public function getUserLogin(): string
{
return $this->userLogin;
}
/**
* Получаем пользовательский логин
*
* @return int
*/
public function getUserId(): int
{
return $this->userId;
}
/**
* Получаем пользовательский логин
*
* @return array
*/
public function getUserADGroups(): array
{
return $this->userADGroups;
}
/**
* Получаем email рассылки куда он входит
*
* @return array
*/
public function getUserEmails(): array
{
return $this->userEmails;
}
/**
* Получаем значение флага является ли пользователь админом приложения Magic
*
* @return boolean
*/
public function getIsAdminFlag(): bool
{
return $this->isAdmin;
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
namespace app\Traits;
use PHPUnit\Event\Code\Throwable;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\Contracts\Activity;
use App\Enums\LogBusinessEvents;
trait LogsActivity_custom
{
//"наследуем" готовый трейт из пакета логирования, чтобы расширить его
use LogsActivity;
/**
*
* @param string имя журнала логирования (название приложения). Нужно переопределять в модели в зависимости от используемого приложения (модуля)
*/
protected $logActivity_custom__name;
/**
* @param array Массив с кастомными значениями, которые хотим логировать независимо от модели, событие в которой логируется
*/
// protected $activityCustomProperties = [
// 'custom__user_login' => session()->get('_auth_login'),
// ];
protected $activityCustomProperties = [];
/**
* @var array массив с кастомными значениями, которые относятся к самому действию логирования, а не к изменениям в результате логируемого действия (properties). Например, название совершенного действия: архивация, отмена, восстановление и т.д.
*/
protected $activityCustomDescription = [];
/**
* @var string название совершенного бизнес-действия (не события из поля event, там могут быть только eloquent события: create, udate и т.д.). По умолчанию действие - изменение.
*
* Свойство статическое, потому что при использовании нестатического свойства, его значение после присваения "сбрасывается" до значения по умолчанию из за специфики работы с моделью. Каждое создание экземпляра модели может привести к сбросу свойства до значения по умолчанию. Если свойство статическое, его можно менять в любой момент и во всех экземплярах этой модели оно будет изменено
*/
protected static $businessEvent = LogBusinessEvents::Edit;
/**
* Единые опции логирования любых события в моделях, где указано использование данного трейта
*
* @return LogOptions
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logAll() //Логируем все поля
->logOnlyDirty() //Логируем только изменившиеся по сравнению с текущим состоянием значения
->useLogName($this->logActivity_custom__name ? $this->logActivity_custom__name : 'default') //Имя журнала (имя приложения) события которого логируются
->logExcept(['created_at', 'updated_at']); //Поля, значения которых не логируются
}
/**
* Тут прописываем дополнительные свойства, которые будут добавляться к каждой записи логирования изменения моделей
*
* @return array
*/
protected function getActivityLogDefaultProp (): array
{
//Добавляем логин пользователя к записи с логированием события. Если изменение произошло через web роут, то берется логин из объекта ldap (свойство samaccountname), если было обращение через api ендпоинт, берется логин из таблицы users, где делается запись при аутентификации пользователя. Это связано со спецификой аутентификации при вызове web роута и api ендпоинта
//UPD: решил отказаться от логирования пользователя через расширение properties, но для примера пока оставил
//return ['custom__user_login' => auth()->user()->login ?? auth()->user()->samaccountname[0]];
return [];
}
// public function getActivitylogOptions(): LogOptions
// {
// //CauserResolver::setCauser('dgavrilov');
// return LogOptions::defaults()
// ->logAll() // Перечисляем логируемые поля. В данном случае, логируем все поля
// ->logExcept(['created_at', 'updated_at']) // Поля, которые не будут логироваться не при каких условиях
// ->logOnlyDirty() // Логируются только поля, данные в которых были изменены
// //->useLogName($this->getLogName());
// ->useLogName($this->logActivity_custom__name ? $this->logActivity_custom__name : 'default'); //Имя журнала логирования. Либо переопределеяется в модели, который использует данный трейт, либо по умолчанию ставится default
// // ->tapActivity(function (Activity $activity) {
// // // Указываем пользователя из сессии/кук
// // //$activity->causer_id = auth()->id(); // или явно: 1
// // $activity->causer_id = 'dgavrilov'; // или явно: 1
// // //$activity->causer_type = \App\Models\User::class;
// // });
// }
/**
* Метод позволяет "вмешаться" в процесс логирования в "последний момент" перед записью в модель activity_log. В данном случае, значения в поле properties обогащаются доп значениями, которые мы описали в свойстве $this->activityCustomProperties
*
* @param Activity $activity
* @param string $eventName
* @return void
*/
public function tapActivity(Activity $activity, string $eventName)
{
$this->activityCustomProperties = array_merge($this->activityCustomProperties, $this->getActivityLogDefaultProp());
// $this->activityCustomDescription = $this->setLogDescription();
$activity->properties = $activity->properties->merge(['custom_props' => $this->activityCustomProperties]);
//переопределение поле description модели activityLog
// $activity->description = json_encode($this->activityCustomDescription);
$activity->business_event = self::$businessEvent;
}
#Гаврилов
//ЛУЧШЕ СДЕЛАТЬ ЕДИНЫЙ МЕТОД, ПРИНИМАЮЩИЙ НАЗВАНИЕ ACTION, СРАВНИВАЯ ЕГО СО СВОЙСТВОМ ТЕКУЩЕГО КЛАССА businessAction И ВОЗВРАЩАЮЩИЙ ОШИБКУ, ЕСЛИ ТАКОГО СВОЙСТВА НЕТ
/**
* Метод инициации логирования бизнес-действия: создание, редактирование, архивация. В поле event по умолчанию логируются только Eloquent события: create, update и тд
*
* @param string $action логируемое бизнес-действие
* @return void
*/
public function logBusinessEvent(LogBusinessEvents $event)
{
self::$businessEvent = $event;
}
/**
* @return array формирование дополнительного описания с информацией для логируемого действия
*/
// public function setLogDescription()
// {
// $activityDescription = array_merge(
// $this->activityCustomDescription,
// );
// return $activityDescription;
// }
/**
* Метод добавления кастомных значений для логирования
*
* Поля, переданные в метод из модели, где подключен этот трейт
*
* @param array $properties
* @return void
*/
public function addCustomLogProperties(array $properties): void
{
$this->activityCustomProperties = array_merge(
$this->activityCustomProperties ?? [],
$properties
);
}
}