Version 0.7.0 #1

Merged
kor-elf merged 90 commits from develop into main 2023-12-08 21:18:23 +06:00
387 changed files with 32741 additions and 24 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

66
.env.example Normal file
View File

@ -0,0 +1,66 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_DEMO_MODE=false
APP_DEMO_EMAIL=
APP_DEMO_PASSWORD=
APP_DEFAULT_USER_TIMEZONE=UTC
# Valid languages: ru | en
APP_DEFAULT_LOCALE=ru
LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=deprecations
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

40
.gitignore vendored
View File

@ -1,25 +1,19 @@
# ---> Laravel /.phpunit.cache
/vendor/ /node_modules
node_modules/ /public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
/.fleet
# Laravel 4 specific /.idea
bootstrap/compiled.php /.vscode
app/storage/
# Laravel 5 & Lumen specific
public/storage
public/hot
# Laravel 5 & Lumen specific with changed public path
public_html/storage
public_html/hot
storage/*.key
.env
Homestead.yaml
Homestead.json
/.vagrant
.phpunit.result.cache

9
LICENSE.md Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 Leonid Nikitin (kor-elf)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,2 +1,50 @@
# Service_captcha ## О проекте
Захотелось написать свой независимый сервис защиты от роботов. Сервис каптча написан на фреймворке Laravel. Вдохновлялся, а так же брал картинки с проекта <a href="https://github.com/wenlng/go-captcha" target="_blank">Go Captcha</a>.
## Зависимости
php 8.2 (модули: redis, gd)
redis
mysql 8
## Демострация
Демо сервис каптча: https://captcha-admin-demo.tut-site.net/
Email: demo@tut-site.net
Пароль: demodemo
Демо каптча: https://captcha-demo.tut-site.net/
## API
https://captcha-admin-demo.tut-site.net/api-docs/
## Javascript клиент для сайта
https://git.kor-elf.net/kor-elf/captcha-rule-for-laravel
## Как проверять со стороны бэкенда
Для Laravel 10 есть готовый пакет: https://git.kor-elf.net/kor-elf/captcha-rule-for-laravel
Можно установить этот пакет так: composer require kor-elf/captcha-rule-for-laravel
<br><b>Curl:</b>
curl -X POST 'https://captcha-admin-demo.tut-site.net/api/v1/captcha/{captcha-token}' -H 'private-token: {your-private-token}' -H 'Content-Type: application/json' -d '{"user_agent": "{user-agent}"}' --max-time 10
Где {captcha-token} - токен получил пользователь от сервиса каптча после успешной проверки.
Где {your-private-token} - приватный токен, который мы создали в админке.
Где {user-agent} - передаём user agent от пользователя, который проходил каптчу.
Успешная проверка пользователя вернёт ответ код 200 и status = true. Иначе считаем, что пользователь не прошёл проверку на робота.
## Репозиторий с демо
https://git.kor-elf.net/kor-elf/service-captcha-demo
## Лицензия
[MIT license](https://opensource.org/licenses/MIT).

View File

@ -0,0 +1,144 @@
<?php declare(strict_types=1);
namespace App\Captcha\Config;
use App\Captcha\Exceptions\CaptchaException;
use Illuminate\Support\Arr;
final readonly class ImageBody
{
public function __construct(
private array $backgrounds,
private array $fonts,
private array $fontColors,
private int $width = 300,
private int $height = 240,
private int $angle = 20,
private array | int $fontSize = [20, 50],
private int $numberLines = 3,
private array $lineColors = []
) {
if ($this->width <= 0) {
throw new CaptchaException('Incorrect $width settings.');
}
if ($this->height <= 0) {
throw new CaptchaException('Incorrect $height settings.');
}
if (count($this->backgrounds) < 1) {
throw new CaptchaException('Invalid $backgrounds parameter.');
}
if (count($this->fonts) < 1) {
throw new CaptchaException('Invalid $fonts parameter.');
}
if (count($this->fontColors) < 1) {
throw new CaptchaException('Invalid $fontColors parameter.');
}
if ($this->angle < 0) {
throw new CaptchaException('Incorrect $angle settings.');
}
if (!is_integer($this->fontSize) && !is_array($this->fontSize)) {
throw new CaptchaException('The $fontSize parameter is not an array or integer.');
} elseif (is_array($this->fontSize)) {
if (count($this->fontSize) > 2) {
throw new CaptchaException('The array of $fontSize parameters contains more than 2 keys.');
}
if ($this->fontSize[0] > $this->fontSize[1]) {
throw new CaptchaException('The number of $fontSize[1] is less than $fontSize[0].');
}
}
}
/**
* @return array
*/
public function getBackgrounds(): array
{
return $this->backgrounds;
}
public function randomBackground(): string
{
return Arr::random($this->getBackgrounds());
}
/**
* @return array
*/
public function getFonts(): array
{
return $this->fonts;
}
public function randomFont(): string
{
return Arr::random($this->getFonts());
}
/**
* @return array
*/
public function getFontColors(): array
{
return $this->fontColors;
}
public function randomFontColor(): string
{
return Arr::random($this->getFontColors());
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
/**
* @return int
*/
public function getAngle(): int
{
return $this->angle;
}
public function randomAngle(): int
{
return mt_rand($this->getAngle() * -1, $this->getAngle());
}
public function getNumberLines(): int
{
return $this->numberLines;
}
public function fontSize(): int
{
$fontSize = $this->fontSize;
if (is_integer($fontSize)) {
return $fontSize;
}
if (empty($fontSize[1])) {
return $fontSize[0];
}
return random_int($fontSize[0], $fontSize[1]);
}
/**
* @return array
*/
public function getLineColors(): array
{
return $this->lineColors;
}
}

View File

