Version 0.7.0 #1

Merged
kor-elf merged 90 commits from develop into main 2023-12-08 21:18:23 +06:00
17 changed files with 356 additions and 5 deletions
Showing only changes of commit f481ee765d - Show all commits

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use App\Dto\Request\Dto;
interface FormRequestDto
{
public function getDto(): Dto;
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto\Request;
final readonly class Authorization extends Dto
{
public function __construct(
private string $email,
private string $password,
private bool $remember = false
) { }
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): string
{
return $this->password;
}
public function getRemember(): bool
{
return $this->remember;
}
}

8
app/Dto/Request/Dto.php Normal file
View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Dto\Request;
abstract readonly class Dto
{
}

View File

@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\AuthorizationRequest;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
final class AuthController extends Controller
{
public function __construct(
private readonly AuthService $authService
) { }
public function login(): View
{
return view('public/login');
}
public function authorization(AuthorizationRequest $request)
{
$authorization = $request->getDto();
$result = $this->authService->authorization($authorization);
if (!$result->isSuccess()) {
if ($result->getCode() === Response::HTTP_UNAUTHORIZED) {
Log::warning('Unauthorized ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
}
return redirect()->route('login')->withInput()->withErrors($result->getMessage());
}
$request->session()->regenerate();
Log::notice('Logged in ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
return redirect()->route('home');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(route('login'));
}
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
final class ProfileController extends Controller
{
public function profile(): View
{
return view('private/profile/profile', [
'user' => Auth::user()
]);
}
public function settings(): View
{
return view('private/profile/settings', [
'user' => Auth::user()
]);
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Http\Requests;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Authorization;
use Illuminate\Foundation\Http\FormRequest;
final class AuthorizationRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'email' => ['required', 'email', 'max:255'],
'password' => ['required', 'min:3'],
'remember' => ['nullable', 'boolean'],
];
}
public function getDto(): Authorization
{
return new Authorization(
email: $this->input('email'),
password: $this->input('password'),
remember: (bool) $this->input('remember', false)
);
}
}

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Models\User;
use Illuminate\Support\Str;
final readonly class UserRepository
{
public function getUserByEmail(string $email): ?User
{
return User::query()->where('email', Str::lower($email))->first();
}
}

View File

@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace App\Services;
use App\Dto\Request\Authorization;
use App\Repositories\UserRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
final class AuthService extends Service
{
public function __construct(
private readonly UserRepository $userRepository
) { }
public function authorization(Authorization $authorization): ServiceResultError | ServiceResultArray
{
$user = $this->userRepository->getUserByEmail($authorization->getEmail());
if (is_null($user)) {
return $this->errUnauthorized(__('auth.failed'));
}
if (Hash::check($authorization->getPassword(), $user->password) !== true) {
return $this->errUnauthorized(__('auth.password'));
}
if ($user->is_active === false) {
return $this->errFobidden(__('auth.disabled'));
}
try {
Auth::login($user, $authorization->getRemember());
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('auth.success'));
}
}

View File

@ -5,32 +5,38 @@ namespace App\Services;
use App\ServiceResults\ServiceResultArray; use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use Illuminate\Http\Response;
abstract class Service abstract class Service
{ {
final protected function errValidate(string $message, array $errors = []): ServiceResultError final protected function errValidate(string $message, array $errors = []): ServiceResultError
{ {
return $this->error(422, $message, $errors); return $this->error(Response::HTTP_UNPROCESSABLE_ENTITY, $message, $errors);
} }
final protected function errFobidden(string $message): ServiceResultError final protected function errFobidden(string $message): ServiceResultError
{ {
return $this->error(403, $message); return $this->error(Response::HTTP_FORBIDDEN, $message);
} }
final protected function errNotFound(string $message): ServiceResultError final protected function errNotFound(string $message): ServiceResultError
{ {
return $this->error(404, $message); return $this->error(Response::HTTP_NOT_FOUND, $message);
} }
final protected function errService(string $message): ServiceResultError final protected function errService(string $message): ServiceResultError
{ {
return $this->error(500, $message); return $this->error(Response::HTTP_INTERNAL_SERVER_ERROR, $message);
} }
final protected function notAcceptable(string $message): ServiceResultError final protected function notAcceptable(string $message): ServiceResultError
{ {
return $this->error(406, $message); return $this->error(Response::HTTP_NOT_ACCEPTABLE, $message);
}
final protected function errUnauthorized(string $message): ServiceResultError
{
return $this->error(Response::HTTP_UNAUTHORIZED, $message);
} }
final protected function ok(string $message = 'OK'): ServiceResultArray final protected function ok(string $message = 'OK'): ServiceResultArray

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\View\Components\Public;
use Illuminate\View\Component;
use Illuminate\View\View;
final class Layout extends Component
{
/**
* @inheritDoc
*/
public function render(): View
{
return view('public.layout.app');
}
}

8
config/rate_limiting.php Normal file
View File

@ -0,0 +1,8 @@
<?php
return [
/**
* Max limit of the hour.
*/
'login_max_request' => env('LOGIN_MAX_REQUEST', 50),
'login_max_email_request' => env('LOGIN_MAX_EMAIL_REQUEST', 10),
];

View File

@ -6,4 +6,6 @@ return [
'failed' => 'These credentials do not match our records.', 'failed' => 'These credentials do not match our records.',
'password' => 'The password is incorrect.', 'password' => 'The password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
'success' => 'The user is logged in.',
'disabled' => 'Disabled the user.',
]; ];

View File

@ -6,4 +6,6 @@ return [
'failed' => 'Неверное имя пользователя или пароль.', 'failed' => 'Неверное имя пользователя или пароль.',
'password' => 'Некорректный пароль.', 'password' => 'Некорректный пароль.',
'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте ещё раз через :seconds секунд.', 'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте ещё раз через :seconds секунд.',
'success' => 'Пользователь вошел в систему.',
'disabled' => 'Отключили пользователя.',
]; ];

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>@yield('meta_title', '')</title>
<meta name="keywords" content="@yield('meta_keywords', '')" />
<meta name="description" content="@yield('meta_description', '')" />
@vite('resources/volt/scss/app.scss')
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<main>
<!-- Section -->
<section class="vh-lg-100 mt-5 mt-lg-0 bg-soft d-flex align-items-center">
<div class="container">
{{ $slot }}
</div>
</section>
</main>
@vite('resources/volt/js/app.js')
</body>
</html>

View File

@ -0,0 +1,60 @@
@section('meta_title', __('Sign in to our platform'))
<x-public.layout>
<div class="row justify-content-center form-bg-image" data-background-lg="{{ Vite::asset('resources/volt/images/illustrations/signin.svg') }}">
<div class="col-12 d-flex align-items-center justify-content-center">
<div class="bg-white shadow border-0 rounded border-light p-4 p-lg-5 w-100 fmxw-500">
<div class="text-center text-md-center mb-4 mt-md-0">
<h1 class="mb-0 h3">{{ __('Sign in to our platform') }}</h1>
</div>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('authorization') }}" class="mt-4" method="post">
@csrf
<!-- Form -->
<div class="form-group mb-4">
<label for="email">{{ __('Your Email') }}</label>
<div class="input-group">
<span class="input-group-text" id="basic-addon1">
<svg class="icon icon-xs text-gray-600" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path></svg>
</span>
<input type="email" name="email" class="form-control" placeholder="example@company.com" id="email" autofocus required>
</div>
</div>
<!-- End of Form -->
<div class="form-group">
<!-- Form -->
<div class="form-group mb-4">
<label for="password">{{ __('Your Password') }}</label>
<div class="input-group">
<span class="input-group-text" id="basic-addon2">
<svg class="icon icon-xs text-gray-600" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg>
</span>
<input type="password" name="password" placeholder="Password" class="form-control" id="password" required>
</div>
</div>
<!-- End of Form -->
<div class="d-flex justify-content-between align-items-top mb-4">
<div class="form-check">
<input name="remember" type="hidden" value="0">
<input class="form-check-input" name="remember" type="checkbox" value="1" id="remember">
<label class="form-check-label mb-0" for="remember">
{{ __('Remember me') }}
</label>
</div>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-gray-800">{{ __('Sign in') }}</button>
</div>
</form>
</div>
</div>
</div>
</x-public.layout>

View File

@ -12,3 +12,17 @@ use Illuminate\Support\Facades\Route;
| be assigned to the "web" middleware group. Make something great! | be assigned to the "web" middleware group. Make something great!
| |
*/ */
Route::middleware('guest')->group(function () {
Route::get('login', [\App\Http\Controllers\AuthController::class, 'login'])->name('login');
Route::middleware(['throttle:login'])->post('login', [\App\Http\Controllers\AuthController::class, 'authorization'])->name('authorization');
});
Route::middleware(['auth', 'verified'])->group(function () {
Route::post('logout', [\App\Http\Controllers\AuthController::class, 'logout'])->name('logout');
Route::get('/', [\App\Http\Controllers\Private\DashboardController::class, 'index'])->name('home');
Route::prefix('profile')->as('profile.')
->group(function () {
Route::get('/', [\App\Http\Controllers\Private\ProfileController::class, 'profile'])->name('edit');
Route::get('settings', [\App\Http\Controllers\Private\ProfileController::class, 'settings'])->name('settings');
});
});