Compare commits

..

10 Commits

Author SHA1 Message Date
vasya e4e7443f81 не могу точно сформулировать зачем этот посредник. Насколько я помню, он нужен только для того, чтобы дополнительно обрабатывать CSRF токены, прописывая от руки некоторые доп проверки/настройки. В частности, тут я добавил некоторые паттерны URL роутов, где CSRF проверка не требуется, так как там не подразумевается 2026-03-15 17:43:05 +03:00
vasya 5a8f309038 Этот посредник упоминается в Kernel, но я не нашел, где конкретно он используется по коду. Лучше оставить, вероятно, он может работать под капотом. Лучше после развертывания попробовать его удалить и посмотреть че будет 2026-03-15 17:38:56 +03:00
vasya 4dc75d7b56 я нашел упоминание этого контроллера только в api ендпоинте, который специально вызывается для получения пользовательской роли. Либо он не нужен был пока, либо я плохо искал, либо этот функционал уже где то реализовал более лучшим образом. Пока лучше оставить, логика у этого конртроллера предельно простая 2026-03-15 17:33:14 +03:00
vasya ca157ace77 Я сначала не хотел использовать модель User при аутентификаии, но в итоге она понадобилась, то ли это связано с Sanctum, я не помню. Точно то, что я ее использую при логировании: в таблице с историей по приложению автоматически кладется id пользователя, совершившего действие. Этот id берется из модели User, где фиксируется при аутентификации. Это позволяет не заморачиваться с хранением логина в поле с JSON со всеми изменениями, что удобно 2026-03-15 17:21:21 +03:00
vasya b734bdf849 добавляю посредник проверки наличие пользователя доступа к определенному функционалу в приложении. То есть, доступ к модулю есть, но нужно проверить может ли пользователь в рамках этого модуля обращаться к определенному web роуту или api ендпоинту 2026-03-15 16:46:22 +03:00
vasya ae5548b27a добавляю посредник, который проверяет наличие доступа у пользователя к саомому приложению\модулю в принципе - может ппользователь работать в данном приложении или нет 2026-03-15 16:41:51 +03:00
vasya 20715e880f api ендпоинты для аутентификации 2026-03-14 20:41:44 +03:00
vasya 681cfc7cad добавляю пустой файл с api ендпоинтами 2026-03-14 20:29:38 +03:00
vasya 5c83b8717c все скрипты для фронта страницы с формой аутентификации 2026-03-14 20:27:12 +03:00
vasya 8d1b19bfa4 веб роуты, нужные для аутентификации на платформе 2026-03-14 19:36:45 +03:00
14 changed files with 374 additions and 20 deletions
@@ -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);
}
}
}
+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');
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Facades\UserContext;
//ЭТОТ ПОСРЕДНИК ПРИМЕНЯЕТСЯ В WEB РОУТАХ И В API ЕНДПОИНТАХ ПОКА ТОЛЬКО ПРИЛОЖЕНИЯ ТАКСИ, НО ПО ЛОГИКЕ, ЕГО НУЖНО ИСПОЛЬЗОВАТЬ ВО ВСЕХ БУДУЩИХ МОДУЛЯХ. С ЕГО ПОМОЩЬЮ ПРОВЕРЯЕТСЯ КАЖДЫЙ ЗАПРОС К МОДУЛЮ - ВООБЩЕ ЕСТЬ ДОСТУП У ПОЛЬЗОВАТЕЛЯ ИЛИ НЕТ
//В KERNEL У ЭТОГО ПОСРЕДНИКА ПРОПИСАН АЛИАС, КОТОРЫЙ УКАЗЫВАЕТСЯ УЖЕ В MODULE/ROUTES/WEB.PHP И API.PHP
/**
* Посредник проверки доступа пользователя к приложению
*/
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 РОУТ НЕ С ФРОНТА, ТО ПРОИСХОДИТ РЕДИРЕКТ БЕЗ УКАЗАНИЯ ТЕКСТА ОШИБКИ. нАПРИМЕР, ПРИ РЕДИРЕКТЕ НИЖЕ НА СТРАНИЦУ МЕНЮ, ПОЛЬЗОВАТЕЛЬ НЕ УВИДИТ НИКАКОГО ОПОВЕЩЕНИЯ ОБ ОШИБКЕ
//ЭТУ ПРОБЛЕМУ Я ИСПРАВЛЯЛ, ЧЕРЕЗ ГЕНЕРАЦИЮ НОТИФИКАЦИЙ НА БЭКЕ И ДОБАВЛЕНИЯ ИХ В ОПРЕДЕЛЕННУЮ ОЧЕРЕДЬ REDIS, КОТОРУЮ ЧИТАЕТ КАЖДАЯ СТРАНИЦА ПРИ ПЕРВИЧНОМ РЕНДЕРИНГЕ. НАДО ПОСМОТРЕТЬ ГДЕ Я УЖЕ ТАКОЕ РЕАЛИЗОВЫВАЛ
return redirect('/menu');
}
}
}
@@ -0,0 +1,67 @@
<?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 РОУТ НЕ С ФРОНТА, ТО ПРОИСХОДИТ РЕДИРЕКТ БЕЗ УКАЗАНИЯ ТЕКСТА ОШИБКИ. нАПРИМЕР, ПРИ РЕДИРЕКТЕ НИЖЕ НА СТРАНИЦУ МЕНЮ, ПОЛЬЗОВАТЕЛЬ НЕ УВИДИТ НИКАКОГО ОПОВЕЩЕНИЯ ОБ ОШИБКЕ.
//ЭТУ ПРОБЛЕМУ Я ИСПРАВЛЯЛ, ЧЕРЕЗ ГЕНЕРАЦИЮ НОТИФИКАЦИЙ НА БЭКЕ И ДОБАВЛЕНИЯ ИХ В ОПРЕДЕЛЕННУЮ ОЧЕРЕДЬ REDIS, КОТОРУЮ ЧИТАЕТ КАЖДАЯ СТРАНИЦА ПРИ ПЕРВИЧНОМ РЕНДЕРИНГЕ. НАДО ПОСМОТРЕТЬ ГДЕ Я УЖЕ ТАКОЕ РЕАЛИЗОВЫВАЛ
return redirect('/menu');
}
#Гаврилов
//РЕДИРЕКТ НА СТРАНИЦУ 403
}
}
+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',
];
}
+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',
];
}
+15
View File
@@ -0,0 +1,15 @@
-- custom.users definition
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`login` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Логин пользователя',
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,3 @@
INSERT INTO custom.users (login,name,email,email_verified_at,password,remember_token,created_at,updated_at) VALUES
('dgavrilov',NULL,NULL,NULL,NULL,NULL,'2025-11-09 16:50:05','2025-11-09 16:50:05'),
('developer',NULL,NULL,NULL,NULL,NULL,'2025-11-20 13:45:05','2025-11-20 13:45:05');
+34
View File
@@ -0,0 +1,34 @@
import React from "react";
import { useEffect, useState } from "react";
import { getCsrfToken } from './../services/getCsrfService';
function MagicLogin ()
{
//Состояние с введенным логином
const [userLogin, setUserLogin] = useState<string>('');
return (
<div id="login-container">
<div id="login-container__magic-name">Magic</div>
<div id="login-container__form">
<form action="login" method="POST" autoComplete="on">
<input
type="hidden"
name="_token"
value={getCsrfToken()}
/>
<input name="_auth_login" type="text" onChange={ (e) => setUserLogin(e.target.value) } placeholder="Логин" required/>
<input name="_auth_password" type="password" placeholder="Пароль" required/>
<button type="submit" onClick={ () => {
//При сабмите кладем логин в локалсторадж для того, чтобы при вызове api ендпоинтов передавать его в заголовках
//гаврилов
//ТОЧНО ЛИ ЭТО НУЖНО? МОЖЕТ, ЛУЧШЕ ВСЕ API ЕНДПОИНТЫ ПЕРЕПИСАТ ПОД ВЫЗОВ WEB РОУТОВ?
localStorage.setItem('magic_auth_login', userLogin);
} }>Вход</button>
</form>
</div>
</div>
);
}
export default MagicLogin;
+7
View File
@@ -0,0 +1,7 @@
import { createRoot } from 'react-dom/client';
import MagicLogin from './components/MagicLogin.tsx';
import React from 'react';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(<MagicLogin />);
+20
View File
@@ -0,0 +1,20 @@
/**
* Сервис для полуения csrt токена для размещения в формах
* @date 24.07.2025
* @author dgavrilov
*/
export const getCsrfToken = ():string => {
const METATAG:HTMLElement|null = document.querySelector('meta[name="csrf-token"]');
if (!METATAG) {
return '';
}
const CSRFTOKEN:string|null = METATAG.getAttribute('content');
if (!CSRFTOKEN) {
return '';
}
return CSRFTOKEN;
}
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laravel + React</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Без команды ниже корректно не обрабатывается React скрипт -->
@viteReactRefresh
<!-- файл со стилями resources/css/magicLogin.css не создан, так как не успел приступить к доделке меню -->
@vite(['resources/css/magicLogin.css', 'resources/js/magicLogin.tsx'])
</head>
<body>
<div id="root"></div>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
<?php
use Illuminate\Http\Request;
use \App\Http\Controllers;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
#Гаврилов
//РОУТ НИЖЕ НУЖЕН? НЕ ПОМНЮ ОТКУДА ОН ВЗЯЛСЯ. Пока оставляю, но надо разобраться для теста он или нужен на проде
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
/**
* Получение пользовательской роли
*/
Route::get('user_role/{moduleName}', [Controllers\AuthorizationController::class, 'getUserRole'])->where('moduleName', '^[a-z0-9_]+$');
+49
View File
@@ -1,7 +1,56 @@
<?php
use Illuminate\Support\Facades\Route;
use \App\Http\Controllers;
use App\Http\Middleware\AuthenticateMagic;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/
#Гаврилов
//РЕДИРЕКТ НА СТРАНИЦУ ЛОГИН
Route::get('/', function () {
return view('welcome');
});
//Роут вызова вьюхи аутентификации (без посредника аутентификации)
Route::get('/login', function () {
return view('magic_login');
})->withoutMiddleware([AuthenticateMagic::class])->name('magic_login');
Route::get('/logout', ([Controllers\LoginController::class, 'logout']))->name('magic_logout');
//Роут вызова процессе аутентификации (без посредника аутентификации)
Route::post('login', [Controllers\LoginController::class, 'ldapCheckUser'])->withoutMiddleware([AuthenticateMagic::class]);
//ГАВРИЛОВ. добавить without middleware AuthMagicApi?
//Фоновое обновление санктум токена, если api вернул 401 (санктум протух), а сессия еще "жива"
Route::get('/silent_token_refresh', [Controllers\LoginController::class, 'silentRefreshUserSanctumToken'])->withoutMiddleware([AuthenticateMagic::class]);
// Route::get('/role', [\App\Http\Controllers\TestController::class, 'getRoles'])->name('get_role');
//Route::get('/access', [\App\Http\Controllers\TestController::class, 'getAccess']);
// Route::controller(Controllers\TestController::class)->group(function ()
// {
// Route::get('/access/{id}', 'getAccess')->where(['id' => '[0-9]+'])->name('getAccessById');
// //Route::get('/access/{id}', 'getAccess')->where(['id' => '[0-9]+']);
// });
// Route::get('/testData/{int}/{char}', [Controllers\TestDataController::class, 'insertNewData'])->where(['int' => '[0-9]+', 'char' => '[a-z]+']);
// Route::get('/testParam/{id}', [\App\Http\Controllers\TestController::class, 'getParam'])->where(['id' => '[0-9]+']);
// Route::get('/redirect', [Controllers\TestController::class, 'redirect']);
// Route::post('/role', [\App\Http\Controllers\TestController::class, 'setRole']);
// Route::post('/role_del', [\App\Http\Controllers\TestController::class, 'delRole']);
// Route::get('/test_table', [Controllers\TestFormController::class, 'getForm']);
// Route::post('/test_table', [Controllers\TestFormController::class, 'setForm'])->name('test_set_form');
// Route::get('/access/{id?}', [Controllers\AccessListController::class, 'getAccess'])->where(['id' => '[0-9]+']);
// Route::delete('/access/{id}', [Controllers\AccessListController::class, 'delAccess'])->where(['id' => '[0-9]+'])->name('del_lk_tm_access');
// Route::post('/access', [Controllers\AccessListController::class, 'postAccess']);