diff --git a/.env.example b/.env.example index bbe856a..3189c76 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ DOCKER_NGINX_PORT=8080 +DOCKER_WEBSOCKET_PORT=8081 DOCKER_DB_PORT=3306 MYSQL_ROOT_PASSWORD=root_pass DB_DATABASE=captcha diff --git a/app/application/.env.example b/app/application/.env.example index 78bdd22..278c1d2 100644 --- a/app/application/.env.example +++ b/app/application/.env.example @@ -72,3 +72,21 @@ VITE_PUSHER_HOST="${PUSHER_HOST}" VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +REVERB_APP_ID= +REVERB_APP_KEY= +REVERB_APP_SECRET= +REVERB_HOST="reverb" +REVERB_PORT=9000 +REVERB_SCHEME=http +# * or localhost.com or localhost.com, localhost.net +REVERB_ALLOWED_ORIGINS="*" + +REVERB_HOST_CLIENT="localhost" +REVERB_PORT_CLIENT=8081 +REVERB_SCHEME_CLIENT=http + +VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" +VITE_REVERB_HOST="${REVERB_HOST}" +VITE_REVERB_PORT="${REVERB_PORT}" +VITE_REVERB_SCHEME="${REVERB_SCHEME}" diff --git a/app/application/app/Broadcasting/CreatedCaptchaLog.php b/app/application/app/Broadcasting/CreatedCaptchaLog.php new file mode 100644 index 0000000..fa8f7ec --- /dev/null +++ b/app/application/app/Broadcasting/CreatedCaptchaLog.php @@ -0,0 +1,17 @@ +can('viewAny', CaptchaToken::class); + } +} diff --git a/app/application/app/Dto/Repository/CaptchaLogRepository/QuantityByDays.php b/app/application/app/Dto/Repository/CaptchaLogRepository/QuantityByDays.php new file mode 100644 index 0000000..27fd89a --- /dev/null +++ b/app/application/app/Dto/Repository/CaptchaLogRepository/QuantityByDays.php @@ -0,0 +1,38 @@ +days[$day])) { + $this->days[$day] = []; + } + $this->days[$day][$type->value] = $count; + } + + /** + * @param string $day ("Y-m-d") + * @return array + */ + public function getDay(string $day): array + { + return $this->days[$day] ?? []; + } + + public function getDays(): array + { + return $this->days; + } +} diff --git a/app/application/app/Enums/CaptchaLogType.php b/app/application/app/Enums/CaptchaLogType.php index 8d9147d..3e4c678 100644 --- a/app/application/app/Enums/CaptchaLogType.php +++ b/app/application/app/Enums/CaptchaLogType.php @@ -8,4 +8,9 @@ enum CaptchaLogType: int case Error = 2; case Verified = 3; case ReadVerified = 4; + + public function getTitle(): string + { + return __('captcha_log_type.' . $this->name); + } } diff --git a/app/application/app/Events/CreatedCaptchaLog.php b/app/application/app/Events/CreatedCaptchaLog.php new file mode 100644 index 0000000..3a99380 --- /dev/null +++ b/app/application/app/Events/CreatedCaptchaLog.php @@ -0,0 +1,47 @@ +chartCaptchaActivity = [ + 'date' => $captchaLog->created_at->format('Y-m-d'), + 'meta' => $captchaLog->type->getTitle(), + 'type' => $captchaLog->type->value, + ]; + $link = ''; + $title = ''; + if ($captchaLog->captcha && $captchaLog->captcha->captchaToken) { + $link = route('captcha-tokens.edit', ['captcha_token' => $captchaLog->captcha->captcha_token_id], false); + $title = $captchaLog->captcha->captchaToken->title; + } + $this->captchaLog = [ + 'created_at' => $captchaLog->created_at->format("d.m.Y H:i:s"), + 'link' => $link, + 'title' => $title, + 'type' => $captchaLog->type->getTitle(), + 'ip' => $captchaLog->ip, + 'user_agent' => $captchaLog->user_agent, + 'referer' => $captchaLog->referer, + ]; + } + + public function broadcastOn(): Channel + { + return new PrivateChannel('chart-captcha-activity'); + } +} diff --git a/app/application/app/Http/Controllers/Private/DashboardController.php b/app/application/app/Http/Controllers/Private/DashboardController.php index 8d1cade..508a25c 100644 --- a/app/application/app/Http/Controllers/Private/DashboardController.php +++ b/app/application/app/Http/Controllers/Private/DashboardController.php @@ -2,12 +2,42 @@ namespace App\Http\Controllers\Private; +use App\Http\Resources\Private\Dashboard\ChartCaptchaActivity; +use App\Services\Private\DashboardService; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Illuminate\View\View; final class DashboardController extends Controller { - public function index(): View + public function __construct( + private readonly DashboardService $dashboardService, + ) { } + + public function index(Request $request): View { - return view('private/dashboard/index'); + $user = $request->user(); + $limit = 30; + $with = ['captcha', 'captcha.captchaToken']; + $result = $this->dashboardService->captchaLog($user, $limit, $with); + if ($result->isError()) { + $this->errors($result); + } + + return view('private/dashboard/index', $result->getData()); + } + + public function chartCaptchaActivity(Request $request): JsonResponse + { + $user = $request->user(); + $from = Carbon::now()->subDays(7); + $to = Carbon::now(); + $result = $this->dashboardService->chartCaptchaActivity($user, $from, $to); + if (!$result->isSuccess()) { + return response()->json($result->getData())->setStatusCode($result->getCode()); + } + + return response()->json(new ChartCaptchaActivity($result)); } } diff --git a/app/application/app/Http/Controllers/Private/WebsocketsController.php b/app/application/app/Http/Controllers/Private/WebsocketsController.php new file mode 100644 index 0000000..d49e825 --- /dev/null +++ b/app/application/app/Http/Controllers/Private/WebsocketsController.php @@ -0,0 +1,24 @@ +websocketService->settings(); + if (!$result->isSuccess()) { + return response()->json($result->getData())->setStatusCode($result->getCode()); + } + + return response()->json(new Setting($result)); + } +} diff --git a/app/application/app/Http/Resources/Private/Dashboard/ChartCaptchaActivity.php b/app/application/app/Http/Resources/Private/Dashboard/ChartCaptchaActivity.php new file mode 100644 index 0000000..b4f6969 --- /dev/null +++ b/app/application/app/Http/Resources/Private/Dashboard/ChartCaptchaActivity.php @@ -0,0 +1,53 @@ +resource->getFrom()->diff($this->resource->getTo())->stepBy('day'); + $days = []; + $values = []; + $types = []; + foreach (CaptchaLogType::cases() as $type) { + $values[$type->value] = []; + $types[$type->value] = [ + 'meta' => $type->getTitle(), + 'value' => 0, + ]; + } + foreach ($period as $item) { + $day = $item->format('Y-m-d'); + $days[] = $day; + $quantity = $this->resource->getQuantityByDays()->getDay($day); + foreach (CaptchaLogType::cases() as $type) { + $values[$type->value][] = [ + 'meta' => $type->getTitle(), + 'value' => $quantity[$type->value] ?? 0, + ]; + } + } + + return [ + 'days' => $days, + 'values' => $values, + 'types' => $types, + ]; + } +} diff --git a/app/application/app/Http/Resources/Private/Websockets/Setting.php b/app/application/app/Http/Resources/Private/Websockets/Setting.php new file mode 100644 index 0000000..1050164 --- /dev/null +++ b/app/application/app/Http/Resources/Private/Websockets/Setting.php @@ -0,0 +1,31 @@ + $this->resource->getKey(), + 'wsHost' => $this->resource->getWsHost(), + 'wsPort' => $this->resource->getWsPort(), + 'wssPort' => $this->resource->getWssPort(), + 'forceTLS' => $this->resource->isForceTLS(), + ]; + } +} diff --git a/app/application/app/Models/Captcha.php b/app/application/app/Models/Captcha.php index 37ed7a9..f173e4b 100644 --- a/app/application/app/Models/Captcha.php +++ b/app/application/app/Models/Captcha.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; final class Captcha extends Model @@ -34,4 +35,9 @@ public function captchaLogs(): HasMany { return $this->hasMany(CaptchaLog::class); } + + public function captchaToken(): BelongsTo + { + return $this->belongsTo(CaptchaToken::class); + } } diff --git a/app/application/app/Models/CaptchaLog.php b/app/application/app/Models/CaptchaLog.php index ce5df82..86cd5bb 100644 --- a/app/application/app/Models/CaptchaLog.php +++ b/app/application/app/Models/CaptchaLog.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; final class CaptchaLog extends Model { @@ -44,4 +45,9 @@ public function scopeLatest(Builder $query): Builder { return $query->orderBy('created_at', 'desc'); } + + public function captcha(): BelongsTo + { + return $this->belongsTo(Captcha::class); + } } diff --git a/app/application/app/Repositories/CaptchaLogRepository.php b/app/application/app/Repositories/CaptchaLogRepository.php index a778315..cc5d03e 100644 --- a/app/application/app/Repositories/CaptchaLogRepository.php +++ b/app/application/app/Repositories/CaptchaLogRepository.php @@ -2,13 +2,30 @@ namespace App\Repositories; +use App\Dto\Repository\CaptchaLogRepository\QuantityByDays; use App\Enums\CaptchaLogType; use App\Models\CaptchaLog; +use App\Services\Search\CreateSearchInstanceCommand; +use App\Services\Search\Search; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; final class CaptchaLogRepository { + public function __construct( + private CreateSearchInstanceCommand $createSearchInstanceCommand, + ) { } + + public function getCaptchaLogs(array $with = []): Search + { + $query = CaptchaLog::query() + ->with($with) + ->latest(); + + return $this->createSearchInstanceCommand->execute($query); + } + public function countByType(CaptchaLogType $type, ?int $captchaId = null): int { return CaptchaLog::query() @@ -32,4 +49,22 @@ public function getCaptchaLogsByTypes(array $types, ?int $captchaId = null, ?int ->latest() ->get(); } + + public function countByDays(Carbon $from, Carbon $to): QuantityByDays + { + $count = CaptchaLog::query() + ->selectRaw('DATE_FORMAT(created_at, \'%Y-%m-%d\') AS date, type, COUNT(id) AS count') + ->where('created_at', '>=', $from) + ->where('created_at', '<=', $to) + ->groupBy('date', 'type') + ->orderBy('date', 'asc') + ->get(); + + $quantity = new QuantityByDays(); + foreach ($count as $item) { + $quantity->add($item->date, $item->type, $item->count); + } + + return $quantity; + } } diff --git a/app/application/app/ServiceResults/Private/Dashboard/ChartCaptchaActivity.php b/app/application/app/ServiceResults/Private/Dashboard/ChartCaptchaActivity.php new file mode 100644 index 0000000..6015d57 --- /dev/null +++ b/app/application/app/ServiceResults/Private/Dashboard/ChartCaptchaActivity.php @@ -0,0 +1,31 @@ +from; + } + + public function getTo(): Carbon + { + return $this->to; + } + + public function getQuantityByDays(): QuantityByDays + { + return $this->quantityByDays; + } +} diff --git a/app/application/app/ServiceResults/Private/Websocket/Setting.php b/app/application/app/ServiceResults/Private/Websocket/Setting.php new file mode 100644 index 0000000..bb21eea --- /dev/null +++ b/app/application/app/ServiceResults/Private/Websocket/Setting.php @@ -0,0 +1,41 @@ +key; + } + + public function getWsHost(): string + { + return $this->wsHost; + } + + public function getWsPort(): int + { + return $this->wsPort; + } + + public function getWssPort(): int + { + return $this->wssPort; + } + + public function isForceTLS(): bool + { + return $this->forceTLS; + } +} diff --git a/app/application/app/Services/CaptchaLog/CaptchaLogHandler.php b/app/application/app/Services/CaptchaLog/CaptchaLogHandler.php index bba2f51..871cc7a 100644 --- a/app/application/app/Services/CaptchaLog/CaptchaLogHandler.php +++ b/app/application/app/Services/CaptchaLog/CaptchaLogHandler.php @@ -4,6 +4,7 @@ use App\Dto\HttpUserData; use App\Enums\CaptchaLogType; +use App\Events\CreatedCaptchaLog; use App\Models\Captcha; use App\Models\CaptchaLog; use Illuminate\Support\Str; @@ -22,12 +23,16 @@ public function handleStore(int $captchaId, CaptchaLogType $captchaLogType, Http $referer = Str::limit($referer, 10000, ''); } - return CaptchaLog::create([ + $captchaLog = CaptchaLog::create([ 'captcha_id' => $captchaId, 'type' => $captchaLogType, 'ip' => $httpUserData->getClientIp(), 'user_agent' => $userAgent, 'referer' => $referer, ]); + + CreatedCaptchaLog::dispatch($captchaLog); + + return $captchaLog; } } diff --git a/app/application/app/Services/Private/DashboardService.php b/app/application/app/Services/Private/DashboardService.php new file mode 100644 index 0000000..d43ce26 --- /dev/null +++ b/app/application/app/Services/Private/DashboardService.php @@ -0,0 +1,54 @@ +cannot('viewAny', CaptchaToken::class)) { + return $this->result([ + 'items' => collect([]), + 'user' => $user, + ]); + } + + return $this->result([ + 'items' => $this->captchaLogRepository->getCaptchaLogs($with)->get($limit), + 'user' => $user, + ]); + } + + public function chartCaptchaActivity(User $user, Carbon $from, Carbon $to): ServiceResultError | ChartCaptchaActivity + { + if ($user->cannot('viewAny', CaptchaToken::class)) { + return $this->errFobidden(__('Access is denied')); + } + + if ($to < $from) { + return $this->errValidate(__('From cannot be greater than To')); + } + + $quantity = $this->captchaLogRepository->countByDays($from, $to); + + return new ChartCaptchaActivity( + from: $from, + to: $to, + quantityByDays: $quantity, + ); + } +} diff --git a/app/application/app/Services/Private/WebsocketService.php b/app/application/app/Services/Private/WebsocketService.php new file mode 100644 index 0000000..e82fea5 --- /dev/null +++ b/app/application/app/Services/Private/WebsocketService.php @@ -0,0 +1,24 @@ +=5.3" + }, + "type": "library", + "autoload": { + "psr-0": { + "Clue\\Redis\\Protocol": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "A streaming redis wire protocol parser and serializer implementation in PHP", + "homepage": "https://github.com/clue/php-redis-protocol", + "keywords": [ + "parser", + "protocol", + "redis", + "serializer", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/php-redis-protocol/issues", + "source": "https://github.com/clue/php-redis-protocol/tree/v0.3.1" + }, + "time": "2017-06-06T16:07:10+00:00" + }, + { + "name": "clue/redis-react", + "version": "v2.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-redis.git", + "reference": "2283690f249e8d93342dd63b5285732d2654e077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-redis/zipball/2283690f249e8d93342dd63b5285732d2654e077", + "reference": "2283690f249e8d93342dd63b5285732d2654e077", + "shasum": "" + }, + "require": { + "clue/redis-protocol": "0.3.*", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3 || ^2.0 || ^1.1", + "react/promise-timer": "^1.9", + "react/socket": "^1.12" + }, + "require-dev": { + "clue/block-react": "^1.5", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Redis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Async Redis client implementation, built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-redis", + "keywords": [ + "async", + "client", + "database", + "reactphp", + "redis" + ], + "support": { + "issues": "https://github.com/clue/reactphp-redis/issues", + "source": "https://github.com/clue/reactphp-redis/tree/v2.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2024-01-05T15:54:20+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -501,6 +617,53 @@ ], "time": "2023-10-06T06:47:41+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -1357,6 +1520,88 @@ }, "time": "2024-04-18T00:45:25+00:00" }, + { + "name": "laravel/reverb", + "version": "v1.0.0-beta9", + "source": { + "type": "git", + "url": "https://github.com/laravel/reverb.git", + "reference": "46d768c88755ba17224c3f3dcdddf24b0e4fe4f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/reverb/zipball/46d768c88755ba17224c3f3dcdddf24b0e4fe4f8", + "reference": "46d768c88755ba17224c3f3dcdddf24b0e4fe4f8", + "shasum": "" + }, + "require": { + "clue/redis-react": "^2.6", + "guzzlehttp/psr7": "^2.6", + "illuminate/console": "^10.47|^11.0", + "illuminate/contracts": "^10.47|^11.0", + "illuminate/http": "^10.47|^11.0", + "illuminate/support": "^10.47|^11.0", + "laravel/prompts": "^0.1.15", + "php": "^8.2", + "pusher/pusher-php-server": "^7.2", + "ratchet/rfc6455": "^0.3.1", + "react/promise-timer": "^1.10", + "react/socket": "^1.14", + "symfony/console": "^6.0|^7.0", + "symfony/http-foundation": "^6.3|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "phpstan/phpstan": "^1.10", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.0", + "react/http": "^1.9" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Reverb\\ApplicationManagerServiceProvider", + "Laravel\\Reverb\\ReverbServiceProvider" + ], + "aliases": { + "Output": "Laravel\\Reverb\\Output" + } + } + }, + "autoload": { + "psr-4": { + "Laravel\\Reverb\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Joe Dixon", + "email": "joe@laravel.com" + } + ], + "description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.", + "keywords": [ + "WebSockets", + "laravel", + "real-time", + "websocket" + ], + "support": { + "issues": "https://github.com/laravel/reverb/issues", + "source": "https://github.com/laravel/reverb/tree/v1.0.0-beta9" + }, + "time": "2024-04-25T07:20:24+00:00" + }, { "name": "laravel/sanctum", "version": "v4.0.2", @@ -2441,6 +2686,142 @@ ], "time": "2024-03-06T16:17:14+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v1.21.1", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/bb312875dcdd20680419564fe42ba1d9564b9e37", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37", + "shasum": "" + }, + "require": { + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v1.21.1" + }, + "time": "2024-04-22T22:05:04+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.2", @@ -3007,6 +3388,67 @@ }, "time": "2024-04-02T15:57:53+00:00" }, + { + "name": "pusher/pusher-php-server", + "version": "7.2.4", + "source": { + "type": "git", + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "de2f72296808f9cafa6a4462b15a768ff130cddb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/de2f72296808f9cafa6a4462b15a768ff130cddb", + "reference": "de2f72296808f9cafa6a4462b15a768ff130cddb", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6", + "php": "^7.3|^8.0", + "psr/log": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "overtrue/phplint": "^2.3", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "Pusher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Library for interacting with the Pusher REST API", + "keywords": [ + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" + ], + "support": { + "issues": "https://github.com/pusher/pusher-http-php/issues", + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.4" + }, + "time": "2023-12-15T10:58:53+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -3232,6 +3674,593 @@ ], "time": "2023-11-08T05:53:05+00:00" }, + { + "name": "ratchet/rfc6455", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2 || ^1.7", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1" + }, + "time": "2021-12-09T23:20:49+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "c134600642fa615b46b41237ef243daa65bb64ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/c134600642fa615b46b41237ef243daa65bb64ec", + "reference": "c134600642fa615b46b41237ef243daa65bb64ec", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.0 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4 || ^3 || ^2", + "react/promise-timer": "^1.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.12.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-29T12:41:06+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-16T16:21:57+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "4cb85c1c2125390748e3908120bb82feb99fe766" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4cb85c1c2125390748e3908120bb82feb99fe766", + "reference": "4cb85c1c2125390748e3908120bb82feb99fe766", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.10.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-07-20T15:40:28+00:00" + }, + { + "name": "react/socket", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.11", + "react/event-loop": "^1.2", + "react/promise": "^3 || ^2.6 || ^1.2.1", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.15.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-12-15T11:02:10+00:00" + }, + { + "name": "react/stream", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/6fbc9672905c7d5a885f2da2fc696f65840f4a66", + "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-06-16T10:52:11+00:00" + }, { "name": "symfony/clock", "version": "v7.0.5", @@ -9069,7 +10098,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "laravel/reverb": 10 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/app/application/config/app.php b/app/application/config/app.php index c9dcca0..4d51dad 100644 --- a/app/application/config/app.php +++ b/app/application/config/app.php @@ -215,7 +215,7 @@ */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, - // App\Providers\BroadcastServiceProvider::class, + App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, diff --git a/app/application/config/reverb.php b/app/application/config/reverb.php new file mode 100644 index 0000000..bb00f80 --- /dev/null +++ b/app/application/config/reverb.php @@ -0,0 +1,97 @@ + env('REVERB_SERVER', 'reverb'), + + /* + |-------------------------------------------------------------------------- + | Reverb Servers + |-------------------------------------------------------------------------- + | + | Here you may define details for each of the supported Reverb servers. + | Each server has its own configuration options that are defined in + | the array below. You should ensure all the options are present. + | + */ + + 'servers' => [ + + 'reverb' => [ + 'host' => env('REVERB_SERVER_HOST', '0.0.0.0'), + 'port' => env('REVERB_SERVER_PORT', 8080), + 'hostname' => env('REVERB_HOST'), + 'options' => [ + 'tls' => [], + ], + 'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000), + 'scaling' => [ + 'enabled' => env('REVERB_SCALING_ENABLED', false), + 'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'), + 'server' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'port' => env('REDIS_PORT', '6379'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'database' => env('REDIS_DB', '0'), + ], + ], + 'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15), + 'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Reverb Applications + |-------------------------------------------------------------------------- + | + | Here you may define how Reverb applications are managed. If you choose + | to use the "config" provider, you may define an array of apps which + | your server will support, including their connection credentials. + | + */ + + 'apps' => [ + + 'provider' => 'config', + + 'apps' => [ + [ + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'allowed_origins' => explode(',', env('REVERB_ALLOWED_ORIGINS', '*')), + 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60), + 'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000), + 'options_for_client' => [ + 'host' => env('REVERB_HOST_CLIENT'), + 'port' => env('REVERB_PORT_CLIENT', 443), + 'scheme' => env('REVERB_SCHEME_CLIENT', 'https'), + 'useTLS' => env('REVERB_SCHEME_CLIENT', 'https') === 'https', + ], + ], + ], + + ], + +]; diff --git a/app/application/lang/en.json b/app/application/lang/en.json index b1013aa..f90d423 100644 --- a/app/application/lang/en.json +++ b/app/application/lang/en.json @@ -73,5 +73,6 @@ "The time for captcha verification has passed": "The time for captcha verification has passed.", "Captcha does not pass verification": "Captcha does not pass verification.", "Add code to the site": "Add code to the site", - "Demo Mode": "!!! Demo Mode !!!" + "Demo Mode": "!!! Demo Mode !!!", + "From cannot be greater than To": "From cannot be greater than To." } diff --git a/app/application/lang/en/captcha_log_type.php b/app/application/lang/en/captcha_log_type.php new file mode 100644 index 0000000..81d7e20 --- /dev/null +++ b/app/application/lang/en/captcha_log_type.php @@ -0,0 +1,7 @@ + 'Creating a captcha', + 'Error' => 'Error in captcha validation', + 'Verified' => 'Successfully verified', + 'ReadVerified' => 'Completed captcha verification', +]; diff --git a/app/application/lang/ru.json b/app/application/lang/ru.json index 04d4e89..b4be525 100644 --- a/app/application/lang/ru.json +++ b/app/application/lang/ru.json @@ -73,5 +73,6 @@ "The time for captcha verification has passed": "Время верификации капчи прошло.", "Captcha does not pass verification": "Капча не проходит проверку.", "Add code to the site": "Добавьте код на сайт", - "Demo Mode": "!!! Включён демо режим !!!" + "Demo Mode": "!!! Включён демо режим !!!", + "From cannot be greater than To": "From не может быть больше To." } diff --git a/app/application/lang/ru/captcha_log_type.php b/app/application/lang/ru/captcha_log_type.php new file mode 100644 index 0000000..c8718ae --- /dev/null +++ b/app/application/lang/ru/captcha_log_type.php @@ -0,0 +1,7 @@ + 'Создание капчи', + 'Error' => 'Ошибка в валидации капчи', + 'Verified' => 'Успешно проверено', + 'ReadVerified' => 'Завершена проверка капчи', +]; diff --git a/app/application/package-lock.json b/app/application/package-lock.json index 02fc257..7ebded0 100644 --- a/app/application/package-lock.json +++ b/app/application/package-lock.json @@ -21,7 +21,9 @@ }, "devDependencies": { "axios": "^1.1.2", + "laravel-echo": "^1.16.1", "laravel-vite-plugin": "^0.7.2", + "pusher-js": "^8.4.0-rc2", "sass-loader": "^13.3.2", "vite": "^4.0.0" } @@ -1251,6 +1253,15 @@ "dev": true, "peer": true }, + "node_modules/laravel-echo": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.16.1.tgz", + "integrity": "sha512-++Ylb6M3ariC9Rk5WE5gZjj6wcEV5kvLF8b+geJ5/rRIfdoOA+eG6b9qJPrarMD9rY28Apx+l3eelIrCc2skVg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/laravel-vite-plugin": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz", @@ -1436,6 +1447,15 @@ "node": ">=6" } }, + "node_modules/pusher-js": { + "version": "8.4.0-rc2", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0-rc2.tgz", + "integrity": "sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g==", + "dev": true, + "dependencies": { + "tweetnacl": "^1.0.3" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1723,6 +1743,12 @@ "node": ">=8.0" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -2755,6 +2781,12 @@ "dev": true, "peer": true }, + "laravel-echo": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.16.1.tgz", + "integrity": "sha512-++Ylb6M3ariC9Rk5WE5gZjj6wcEV5kvLF8b+geJ5/rRIfdoOA+eG6b9qJPrarMD9rY28Apx+l3eelIrCc2skVg==", + "dev": true + }, "laravel-vite-plugin": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz", @@ -2883,6 +2915,15 @@ "dev": true, "peer": true }, + "pusher-js": { + "version": "8.4.0-rc2", + "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0-rc2.tgz", + "integrity": "sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g==", + "dev": true, + "requires": { + "tweetnacl": "^1.0.3" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3056,6 +3097,12 @@ "is-number": "^7.0.0" } }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true + }, "update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", diff --git a/app/application/package.json b/app/application/package.json index 612ecc5..c446ebb 100644 --- a/app/application/package.json +++ b/app/application/package.json @@ -21,7 +21,9 @@ }, "devDependencies": { "axios": "^1.1.2", + "laravel-echo": "^1.16.1", "laravel-vite-plugin": "^0.7.2", + "pusher-js": "^8.4.0-rc2", "sass-loader": "^13.3.2", "vite": "^4.0.0" } diff --git a/app/application/resources/views/private/dashboard/index.blade.php b/app/application/resources/views/private/dashboard/index.blade.php index 057ac8b..7624168 100644 --- a/app/application/resources/views/private/dashboard/index.blade.php +++ b/app/application/resources/views/private/dashboard/index.blade.php @@ -1,5 +1,40 @@ @section('meta_title', __('sections.Dashboard')) @section('h1', __('sections.Dashboard')) - +
+
+ @can('viewAny', \App\Models\CaptchaToken::class) +
+
+ + + + + + + + + + @foreach($items as $item) + + + + + + @endforeach + +
{{ __('validation.attributes.date') }}Capctha
{{ $item->created_at->timezone(\App\Helpers\Helpers::getUserTimeZone())->format("d.m.Y H:i:s") }} + @if($item->captcha && $item->captcha->captchaToken) + {{ $item->captcha->captchaToken->title }} + @endif +