@ -0,0 +1,147 @@
<?php declare(strict_types=1);
namespace App\Captcha\Config;
use App\Captcha\Exceptions\CaptchaException;
use Illuminate\Support\Arr;
final readonly class ImageHead
{
public function __construct(
private array $backgrounds,
private array $fonts,
private array $fontColors,
private int $width = 150,
private int $height = 40,
private int $textPaddingTop = 5,
private int $textPaddingLeft = 10,
private int $angle = 20,
private int $numberLines = 3,
private array $lineColors = []
) {
if ($this->width <= 0) {
throw new CaptchaException('Incorrect $width settings.');
}
if ($this->height <= 0) {
throw new CaptchaException('Incorrect $height settings.');
}
if ($this->textPaddingTop < 0) {
throw new CaptchaException('Incorrect $textPaddingTop settings.');
}
if ($this->textPaddingLeft < 0) {
throw new CaptchaException('Incorrect $textPaddingLeft settings.');
}
if (count($this->backgrounds) < 1) {
throw new CaptchaException('Invalid $backgrounds parameter.');
}
if (count($this->fonts) < 1) {
throw new CaptchaException('Invalid $fonts parameter.');
}
if (count($this->fontColors) < 1) {
throw new CaptchaException('Invalid $fontColors parameter.');
}
if ($this->angle < 0) {
throw new CaptchaException('Incorrect $angle settings.');
}
if ($this->numberLines < 0) {
throw new CaptchaException('Incorrect $numberLines settings.');
}
}
/**
* @return array
*/
public function getBackgrounds(): array
{
return $this->backgrounds;
}
public function randomBackground(): string
{
return Arr::random($this->getBackgrounds());
}
/**
* @return array
*/
public function getFonts(): array
{
return $this->fonts;
}
public function randomFont(): string
{
return Arr::random($this->getFonts());
}
/**
* @return array
*/
public function getFontColors(): array
{
return $this->fontColors;
}
public function randomFontColor(): string
{
return Arr::random($this->getFontColors());
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
/**
* @return int
*/
public function getTextPaddingTop(): int
{
return $this->textPaddingTop;
}
/**
* @return int
*/
public function getTextPaddingLeft(): int
{
return $this->textPaddingLeft;
}
/**
* @return int
*/
public function getAngle(): int
{
return $this->angle;
}
public function randomAngle(): int
{
return mt_rand($this->getAngle() * -1, $this->getAngle());
}
public function getNumberLines(): int
{
return $this->numberLines;
}
/**
* @return array
*/
public function getLineColors(): array
{
return $this->lineColors;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
use App\Captcha\Dto\Coordinators;
interface Image
{
public function __construct(int $width, int $height);
public function getWidth(): int;
public function getHeight(): int;
public function insertBackground(string $pathToFile): self;
public function addText(string $text, int $x, int $y, float $size, float $angle, string $hexColor, string $fontName): Coordinators;
public function addLine(int $x1, int $y1, int $x2, int $y2, string $hexColor): self;
public function encode(): string;
public function __destruct();
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
use App\Captcha\Config\ImageBody as ImageBodyConfig;
use App\Captcha\Dto\ImageBody as ImageBodyDto;
use App\Captcha\Dto\Symbols;
interface ImageBody
{
public function processing(Symbols $symbols, ImageBodyConfig $config): ImageBodyDto;
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
use App\Captcha\Dto\ImageHead as ImageHeadDto;
use App\Captcha\Dto\Symbols;
use App\Captcha\Config\ImageHead as ImageHeadConfig;
interface ImageHead
{
public function processing(Symbols $symbols, ImageHeadConfig $config): ImageHeadDto;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
interface ImageLines
{
public function processing(Image $image, array $colors, int $lines = 3): Image;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Contracts;
interface ImageManager
{
public function createImage(int $width, int $height): Image;
}

View File

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

View File

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class Coordinators
{
public function __construct(
private int $x1,
private int $y1,
private int $x2,
private int $y2,
private int $x3,
private int $y3,
private int $x4,
private int $y4
) { }
/**
* lower left x-coordinate
* @return int
*/
public function getX1(): int
{
return $this->x1;
}
/**
* lower left y-coordinate
* @return int
*/
public function getY1(): int
{
return $this->y1;
}
/**
* lower right x-coordinate
* @return int
*/
public function getX2(): int
{
return $this->x2;
}
/**
* lower right y-coordinate
* @return int
*/
public function getY2(): int
{
return $this->y2;
}
/**
* upper right x-coordinate
* @return int
*/
public function getX3(): int
{
return $this->x3;
}
/**
* upper right y-coordinate
* @return int
*/
public function getY3(): int
{
return $this->y3;
}
/**
* upper left x-coordinate
* @return int
*/
public function getX4(): int
{
return $this->x4;
}
/**
* upper left y-coordinate
* @return int
*/
public function getY4(): int
{
return $this->y4;
}
}

36
app/Captcha/Dto/Image.php Normal file
View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class Image
{
public function __construct(
private string $imageBase64,
private int $width,
private int $height
) { }
/**
* @return string
*/
public function getImageBase64(): string
{
return $this->imageBase64;
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class ImageBody
{
public function __construct(
private Image $image,
private array $coordinators
) { }
/**
* @return Image
*/
public function getImage(): Image
{
return $this->image;
}
/**
* @return array
*/
public function getCoordinators(): array
{
return $this->coordinators;
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class ImageHead
{
public function __construct(
private Image $image
) { }
/**
* @return Image
*/
public function getImage(): Image
{
return $this->image;
}
}

View File

@ -0,0 +1,45 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final readonly class Sector
{
public function __construct(
private int $x,
private int $y,
private int $width,
private int $height,
) { }
/**
* @return int
*/
public function getX(): int
{
return $this->x;
}
/**
* @return int
*/
public function getY(): int
{
return $this->y;
}
/**
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
final class Sectors
{
private array $points = [];
public function add(int|float $x, int|float $y, int|float $width, int|float $height): self
{
$this->points[] = new Sector((int) $x, (int) $y, (int) $width, (int) $height);
return $this;
}
public function random(): Sector
{
$key = array_rand($this->points);
$sector = $this->points[$key];
unset($this->points[$key]);
return $sector;
}
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace App\Captcha\Dto;
use App\Captcha\Enums\SymbolType;
final readonly class Symbols
{
public function __construct(
private array $success,
private array $fakes,
private SymbolType $type
) { }
/**
* @return array
*/
public function getSuccess(): array
{
return $this->success;
}
/**
* @return array
*/
public function getFakes(): array
{
return $this->fakes;
}
/**
* @return SymbolType
*/
public function getType(): SymbolType
{
return $this->type;
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Enums;
Enum SymbolType {
case String;
case ImagePath;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Captcha\Exceptions;
final class CaptchaException extends \Exception
{
}

117
app/Captcha/Images/Body.php Normal file
View File

@ -0,0 +1,117 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Config\ImageBody as ImageBodyConfig;
use App\Captcha\Contracts\ImageLines;
use App\Captcha\Dto\Coordinators;
use App\Captcha\Dto\Image as ImageDto;
use App\Captcha\Dto\ImageBody as ImageBodyDto;
use App\Captcha\Dto\Sector;
use App\Captcha\Dto\Sectors;
use App\Captcha\Dto\Symbols;
use App\Captcha\Contracts\ImageBody;
use App\Captcha\Enums\SymbolType;
use App\Captcha\Contracts\ImageManager;
final class Body implements ImageBody
{
public function __construct(
private readonly ImageManager $imageManager,
private readonly ImageLines $imageLines
) { }
public function processing(Symbols $symbols, ImageBodyConfig $config): ImageBodyDto
{
$image = $this->imageManager->createImage($config->getWidth(), $config->getHeight());
$image->insertBackground($config->randomBackground());
$imageData = match ($symbols->getType()) {
SymbolType::String => $this->processingString($image, $symbols, $config),
};
$image = $imageData['image'];
if ($config->getNumberLines() > 0) {
$this->imageLines->processing(image: $image, colors: $config->getLineColors(), lines: $config->getNumberLines());
}
$image = new ImageDto(
imageBase64: $image->encode(),
width: $image->getWidth(),
height: $image->getHeight()
);
return new ImageBodyDto(
image: $image,
coordinators: $imageData['coordinators']
);
}
private function processingString(Image $image, Symbols $symbols, ImageBodyConfig $config): array
{
$sectors = $this->calculateSectors($symbols, $image->getWidth(), $image->getHeight());
$coordinators = [];
foreach ($symbols->getSuccess() as $number => $symbol) {
$coordinators[] = $this->processingStringSymbol($image, $symbol, $config, $sectors->random());
}
if (!empty($symbols->getFakes())) {
foreach ($symbols->getFakes() as $number => $symbol) {
$this->processingStringSymbol($image, $symbol, $config, $sectors->random());
}
}
return [
'image' => $image,
'coordinators' => $coordinators
];
}
private function processingStringSymbol(Image &$image, string|int $symbol, ImageBodyConfig $config, Sector $sector): Coordinators
{
$fontSize = $config->fontSize();
if ($fontSize > $sector->getHeight()) {
$fontSize = $sector->getHeight();
}
$fontPathFile = $config->randomFont();
$fontColor = $config->randomFontColor();
$angle = $config->randomAngle();
$marginLeft = mt_rand($sector->getX(), $sector->getX() + $sector->getWidth() - $fontSize);
$marginTop = mt_rand($sector->getY(), $sector->getY() + $sector->getHeight() - $fontSize);
return $image->addText(
text: $symbol,
x: $marginLeft,
y: $marginTop,
size: $fontSize,
angle: $angle,
hexColor: $fontColor,
fontName: $fontPathFile
);
}
private function calculateSectors(Symbols $symbols, int $width, int $height): Sectors
{
$points = count($symbols->getSuccess()) + count($symbols->getFakes());
$sumFloors = 3;
$sumRooms = ceil($points / $sumFloors);
$heightFloor = floor($height / $sumFloors);
$widthFloor = floor($width / $sumRooms);
$sectors = new Sectors();
for ($floor = 0; $floor < $sumFloors; $floor++) {
$y = $heightFloor * $floor;
for ($room = 0; $room < $sumRooms; $room++) {
$sectors->add(
x: ($widthFloor * $room),
y: $y,
width: $widthFloor,
height: $heightFloor
);
}
}
return $sectors;
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Config\ImageHead as ImageHeadConfig;
use App\Captcha\Contracts\ImageLines;
use App\Captcha\Dto\Image as ImageDto;
use App\Captcha\Dto\ImageHead as ImageHeadDto;
use App\Captcha\Dto\Symbols;
use App\Captcha\Contracts\ImageHead;
use \App\Captcha\Contracts\Image as ImageContract;
use App\Captcha\Enums\SymbolType;
use App\Captcha\Contracts\ImageManager;
final class Head implements ImageHead
{
public function __construct(
private readonly ImageManager $imageManager,
private readonly ImageLines $imageLines
) { }
public function processing(Symbols $symbols, ImageHeadConfig $config): ImageHeadDto
{
$image = $this->imageManager->createImage($config->getWidth(), $config->getHeight());
$image->insertBackground($config->randomBackground());
$image = match ($symbols->getType()) {
SymbolType::String => $this->processingString($image, $symbols, $config),
};
if ($config->getNumberLines() > 0) {
$this->imageLines->processing(image: $image, colors: $config->getLineColors(), lines: $config->getNumberLines());
}
$image = new ImageDto(
imageBase64: $image->encode(),
width: $image->getWidth(),
height: $image->getHeight()
);
return new ImageHeadDto(image: $image);
}
private function processingString(ImageContract $image, Symbols $symbols, ImageHeadConfig $config): Image
{
$textLeftPadding = $config->getTextPaddingLeft();
$textTopPadding = $config->getTextPaddingTop();
$countSymbols = count($symbols->getSuccess());
$widthPrefix = $image->getWidth() - $textLeftPadding;
$imageHeight = $image->getHeight() - $textTopPadding;
foreach ($symbols->getSuccess() as $number => $symbol) {
$marginLeft = $textLeftPadding + $number * $widthPrefix / $countSymbols;
$image->addText(
text: $symbol,
x: intval($marginLeft),
y: $textTopPadding,
size: $this->randomFontSize($imageHeight),
angle: $config->randomAngle(),
hexColor: $config->randomFontColor(),
fontName: $config->randomFont()
);
}
return $image;
}
private function randomFontSize(int $imageHeight): int
{
return mt_rand($imageHeight - 10, $imageHeight);
}
}

View File

@ -0,0 +1,138 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Contracts\Image as ImageContract;
use App\Captcha\Dto\Coordinators;
use App\Captcha\Exceptions\CaptchaException;
final class Image implements ImageContract
{
private readonly \GdImage $image;
public function __construct(int $width, int $height) {
$this->image = imagecreatetruecolor($width, $height);
}
public function getWidth(): int
{
return imagesx($this->image);
}
public function getHeight(): int
{
return imagesy($this->image);
}
public function insertBackground(string $pathToFile): ImageContract
{
list($backgroundWidth, $backgroundHeight) = getimagesize($pathToFile);
$background = $this->createImageFromPathToFile($pathToFile);
imagecopyresampled(
dst_image: $this->image,
src_image: $background,
dst_x: 0,
dst_y: 0,
src_x: 0,
src_y: 0,
dst_width: $this->getWidth(),
dst_height: $this->getHeight(),
src_width: $backgroundWidth,
src_height: $backgroundHeight
);
imagedestroy($background);
return $this;
}
public function addText(string $text, int $x, int $y, float $size, float $angle, string $hexColor, string $fontName): Coordinators
{
$y += intval($size);
$result = imagefttext(
image: $this->image,
size: $size,
angle: $angle,
x: $x,
y: $y,
color: $this->colorConvertHexToColorIndex($hexColor),
font_filename: $fontName,
text: $text
);
return new Coordinators(
x1: $result[0],
y1: $result[1],
x2: $result[2],
y2: $result[3],
x3: $result[4],
y3: $result[5],
x4: $result[6],
y4: $result[7]
);
}
public function addLine(int $x1, int $y1, int $x2, int $y2, string $hexColor): self
{
imageline(
image: $this->image,
x1: $x1,
y1: $y1,
x2: $x2,
y2: $y2,
color: $this->colorConvertHexToColorIndex($hexColor)
);
return $this;
}
public function encode(): string
{
ob_start();
imagealphablending($this->image, false);
imagesavealpha($this->image, true);
imagepng($this->image, null, -1);
$mime = image_type_to_mime_type(IMAGETYPE_PNG);
$buffer = ob_get_contents();
ob_end_clean();
return sprintf('data:%s;base64,%s',
$mime,
base64_encode($buffer)
);
}
public function __destruct() {
imagedestroy($this->image);
}
private function createImageFromPathToFile(string $pathToFile): \GdImage
{
return match (mime_content_type($pathToFile)) {
'image/jpeg' => imagecreatefromjpeg($pathToFile),
'image/png' => imagecreatefrompng($pathToFile),
default => throw new CaptchaException('Couldn\'t open the file. Not a valid type. File ' . $pathToFile . '.')
};
}
private function colorConvertHexToColorIndex(string $hexColor): int
{
$hexColor = str_replace('#', '', $hexColor);
if (strlen($hexColor) === 3) {
return imagecolorexact(
image: $this->image,
red: hexdec(str_repeat(substr($hexColor, 0, 1), 2)),
blue: hexdec(str_repeat(substr($hexColor, 1, 1), 2)),
green: hexdec(str_repeat(substr($hexColor, 2, 1), 2))
);
}
$colorVal = hexdec($hexColor);
return imagecolorexact(
image: $this->image,
red: 0xFF & ($colorVal >> 0x10),
blue: 0xFF & ($colorVal >> 0x8),
green: 0xFF & $colorVal
);
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Contracts\Image;
use App\Captcha\Contracts\ImageManager as ImageManagerContract;
final class ImageManager implements ImageManagerContract
{
public function __construct(
private readonly string $imageClassName
) { }
public function createImage(int $width, int $height): Image
{
return new $this->imageClassName($width, $height);
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace App\Captcha\Images;
use App\Captcha\Contracts\ImageLines;
use Illuminate\Support\Arr;
use App\Captcha\Contracts\Image as ImageContract;
class Lines implements ImageLines
{
public function processing(ImageContract $image, array $colors, $lines = 3): ImageContract
{
$imageWidth = $image->getWidth();
$imageHeight = $image->getHeight();
for ($i = 0; $i <= $lines; $i++) {
$image->addLine(
x1: mt_rand(0, $imageWidth) + $i * mt_rand(0, $imageHeight),
y1: mt_rand(0, $imageHeight),
x2: mt_rand(0, $imageWidth),
y2: mt_rand(0, $imageHeight),
hexColor: $this->lineColors($colors)
);
}
return $image;
}
private function lineColors(array $colors): string
{
if (!empty($colors)) {
return Arr::random($colors);
}
return '#' . str_pad(dechex(mt_rand(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT);
}
}

26
app/Captcha/Type.php Normal file
View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Captcha;
use App\Captcha\Contracts\Type as TypeContract;
use App\Captcha\Dto\Symbols;
abstract class Type implements TypeContract
{
public function __construct(
private readonly array $config
) { }
abstract public function getSymbols(): Symbols;
final public function getConfig(): array
{
return $this->config;
}
final public function getConfigValue(string | int $name, mixed $default = null): mixed
{
$config = $this->getConfig();
return $config[$name] ?? $default;
}
}

View File

@ -0,0 +1,104 @@
<?php declare(strict_types=1);
namespace App\Captcha\Types;
use App\Captcha\Dto\Symbols;
use App\Captcha\Enums\SymbolType;
use App\Captcha\Exceptions\CaptchaException;
use App\Captcha\Type;
use Illuminate\Support\Arr;
final class StringType extends Type
{
public function getSymbols(): Symbols
{
$lengthSymbols = $this->lengthSymbols();
$lengthFakeSymbols = $this->lengthFakeSymbols();
$success = $this->randomSymbols($lengthSymbols);
$fakes = [];
if (!empty($lengthFakeSymbols)) {
$fakes = $this->randomSymbols($lengthFakeSymbols, $success);
}
return new Symbols(
success: $success,
fakes: $fakes,
type: SymbolType::String
);
}
/**
* @param int $min
* @param int $max
* @param array $except
* @return array
* @throws CaptchaException
*/
private function randomSymbols(int $lenght, array $except = []): array
{
$symbols = $this->symbols();
if (!empty($except)) {
$symbols = array_diff($symbols, $except);
}
if (count($symbols) < $lenght) {
throw new CaptchaException('The number of characters is less than $lenght.');
}
return Arr::random($symbols, $lenght);
}
/**
* @return array
* @throws CaptchaException
*/
private function symbols(): array
{
$symbols = $this->getConfigValue('symbols');
if (!is_array($symbols)) {
throw new CaptchaException('The symbols parameter is not an array.');
}
return $symbols;
}
private function lengthSymbols(): int
{
$length = $this->getConfigValue('length_symbols');
if (is_integer($length)) {
return $length;
}
if (!is_array($length)) {
throw new CaptchaException('The length_symbols parameter is not an array or integer.');
}
if (count($length) > 2) {
throw new CaptchaException('The array of length_symbols parameters contains more than 2 keys.');
}
if ($length[0] > $length[1]) {
throw new CaptchaException('The number of length_symbols[1] is less than length_symbols[0].');
}
return random_int($length[0], $length[1]);
}
private function lengthFakeSymbols(): int
{
$length = $this->getConfigValue('length_fake_symbols');
if (is_integer($length)) {
return $length;
}
if (is_null($length) || $length === false) {
return 0;
}
if (!is_array($length)) {
throw new CaptchaException('The length_fake_symbols parameter is not an array or integer or null or false.');
}
if (count($length) > 2) {
throw new CaptchaException('The array of length_fake_symbols parameters contains more than 2 keys.');
}
if ($length[0] > $length[1]) {
throw new CaptchaException('The number of length_fake_symbols[1] is less than length_fake_symbols[0].');
}
return random_int($length[0], $length[1]);
}
}

View File

@ -0,0 +1,94 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use App\Dto\User\ManyRoleDto;
use App\Enums\SystemRole;
use App\Repositories\RoleRepository;
use App\Services\User\UserCommandHandler;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Validator;
final class CreateUserAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:create-user-admin {email} {password}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create admin user.';
/**
* Execute the console command.
*/
public function handle(UserCommandHandler $userCommandHandler, RoleRepository $roleRepository): void
{
$validator = $this->getData();
if ($validator->fails()) {
$this->errorMessageAndStop($validator->errors()->all());
}
$data = $validator->valid();
try {
$role = $roleRepository->getRoleByCode(SystemRole::Admin->value);
if (is_null($role)) {
$this->errorMessageAndStop('Administrator role not found.');
}
$user = DB::transaction(function () use($data, $userCommandHandler, $role) {
$data['name'] = 'Administrator';
$user = $userCommandHandler->handleStore($data, $data['password']);
$userCommandHandler->handleConfirmationByEmail($user);
$roles = new ManyRoleDto([$role->id]);
$userCommandHandler->handleSyncRoles($user, $roles);
return $user;
});
} catch (\Throwable $e) {
$this->errorMessageAndStop($e->getMessage());
}
$this->info('The command was successful!');
}
private function getData(): Validator
{
return ValidatorFacade::make([
'email' => $this->argument('email'),
'password' => $this->argument('password'),
], [
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', Password::default()],
]);
}
private function stop(): never
{
exit;
}
private function errorMessageAndStop(string | array $error): never
{
$this->info('User not created. See error messages below:');
if (is_array($error)) {
foreach ($error as $err) {
$this->error($err);
}
} else {
$this->error($error);
}
$this->stop();
}
}

27
app/Console/Kernel.php Normal file
View File

@ -0,0 +1,27 @@
<?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();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface CryptographyContract
{
public function encrypt(string $text): string;
public function decrypt(string $encryptedText): string;
}

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,11 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
interface GenerateTokenCommand
{
public function execute(): string;
public function unique(Builder $builder, string $field, int $numberAttempts = 10): string;
}

18
app/Contracts/Search.php Normal file
View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
interface Search
{
public function __construct(Relation | Builder $query);
public function all(): Collection;
public function get(int $limit): Collection;
public function pagination(int $limit, int $page = 1): LengthAwarePaginator;
public function cursorPaginate(int $limit): CursorPaginator;
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace App\Contracts;
interface ServiceResult
{
public function isSuccess(): bool;
public function isError(): bool;
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface ServiceResultError
{
public function getCode(): ?int;
public function getMessage(): string;
public function getErrors(): array;
public function getErrorsOrMessage(): array|string;
public function getData(): array;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class CaptchaToken
{
public function __construct(
) { }
}

10
app/Dto/Builder/Role.php Normal file
View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class Role
{
public function __construct(
) { }
}

10
app/Dto/Builder/User.php Normal file
View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class User
{
public function __construct(
) { }
}

27
app/Dto/HttpUserData.php Normal file
View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto;
final readonly class HttpUserData
{
public function __construct(
private ?string $clientIp = null,
private ?string $userAgent = null,
private ?string $referer = null
) { }
public function getClientIp(): ?string
{
return $this->clientIp;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function getReferer(): ?string
{
return $this->referer;
}
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto;
final readonly class QuerySettingsDto
{
public function __construct(
private int $limit,
private int $page = 1,
private array $queryWith = []
) { }
public function getLimit(): int
{
return $this->limit;
}
public function getPage(): int
{
return $this->page;
}
public function getQueryWith(): array
{
return $this->queryWith;
}
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Dto\Repository\DataCaptchaRepository;
final readonly class DataCaptcha
{
public function __construct(
private int $captchaId,
private array $coordinators,
) { }
public function getCaptchaId(): int
{
return $this->captchaId;
}
public function getCoordinators(): array
{
return $this->coordinators;
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Api\V1\Captcha;
use App\Dto\HttpUserData;
use App\Dto\Request\Dto;
use App\Models\CaptchaToken;
final readonly class CaptchaPublicToken extends Dto
{
public function __construct(
private CaptchaToken $captchaToken,
private HttpUserData $httpUserData
) { }
public function getCaptchaToken(): CaptchaToken
{
return $this->captchaToken;
}
public function getHttpUserData(): HttpUserData
{
return $this->httpUserData;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Api\V1\Captcha;
use App\Dto\Request\Dto;
final readonly class CheckingDto extends Dto
{
public function __construct(
private CaptchaPublicToken $captchaPublicToken,
private string $captchaKey,
private array $coordinators,
) { }
public function getCaptchaPublicToken(): CaptchaPublicToken
{
return $this->captchaPublicToken;
}
public function getCaptchaKey(): string
{
return $this->captchaKey;
}
public function getCoordinators(): array
{
return $this->coordinators;
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Api\V1\Captcha;
use App\Dto\HttpUserData;
use App\Dto\Request\Dto;
use App\Models\CaptchaToken;
final readonly class VerificationInformationDto extends Dto
{
public function __construct(
private CaptchaToken $captchaToken,
private HttpUserData $httpUserData,
private ?string $userAgent = null,
) { }
public function getCaptchaToken(): CaptchaToken
{
return $this->captchaToken;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function getHttpUserData(): HttpUserData
{
return $this->httpUserData;
}
}

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,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\CaptchaToken;
use App\Dto\Builder\CaptchaToken;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private CaptchaToken $captchaTokenDto,
private int $page
) { }
public function getCaptchaTokenDto(): CaptchaToken
{
return $this->captchaTokenDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\CaptchaToken;
use App\Dto\Request\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $title
) { }
public function getTitle(): string
{
return $this->title;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Profile;
use App\Dto\Request\Dto;
final readonly class Update extends Dto
{
public function __construct(
private string $name
) { }
public function getName(): string
{
return $this->name;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Profile;
use App\Dto\Request\Dto;
use App\Enums\Lang;
final readonly class UpdateSettings extends Dto
{
public function __construct(
private ?Lang $lang,
private ?string $timezone
) { }
public function getLang(): ?Lang
{
return $this->lang;
}
public function getTimezone(): ?string
{
return $this->timezone;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Role;
use App\Dto\Builder\Role;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private Role $roleBuilderDto,
private int $page
) { }
public function getRoleBuilderDto(): Role
{
return $this->roleBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Role;
use App\Dto\Request\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private ?string $code,
private array $permissions,
) { }
public function getName(): string
{
return $this->name;
}
public function getCode(): ?string
{
return $this->code;
}
public function getPermissions(): array
{
return $this->permissions;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Builder\User;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private User $userBuilderDto,
private int $page
) { }
public function getUserBuilderDto(): User
{
return $this->userBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Request\Dto;
use App\Dto\User\ManyRoleDto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $email,
private bool $isActive,
private ManyRoleDto $roles,
private ?string $password = null
) { }
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getRoles(): ManyRoleDto
{
return $this->roles;
}
public function isActive(): bool
{
return $this->isActive;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Request\Dto;
final readonly class UpdatePassword extends Dto
{
public function __construct(
private string $password
) { }
public function getPassword(): string
{
return $this->password;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace App\Dto\User;
use App\Exceptions\Dto\User\ManyRoleDtoException;
final class ManyRoleDto
{
private array $roles = [];
public function __construct(array $roles = []) {
foreach ($roles as $role) {
if (!is_numeric($role) || is_float($role)) {
throw new ManyRoleDtoException('Not an integer: ' . $role . '.');
}
$this->add((int) $role);
}
}
public function add(int $id): void
{
if ($id < 1) {
throw new ManyRoleDtoException('Only Integer > 0.');
}
$this->roles[] = $id;
}
public function toArray(): array
{
return $this->roles;
}
}

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum CaptchaLogType: int
{
case Created = 1;
case Error = 2;
case Verified = 3;
case ReadVerified = 4;
}

46
app/Enums/Lang.php Normal file
View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace App\Enums;
use Illuminate\Support\Collection;
enum Lang: int
{
case Ru = 1;
case En = 2;
public function getTitle(): string
{
return match ($this) {
self::Ru => 'Русский',
self::En => 'English'
};
}
public function getLocale(): string
{
return match ($this) {
self::Ru => 'ru',
self::En => 'en'
};
}
public static function toArray(): array
{
$choices = [];
foreach (self::cases() as $lang) {
$choices[] = [
'name' => $lang->name,
'value' => $lang->value,
'title' => $lang->getTitle(),
'locale' => $lang->getLocale()
];
}
return $choices;
}
public static function toCollection(): Collection
{
return collect(self::toArray());
}
}

67
app/Enums/Permission.php Normal file
View File

@ -0,0 +1,67 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum Permission: string
{
case Role = 'role';
case User = 'user';
case CaptchaToken = 'captcha_token';
public function getPermissions(): array
{
$permissions = match ($this) {
self::CaptchaToken => [
'view' => __('permissions.Allowed to watch all tokens'),
'view_own' => __('permissions.Allowed to view own tokens'),
'create' => __('permissions.Allowed to create tokens'),
'update' => __('permissions.Allowed to edit all tokens'),
'update_own' => __('permissions.Allowed to edit own tokens'),
'delete' => __('permissions.Allowed to delete all tokens'),
'delete_own' => __('permissions.Allowed to delete own tokens'),
],
default => $this->getBasePermissions()
};
return $permissions;
}
public function getTitle(): string
{
return __('permissions.' . $this->name);
}
public function formatValue(string $permission): string
{
return $this->value . '.' . $permission;
}
public static function toArrayList(): array
{
$permissions = [];
foreach (self::cases() as $permissionEnum) {
foreach ($permissionEnum->getPermissions() as $permissionName => $permissionTitle) {
$name = $permissionEnum->formatValue($permissionName);
$title = $permissionEnum->getTitle() . ' - ' . $permissionTitle;
$permissions[$name] = $title;
}
}
return $permissions;
}
public static function toArrayListCodes(): array
{
return \array_keys(self::toArrayList());
}
private function getBasePermissions(): array
{
return [
'view' => __('permissions.Allowed to watch'),
'create' => __('permissions.Allowed to create'),
'update' => __('permissions.Allowed to edit'),
'delete' => __('permissions.Allowed to delete'),
];
}
}

8
app/Enums/SystemRole.php Normal file
View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum SystemRole: string
{
case Admin = 'admin';
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Dto\User;
final class ManyRoleDtoException extends \Exception
{
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];
/**
* A 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) {
//
});
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Repositories;
final class DataCaptchaRepositoryException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Rule;
final class RoleSyncPermissionsCommandHandlerException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Service;
final class GenerateTokenCommandException extends \Exception
{
}

39
app/Helpers/Helpers.php Normal file
View File

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace App\Helpers;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Collection;
final readonly class Helpers
{
public static function getTimeZoneList(): Collection
{
return Cache::rememberForever('timezones_list_collection', function () {
$timezone = [];
foreach (timezone_identifiers_list(\DateTimeZone::ALL) as $key => $value) {
$timezone[$value] = $value . ' (UTC ' . now($value)->format('P') . ')';
}
return collect($timezone)->sortKeys();
});
}
public static function getUserTimeZone() {
return auth()->user()?->timezone ?? config('app.user_timezone');
}
public static function isDemoMode(): bool
{
return config('app.demo_mode', false);
}
public static function isDemoModeAndUserDenyUpdate(User $user): bool
{
if (self::isDemoMode() !== true) {
return false;
}
return $user->email === config('app.demo_email');
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Requests\Api\V1\Captcha\CaptchaRequest;
use App\Http\Requests\Api\V1\Captcha\CheckingRequest;
use App\Http\Requests\Api\V1\Captcha\VerificationInformationRequest;
use App\Http\Resources\Api\V1\Captcha;
use App\Http\Resources\Api\V1\CaptchaVerificationInformation;
use App\Http\Resources\Api\V1\CaptchaVerified;
use App\Services\Api\V1\CaptchaService;
use Illuminate\Http\JsonResponse;
final class CaptchaController extends Controller
{
public function __construct(
private readonly CaptchaService $captchaService
) { }
public function getCaptcha(CaptchaRequest $request): JsonResponse
{
$captchaPublicToken = $request->getDto();
$expires = now()->addSeconds(config('captcha.waiting_for_captcha_verification_in_seconds'));
$result = $this->captchaService->createKeyWithCaptcha($captchaPublicToken, $expires);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new Captcha($result));
}
public function checking(CheckingRequest $request): JsonResponse
{
$maxCountError = config('captcha.validate_max_count_errors');
$params = $request->getDto();
$result = $this->captchaService->checking($params, $maxCountError);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new CaptchaVerified($result));
}
public function verificationInformation(string $captchaUuid, VerificationInformationRequest $request): JsonResponse
{
$params = $request->getDto();
$expiresMinutes = config('captcha.verification_data_view_limit_in_minutes');
$maxInfoDisplayCount = config('captcha.max_info_display_count');
$result = $this->captchaService->verificationInformation($captchaUuid, $params, $expiresMinutes, $maxInfoDisplayCount);
if (!$result->isSuccess()) {
return response()->json($result->getData())->setStatusCode($result->getCode());
}
return response()->json(new CaptchaVerificationInformation($result));
}
}

View File

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

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): RedirectResponse
{
$authorization = $request->getDto();
$result = $this->authService->authorization($authorization);
if ($result->isError()) {
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,93 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\CaptchaTokens\IndexRequest;
use App\Http\Requests\Private\CaptchaTokens\StoreUpdateRequest;
use App\Services\Private\CaptchaTokenService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class CaptchaTokensController extends Controller
{
public function __construct(
private readonly CaptchaTokenService $captchaTokenService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->captchaTokenService->index($data->getCaptchaTokenDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->captchaTokenService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->captchaTokenService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->captchaTokenService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->captchaTokenService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->captchaTokenService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.index')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Contracts\ServiceResultError as ServiceResultErrorContract;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
final protected function errors(ServiceResultErrorContract $result): never
{
if ($result->getCode() === Response::HTTP_UNPROCESSABLE_ENTITY) {
redirect()->back()->withInput()->withErrors($result->getErrors());
exit;
}
abort($result->getCode(), $result->getMessage());
}
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use Illuminate\View\View;
final class DashboardController extends Controller
{
public function index(): View
{
return view('private/dashboard/index');
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Enums\Lang;
use App\Helpers\Helpers;
use App\Http\Requests\Private\Profile\UpdatePasswordRequest;
use App\Http\Requests\Private\Profile\UpdateRequest;
use App\Http\Requests\Private\Profile\UpdateSettingsRequest;
use App\Services\Private\ProfileService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ProfileController extends Controller
{
public function __construct(
private readonly ProfileService $profileService
) { }
public function profile(Request $request): View
{
return view('private/profile/profile', [
'user' => $request->user()
]);
}
public function settings(Request $request): View
{
return view('private/profile/settings', [
'user' => $request->user(),
'languages' => Lang::toCollection()->pluck(value: 'title', key: 'value')->toArray(),
'timezone' => Helpers::getTimeZoneList()->toArray(),
]);
}
public function update(UpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->update($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updatePassword(UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updatePassword($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updateSettings(UpdateSettingsRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updateSettings($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.settings')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\Roles\IndexRequest;
use App\Http\Requests\Private\Roles\StoreUpdateRequest;
use App\Services\Private\RoleService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class RolesController extends Controller
{
public function __construct(
private readonly RoleService $roleService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->roleService->index($data->getRoleBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/roles/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->roleService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/roles/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->roleService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/roles/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->roleService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('roles.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->roleService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('roles.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->roleService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('roles.index')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,106 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\Users\IndexRequest;
use App\Http\Requests\Private\Users\StoreUpdateRequest;
use App\Http\Requests\Private\Users\UpdatePasswordRequest;
use App\Services\Private\UserService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UsersController extends Controller
{
public function __construct(
private readonly UserService $userService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->userService->index($data->getUserBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->userService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->userService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function updatePassword(int $id, UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->updatePassword($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->userService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.index')->withSuccess($result->getMessage());
}
}

68
app/Http/Kernel.php Normal file
View File

@ -0,0 +1,68 @@
<?php
namespace App\Http;
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,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used 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,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
'verified' => \App\Http\Middleware\EnsureUserIsVerified::class,
'user.locale' => \App\Http\Middleware\UserLocale::class,
];
}

View File

@ -0,0 +1,17 @@
<?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');
}
}

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 = [
//
];
}

View File

@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
final class EnsureUserIsVerified
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $redirectToRoute
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse|null
*/
public function handle($request, Closure $next, $redirectToRoute = null)
{
if (! $request->user()) {
return $request->expectsJson()
? abort(403)
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
if ($request->user() instanceof MustVerifyEmail && ! $request->user()->hasVerifiedEmail()) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
if ($request->user()->is_active === false) {
return $request->expectsJson()
? abort(403, 'User disabled.')
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
return $next($request);
}
}

View File

@ -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 = [
//
];
}

View File

@ -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);
}
}

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',
];
}

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(),
];
}
}

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;
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
final class UserLocale
{
public function handle(Request $request, Closure $next)
{
if ($request->user() && $request->user()->lang) {
App::setLocale($request->user()->lang->getLocale());
}
return $next($request);
}
}

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',
];
}

View File

@ -0,0 +1,17 @@
<?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 = [
//
];
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Captcha;
use App\Contracts\FormRequestDto;
use App\Dto\HttpUserData;
use App\Dto\Request\Api\V1\Captcha\CaptchaPublicToken;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use Illuminate\Foundation\Http\FormRequest;
final class CaptchaRequest extends FormRequest implements FormRequestDto
{
private readonly CaptchaToken $captchaToken;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(CaptchaTokenRepository $captchaTokenRepository): bool
{
if (!$this->hasHeader('public-token')) {
return false;
}
$captchaToken = $captchaTokenRepository->getCaptchaTokenByPublicToken($this->header('public-token'));
if (is_null($captchaToken)) {
return false;
}
$this->captchaToken = $captchaToken;
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
];
}
public function getDto(): CaptchaPublicToken
{
$httpUserData = new HttpUserData(
$this->getClientIp(),
$this->userAgent(),
$this->header('referer')
);
return new CaptchaPublicToken(
$this->captchaToken,
$httpUserData
);
}
}

View File

@ -0,0 +1,68 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Captcha;
use App\Contracts\FormRequestDto;
use App\Dto\HttpUserData;
use App\Dto\Request\Api\V1\Captcha\CaptchaPublicToken;
use App\Dto\Request\Api\V1\Captcha\CheckingDto;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use Illuminate\Foundation\Http\FormRequest;
final class CheckingRequest extends FormRequest implements FormRequestDto
{
private readonly CaptchaToken $captchaToken;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(CaptchaTokenRepository $captchaTokenRepository): bool
{
if (!$this->hasHeader('public-token')) {
return false;
}
$captchaToken = $captchaTokenRepository->getCaptchaTokenByPublicToken($this->header('public-token'));
if (is_null($captchaToken)) {
return false;
}
$this->captchaToken = $captchaToken;
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'captcha_key' => ['required', 'string', 'max:75'],
'verification' => ['required', 'array'],
'verification.*' => ['required', 'array', 'size:2'],
'verification.*.x' => ['required', 'numeric', 'min:0'],
'verification.*.y' => ['required', 'numeric', 'min:0'],
];
}
public function getDto(): CheckingDto
{
$httpUserData = new HttpUserData(
$this->getClientIp(),
$this->userAgent(),
$this->header('referer')
);
$captchaPublicToken = new CaptchaPublicToken(
$this->captchaToken,
$httpUserData
);
return new CheckingDto(
captchaPublicToken: $captchaPublicToken,
captchaKey: $this->input('captcha_key'),
coordinators: $this->input('verification'),
);
}
}

View File

@ -0,0 +1,58 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Api\V1\Captcha;
use App\Contracts\FormRequestDto;
use App\Dto\HttpUserData;
use App\Dto\Request\Api\V1\Captcha\VerificationInformationDto;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use Illuminate\Foundation\Http\FormRequest;
final class VerificationInformationRequest extends FormRequest implements FormRequestDto
{
private readonly CaptchaToken $captchaToken;
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(CaptchaTokenRepository $captchaTokenRepository): bool
{
if (!$this->hasHeader('private-token')) {
return false;
}
$captchaToken = $captchaTokenRepository->getCaptchaTokenByPrivateToken($this->header('private-token'));
if (is_null($captchaToken)) {
return false;
}
$this->captchaToken = $captchaToken;
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'user_agent' => ['nullable', 'string'],
];
}
public function getDto(): VerificationInformationDto
{
$httpUserData = new HttpUserData(
$this->getClientIp(),
$this->userAgent(),
$this->header('referer')
);
return new VerificationInformationDto(
captchaToken: $this->captchaToken,
httpUserData: $httpUserData,
userAgent: $this->input('user_agent', null),
);
}
}

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,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\CaptchaTokens;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\CaptchaToken;
use App\Dto\Request\Private\CaptchaToken\Index;
use Illuminate\Foundation\Http\FormRequest;
final class IndexRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$this->redirect = route('users.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
captchaTokenDto: new CaptchaToken(),
page: (int) $this->input('page', 1)
);
}
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\CaptchaTokens;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\CaptchaToken\StoreUpdate;
use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => ['required', 'max:255'],
];
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
title: $this->input('title'),
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
final class UpdatePasswordRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => ['required', 'confirmed', Password::default()],
];
}
public function getDto(): UpdatePassword
{
return new UpdatePassword(password: $this->input('password'));
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\Profile\Update;
use Illuminate\Foundation\Http\FormRequest;
final class UpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'max:255'],
];
}
public function getDto(): Update
{
return new Update(name: $this->input('name'));
}
}

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\Profile\UpdateSettings;
use App\Enums\Lang;
use App\Helpers\Helpers;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
final class UpdateSettingsRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'lang' => ['nullable', new Enum(Lang::class)],
'timezone' => ['nullable', Rule::in(Helpers::getTimeZoneList()->keys()->toArray())]
];
}
public function getDto(): UpdateSettings
{
$lang = $this->input('lang', null);
if (!is_null($lang)) {
$lang = Lang::from((int) $lang);
}
return new UpdateSettings(
lang: $lang,
timezone: $this->input('timezone', null),
);
}
}

Some files were not shown because too many files have changed in this diff Show More