{{ $item->type->getTitle() }}

+
+

IP: {{ $item->ip }}

+

User Agent: {{ $item->user_agent }}

+

referer: {{ $item->referer }}

+
+
+ @endcan +
+
diff --git a/app/application/resources/volt/js/app.js b/app/application/resources/volt/js/app.js index daaea29..6e6246c 100644 --- a/app/application/resources/volt/js/app.js +++ b/app/application/resources/volt/js/app.js @@ -2,4 +2,6 @@ import.meta.glob([ '../images/**', ]); import './bootstrap'; -import './volt.js'; +import './echo'; +import './volt'; +import './dashboard' diff --git a/app/application/resources/volt/js/dashboard.js b/app/application/resources/volt/js/dashboard.js new file mode 100644 index 0000000..1bede54 --- /dev/null +++ b/app/application/resources/volt/js/dashboard.js @@ -0,0 +1,80 @@ +"use strict"; + +import Chartist from "chartist"; +import 'chartist-plugin-tooltips'; + +if(document.querySelector('#chart-сaptcha-activity')) { + let chartCaptchaActivity = null; + axios.get('/dashboard/chart-captcha-activity') + .then(response => { + chartCaptchaActivity = new Chartist.Line('#chart-сaptcha-activity', { + labels: response.data.days, + series: response.data.values, + }, { + low: 0, + showArea: true, + fullWidth: true, + plugins: [ + Chartist.plugins.tooltip() + ], + axisX: { + // On the x-axis start means top and end means bottom + position: 'end', + showGrid: true, + }, + axisY: { + // On the y-axis start means left and end means right + showGrid: true, + showLabel: true, + } + }); + + let currentDate = response.data.days[response.data.days.length - 1]; + + window.websocket.add(function (echo) { + echo.private('chart-captcha-activity') + .listen('CreatedCaptchaLog', (e) => { + let options = chartCaptchaActivity.data; + + if (currentDate !== e.chartCaptchaActivity.date) { + currentDate = e.chartCaptchaActivity.date; + + options['labels'].push(currentDate); + options['series'].forEach(function(series, i) { + series.push(response.data.types[i]); + options['series'].push(series); + }); + } + let values = options['series'][e.chartCaptchaActivity['type'] - 1]; + options['series'][e.chartCaptchaActivity['type'] - 1][values.length - 1]['value']++; + chartCaptchaActivity.update(options); + + let captchaLog = document.querySelector('#captcha-log'); + if (captchaLog) { + let tr = document.createElement("tr"); + let link = ''; + if (e.captchaLog.link !== '' && e.captchaLog.title !== '') { + link = `${e.captchaLog.title}`; + } + tr.innerHTML = ` + ${e.captchaLog.created_at} + + ${link} +

${e.captchaLog.type}

+ + +

IP: ${e.captchaLog.ip}

+

User Agent: ${e.captchaLog.user_agent}

+

referer: ${e.captchaLog.referer}

+ + `; + + captchaLog.prepend(tr); + } + }); + }); + }) + .catch(error => { + + }); +} diff --git a/app/application/resources/volt/js/echo.js b/app/application/resources/volt/js/echo.js new file mode 100644 index 0000000..1ea3e4a --- /dev/null +++ b/app/application/resources/volt/js/echo.js @@ -0,0 +1,49 @@ +"use strict"; + +import Echo from 'laravel-echo'; +import Pusher from 'pusher-js'; + +class websocket { + _echo = null; + pusher = Pusher; + + callbacks = [] + + constructor() { + let _this = this; + axios.get('/websockets/settings') + .then(response => { + _this._echo = new Echo({ + broadcaster: 'reverb', + key: response.data.key, + wsHost: response.data.wsHost, + wsPort: response.data.wsPort, + wssPort: response.data.wssPort, + forceTLS: response.data.forceTLS, + enabledTransports: ['ws', 'wss'], + }); + _this.startCallbacks(); + }) + .catch(error => { + + }); + } + + add(callback) { + if (this._echo !== null) { + callback(this._echo) + return ; + } + + this.callbacks.push(callback); + } + + startCallbacks() { + let _this = this; + this.callbacks.forEach(function(callback) { + callback(_this._echo) + }); + } +} + +window.websocket = new websocket(); diff --git a/app/application/resources/volt/js/volt.js b/app/application/resources/volt/js/volt.js index 6a9ba93..483167e 100644 --- a/app/application/resources/volt/js/volt.js +++ b/app/application/resources/volt/js/volt.js @@ -21,8 +21,6 @@ const d = document; import * as bootstrap from 'bootstrap'; import Swal from 'sweetalert2'; import SmoothScroll from 'smooth-scroll'; -import Chartist from 'chartist'; -import 'chartist-plugin-tooltips'; d.addEventListener("DOMContentLoaded", function(event) { @@ -176,102 +174,6 @@ d.addEventListener("DOMContentLoaded", function(event) { }); } - //Chartist - - if(d.querySelector('.ct-chart-sales-value')) { - //Chart 5 - new Chartist.Line('.ct-chart-sales-value', { - labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - series: [ - [0, 10, 30, 40, 80, 60, 100] - ] - }, { - low: 0, - showArea: true, - fullWidth: true, - plugins: [ - Chartist.plugins.tooltip() - ], - axisX: { - // On the x-axis start means top and end means bottom - position: 'end', - showGrid: true - }, - axisY: { - // On the y-axis start means left and end means right - showGrid: false, - showLabel: false, - labelInterpolationFnc: function(value) { - return '$' + (value / 1) + 'k'; - } - } - }); - } - - if(d.querySelector('.ct-chart-ranking')) { - var chart = new Chartist.Bar('.ct-chart-ranking', { - labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], - series: [ - [1, 5, 2, 5, 4, 3], - [2, 3, 4, 8, 1, 2], - ] - }, { - low: 0, - showArea: true, - plugins: [ - Chartist.plugins.tooltip() - ], - axisX: { - // On the x-axis start means top and end means bottom - position: 'end' - }, - axisY: { - // On the y-axis start means left and end means right - showGrid: false, - showLabel: false, - offset: 0 - } - }); - - chart.on('draw', function(data) { - if(data.type === 'line' || data.type === 'area') { - data.element.animate({ - d: { - begin: 2000 * data.index, - dur: 2000, - from: data.path.clone().scale(1, 0).translate(0, data.chartRect.height()).stringify(), - to: data.path.clone().stringify(), - easing: Chartist.Svg.Easing.easeOutQuint - } - }); - } - }); - } - - if(d.querySelector('.ct-chart-traffic-share')) { - var data = { - series: [70, 20, 10] - }; - - var sum = function(a, b) { return a + b }; - - new Chartist.Pie('.ct-chart-traffic-share', data, { - labelInterpolationFnc: function(value) { - return Math.round(value / data.series.reduce(sum) * 100) + '%'; - }, - low: 0, - high: 8, - donut: true, - donutWidth: 20, - donutSolid: true, - fullWidth: false, - showLabel: false, - plugins: [ - Chartist.plugins.tooltip() - ], - }); - } - if (d.getElementById('loadOnClick')) { d.getElementById('loadOnClick').addEventListener('click', function () { var button = this; diff --git a/app/application/routes/channels.php b/app/application/routes/channels.php index 409c335..f76d0ab 100644 --- a/app/application/routes/channels.php +++ b/app/application/routes/channels.php @@ -12,3 +12,5 @@ | used to check if an authenticated user can listen to the channel. | */ + +Broadcast::channel('chart-captcha-activity', \App\Broadcasting\CreatedCaptchaLog::class); diff --git a/app/application/routes/web.php b/app/application/routes/web.php index a2afbc3..3a7c842 100644 --- a/app/application/routes/web.php +++ b/app/application/routes/web.php @@ -20,6 +20,7 @@ Route::middleware(['auth', 'verified', 'user.locale'])->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::get('/dashboard/chart-captcha-activity', [\App\Http\Controllers\Private\DashboardController::class, 'chartCaptchaActivity']); Route::prefix('profile')->as('profile.') ->group(function () { Route::get('/', [\App\Http\Controllers\Private\ProfileController::class, 'profile'])->name('edit'); @@ -34,4 +35,6 @@ Route::resource('roles', \App\Http\Controllers\Private\RolesController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['role' => '[0-9]+']); Route::resource('captcha-tokens', \App\Http\Controllers\Private\CaptchaTokensController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['captcha_token' => '[0-9]+']); + + Route::get('websockets/settings', [\App\Http\Controllers\Private\WebsocketsController::class, 'settings']); }); diff --git a/app/docker/Dockerfile b/app/docker/Dockerfile index e0c8fc1..72772c5 100644 --- a/app/docker/Dockerfile +++ b/app/docker/Dockerfile @@ -73,7 +73,8 @@ STOPSIGNAL SIGTERM ENTRYPOINT ["/home/unit/docker-entrypoint.sh"] EXPOSE 9000 -CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock", "--user", "unit", "--group", "unit"] +COPY docker/start_prod.sh /usr/local/bin/start +CMD ["/usr/local/bin/start"] FROM BUILD AS DEVELOP @@ -91,9 +92,9 @@ RUN chmod 755 /home/unit/docker-entrypoint.sh \ ENTRYPOINT ["/home/unit/docker-entrypoint.sh"] EXPOSE 9000 -CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock", "--user", "unit", "--group", "unit"] - +COPY docker/start_dev.sh /usr/local/bin/start +CMD ["/usr/local/bin/start"] FROM BUILD AS ARTISAN WORKDIR /var/www/html diff --git a/app/docker/start_dev.sh b/app/docker/start_dev.sh new file mode 100755 index 0000000..007ea47 --- /dev/null +++ b/app/docker/start_dev.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh +set -e +role=${CONTAINER_ROLE:-app} +if [ "$role" = "app" ]; then + exec unitd --no-daemon --control unix:/var/run/control.unit.sock --user unit --group unit +elif [ "$role" = "queue" ]; then + echo "Running the queue..." + php /var/www/html/artisan queue:work --verbose --sleep=5 --tries=10 +elif [ "$role" = "websockets" ]; then + echo "Running the websockets..." + php /var/www/html/artisan reverb:start --port=9000 +elif [ "$role" = "scheduler" ]; then + while [ true ] + do + php /var/www/html/artisan schedule:run --verbose --no-interaction & + sleep 60 + done +else + echo "Could not match the container role \"$role\"" + exit 1 +fi diff --git a/app/docker/start_prod.sh b/app/docker/start_prod.sh new file mode 100755 index 0000000..e4d2379 --- /dev/null +++ b/app/docker/start_prod.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh +set -e +role=${CONTAINER_ROLE:-app} +if [ "$role" = "app" ]; then + exec unitd --no-daemon --control unix:/var/run/control.unit.sock --user unit --group unit +elif [ "$role" = "queue" ]; then + echo "Running the queue..." + php /var/www/html/artisan queue:work --verbose --sleep=5 --tries=10 --max-time=3600 +elif [ "$role" = "websockets" ]; then + echo "Running the websockets..." + php /var/www/html/artisan reverb:start --port=9000 +elif [ "$role" = "scheduler" ]; then + while [ true ] + do + php /var/www/html/artisan schedule:run --verbose --no-interaction & + sleep 60 + done +else + echo "Could not match the container role \"$role\"" + exit 1 +fi diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 7814104..a4bb96c 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -26,9 +26,52 @@ services: depends_on: - db - redis + - reverb env_file: app/application/.env + queue: + build: + context: app + dockerfile: docker/Dockerfile + target: PRODUCTION + # restart: always + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: queue + volumes: + - ./app/application:/var/www/html + reverb: + build: + context: app + dockerfile: docker/Dockerfile + target: PRODUCTION + # restart: always + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: websockets + ports: + - ${DOCKER_WEBSOCKET_PORT}:9000 + volumes: + - ./app/application:/var/www/html + scheduler: + build: + context: app + dockerfile: docker/Dockerfile + target: PRODUCTION + # restart: always + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: scheduler + volumes: + - ./app/application:/var/www/html swagger: image: swaggerapi/swagger-ui + # restart: always depends_on: - app environment: @@ -36,6 +79,7 @@ services: BASE_URL: /api-docs redis: image: redis:3.0-alpine + # restart: always db: image: docker.io/mysql:8.0.33 command: --default-authentication-plugin=mysql_native_password diff --git a/docker-compose-prod_docker-hub.yml b/docker-compose-prod_docker-hub.yml index 0524d65..003bbf7 100644 --- a/docker-compose-prod_docker-hub.yml +++ b/docker-compose-prod_docker-hub.yml @@ -11,11 +11,41 @@ services: ports: - ${DOCKER_NGINX_PORT}:80 app: - image: korelf/service-captcha:0.7.1 + image: korelf/service-captcha:0.8.0 # restart: always depends_on: - db - redis + - reverb + env_file: app/.env + queue: + image: korelf/service-captcha:0.8.0 + # restart: always + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: queue + env_file: app/.env + reverb: + image: korelf/service-captcha:0.8.0 + # restart: always + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: websockets + env_file: app/.env + ports: + - ${DOCKER_WEBSOCKET_PORT}:9000 + scheduler: + image: korelf/service-captcha:0.8.0 + # restart: always + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: scheduler env_file: app/.env redis: image: redis:3.0-alpine diff --git a/docker-compose.yml b/docker-compose.yml index ca45a50..b098d3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,54 @@ services: - db - redis - app + - reverb app: build: context: app dockerfile: docker/Dockerfile target: DEVELOP + depends_on: + - db + - redis + - reverb + volumes: + - ./app/application:/var/www/html + queue: + build: + context: app + dockerfile: docker/Dockerfile + target: DEVELOP + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: queue + volumes: + - ./app/application:/var/www/html + reverb: + build: + context: app + dockerfile: docker/Dockerfile + target: DEVELOP + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: websockets + ports: + - ${DOCKER_WEBSOCKET_PORT}:9000 + volumes: + - ./app/application:/var/www/html + scheduler: + build: + context: app + dockerfile: docker/Dockerfile + target: DEVELOP + depends_on: + - db + - redis + environment: + CONTAINER_ROLE: scheduler volumes: - ./app/application:/var/www/html redis: