Версия 0.8.0 #4
@ -1,4 +1,5 @@
|
|||||||
DOCKER_NGINX_PORT=8080
|
DOCKER_NGINX_PORT=8080
|
||||||
|
DOCKER_WEBSOCKET_PORT=8081
|
||||||
DOCKER_DB_PORT=3306
|
DOCKER_DB_PORT=3306
|
||||||
MYSQL_ROOT_PASSWORD=root_pass
|
MYSQL_ROOT_PASSWORD=root_pass
|
||||||
DB_DATABASE=captcha
|
DB_DATABASE=captcha
|
||||||
|
@ -72,3 +72,21 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
|
|||||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
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}"
|
||||||
|
17
app/application/app/Broadcasting/CreatedCaptchaLog.php
Normal file
17
app/application/app/Broadcasting/CreatedCaptchaLog.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Broadcasting;
|
||||||
|
|
||||||
|
use App\Models\CaptchaToken;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
final readonly class CreatedCaptchaLog
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Authenticate the user's access to the channel.
|
||||||
|
*/
|
||||||
|
public function join(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->can('viewAny', CaptchaToken::class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto\Repository\CaptchaLogRepository;
|
||||||
|
|
||||||
|
use App\Enums\CaptchaLogType;
|
||||||
|
|
||||||
|
final class QuantityByDays
|
||||||
|
{
|
||||||
|
private array $days = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $day ("Y-m-d")
|
||||||
|
* @param CaptchaLogType $type
|
||||||
|
* @param int $count
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add(string $day, CaptchaLogType $type, int $count): void
|
||||||
|
{
|
||||||
|
if (!isset($this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -8,4 +8,9 @@ enum CaptchaLogType: int
|
|||||||
case Error = 2;
|
case Error = 2;
|
||||||
case Verified = 3;
|
case Verified = 3;
|
||||||
case ReadVerified = 4;
|
case ReadVerified = 4;
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return __('captcha_log_type.' . $this->name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
47
app/application/app/Events/CreatedCaptchaLog.php
Normal file
47
app/application/app/Events/CreatedCaptchaLog.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Helpers\Helpers;
|
||||||
|
use App\Models\CaptchaLog;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
final class CreatedCaptchaLog implements ShouldBroadcast
|
||||||
|
{
|
||||||
|
use SerializesModels, Dispatchable;
|
||||||
|
|
||||||
|
public array $chartCaptchaActivity = [];
|
||||||
|
public array $captchaLog = [];
|
||||||
|
|
||||||
|
public function __construct(CaptchaLog $captchaLog) {
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,42 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Private;
|
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;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
final class DashboardController extends Controller
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Private;
|
||||||
|
|
||||||
|
use App\Http\Resources\Private\Websockets\Setting;
|
||||||
|
use App\Services\Private\WebsocketService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
final class WebsocketsController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly WebsocketService $websocketService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function settings(): JsonResponse
|
||||||
|
{
|
||||||
|
$result = $this->websocketService->settings();
|
||||||
|
if (!$result->isSuccess()) {
|
||||||
|
return response()->json($result->getData())->setStatusCode($result->getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(new Setting($result));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Private\Dashboard;
|
||||||
|
|
||||||
|
use App\Enums\CaptchaLogType;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
final class ChartCaptchaActivity extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \App\ServiceResults\Private\Dashboard\ChartCaptchaActivity
|
||||||
|
*/
|
||||||
|
public $resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
$period = $this->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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Private\Websockets;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
final class Setting extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \App\ServiceResults\Private\Websocket\Setting
|
||||||
|
*/
|
||||||
|
public $resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'key' => $this->resource->getKey(),
|
||||||
|
'wsHost' => $this->resource->getWsHost(),
|
||||||
|
'wsPort' => $this->resource->getWsPort(),
|
||||||
|
'wssPort' => $this->resource->getWssPort(),
|
||||||
|
'forceTLS' => $this->resource->isForceTLS(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
final class Captcha extends Model
|
final class Captcha extends Model
|
||||||
@ -34,4 +35,9 @@ public function captchaLogs(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(CaptchaLog::class);
|
return $this->hasMany(CaptchaLog::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function captchaToken(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CaptchaToken::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
final class CaptchaLog extends Model
|
final class CaptchaLog extends Model
|
||||||
{
|
{
|
||||||
@ -44,4 +45,9 @@ public function scopeLatest(Builder $query): Builder
|
|||||||
{
|
{
|
||||||
return $query->orderBy('created_at', 'desc');
|
return $query->orderBy('created_at', 'desc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function captcha(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Captcha::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,30 @@
|
|||||||
|
|
||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Dto\Repository\CaptchaLogRepository\QuantityByDays;
|
||||||
use App\Enums\CaptchaLogType;
|
use App\Enums\CaptchaLogType;
|
||||||
use App\Models\CaptchaLog;
|
use App\Models\CaptchaLog;
|
||||||
|
use App\Services\Search\CreateSearchInstanceCommand;
|
||||||
|
use App\Services\Search\Search;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
final class CaptchaLogRepository
|
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
|
public function countByType(CaptchaLogType $type, ?int $captchaId = null): int
|
||||||
{
|
{
|
||||||
return CaptchaLog::query()
|
return CaptchaLog::query()
|
||||||
@ -32,4 +49,22 @@ public function getCaptchaLogsByTypes(array $types, ?int $captchaId = null, ?int
|
|||||||
->latest()
|
->latest()
|
||||||
->get();
|
->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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ServiceResults\Private\Dashboard;
|
||||||
|
|
||||||
|
use App\Dto\Repository\CaptchaLogRepository\QuantityByDays;
|
||||||
|
use App\ServiceResults\ServiceResult;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
final class ChartCaptchaActivity extends ServiceResult
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Carbon $from,
|
||||||
|
private readonly Carbon $to,
|
||||||
|
private readonly QuantityByDays $quantityByDays,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getFrom(): Carbon
|
||||||
|
{
|
||||||
|
return $this->from;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTo(): Carbon
|
||||||
|
{
|
||||||
|
return $this->to;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQuantityByDays(): QuantityByDays
|
||||||
|
{
|
||||||
|
return $this->quantityByDays;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ServiceResults\Private\Websocket;
|
||||||
|
|
||||||
|
use App\ServiceResults\ServiceResult;
|
||||||
|
|
||||||
|
final class Setting extends ServiceResult
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $key,
|
||||||
|
private readonly string $wsHost,
|
||||||
|
private readonly int $wsPort,
|
||||||
|
private readonly int $wssPort,
|
||||||
|
private readonly bool $forceTLS,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getKey(): string
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Dto\HttpUserData;
|
use App\Dto\HttpUserData;
|
||||||
use App\Enums\CaptchaLogType;
|
use App\Enums\CaptchaLogType;
|
||||||
|
use App\Events\CreatedCaptchaLog;
|
||||||
use App\Models\Captcha;
|
use App\Models\Captcha;
|
||||||
use App\Models\CaptchaLog;
|
use App\Models\CaptchaLog;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -22,12 +23,16 @@ public function handleStore(int $captchaId, CaptchaLogType $captchaLogType, Http
|
|||||||
$referer = Str::limit($referer, 10000, '');
|
$referer = Str::limit($referer, 10000, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return CaptchaLog::create([
|
$captchaLog = CaptchaLog::create([
|
||||||
'captcha_id' => $captchaId,
|
'captcha_id' => $captchaId,
|
||||||
'type' => $captchaLogType,
|
'type' => $captchaLogType,
|
||||||
'ip' => $httpUserData->getClientIp(),
|
'ip' => $httpUserData->getClientIp(),
|
||||||
'user_agent' => $userAgent,
|
'user_agent' => $userAgent,
|
||||||
'referer' => $referer,
|
'referer' => $referer,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
CreatedCaptchaLog::dispatch($captchaLog);
|
||||||
|
|
||||||
|
return $captchaLog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
app/application/app/Services/Private/DashboardService.php
Normal file
54
app/application/app/Services/Private/DashboardService.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Private;
|
||||||
|
|
||||||
|
use App\Enums\CaptchaLogType;
|
||||||
|
use App\Models\CaptchaToken;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\CaptchaLogRepository;
|
||||||
|
use App\ServiceResults\Private\Dashboard\ChartCaptchaActivity;
|
||||||
|
use App\ServiceResults\ServiceResultArray;
|
||||||
|
use App\ServiceResults\ServiceResultError;
|
||||||
|
use App\Services\Service;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
final class DashboardService extends Service
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CaptchaLogRepository $captchaLogRepository,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function captchaLog(User $user, int $limit = 10, array $with = []): ServiceResultError | ServiceResultArray
|
||||||
|
{
|
||||||
|
if ($user->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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
app/application/app/Services/Private/WebsocketService.php
Normal file
24
app/application/app/Services/Private/WebsocketService.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Private;
|
||||||
|
|
||||||
|
use App\ServiceResults\Private\Websocket\Setting;
|
||||||
|
use App\ServiceResults\ServiceResultError;
|
||||||
|
use App\Services\Service;
|
||||||
|
|
||||||
|
final class WebsocketService extends Service
|
||||||
|
{
|
||||||
|
public function settings(): ServiceResultError | Setting
|
||||||
|
{
|
||||||
|
$config = config('reverb.apps');
|
||||||
|
$config = $config['apps'][0] ?? [];
|
||||||
|
|
||||||
|
return new Setting(
|
||||||
|
key: $config['key'] ?? '',
|
||||||
|
wsHost: $config['options_for_client']['host'] ?? '',
|
||||||
|
wsPort: (int) $config['options_for_client']['port'] ?? 80,
|
||||||
|
wssPort: 443,
|
||||||
|
forceTLS: $config['options_for_client']['useTLS'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"kor-elf/captcha-rule-for-laravel": "^1.0",
|
"kor-elf/captcha-rule-for-laravel": "^1.0",
|
||||||
"laravel/framework": "^11.0",
|
"laravel/framework": "^11.0",
|
||||||
|
"laravel/reverb": "@beta",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.8"
|
"laravel/tinker": "^2.8"
|
||||||
},
|
},
|
||||||
|
1035
app/application/composer.lock
generated
1035
app/application/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -215,7 +215,7 @@
|
|||||||
*/
|
*/
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
App\Providers\AuthServiceProvider::class,
|
App\Providers\AuthServiceProvider::class,
|
||||||
// App\Providers\BroadcastServiceProvider::class,
|
App\Providers\BroadcastServiceProvider::class,
|
||||||
App\Providers\EventServiceProvider::class,
|
App\Providers\EventServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
App\Providers\RouteServiceProvider::class,
|
||||||
|
|
||||||
|
97
app/application/config/reverb.php
Normal file
97
app/application/config/reverb.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Reverb Server
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default server used by Reverb to handle
|
||||||
|
| incoming messages as well as broadcasting message to all your
|
||||||
|
| connected clients. At this time only "reverb" is supported.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 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',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
@ -73,5 +73,6 @@
|
|||||||
"The time for captcha verification has passed": "The time for captcha verification has passed.",
|
"The time for captcha verification has passed": "The time for captcha verification has passed.",
|
||||||
"Captcha does not pass verification": "Captcha does not pass verification.",
|
"Captcha does not pass verification": "Captcha does not pass verification.",
|
||||||
"Add code to the site": "Add code to the site",
|
"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."
|
||||||
}
|
}
|
||||||
|
7
app/application/lang/en/captcha_log_type.php
Normal file
7
app/application/lang/en/captcha_log_type.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'Created' => 'Creating a captcha',
|
||||||
|
'Error' => 'Error in captcha validation',
|
||||||
|
'Verified' => 'Successfully verified',
|
||||||
|
'ReadVerified' => 'Completed captcha verification',
|
||||||
|
];
|
@ -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": "!!! Включён демо режим !!!",
|
||||||
|
"From cannot be greater than To": "From не может быть больше To."
|
||||||
}
|
}
|
||||||
|
7
app/application/lang/ru/captcha_log_type.php
Normal file
7
app/application/lang/ru/captcha_log_type.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'Created' => 'Создание капчи',
|
||||||
|
'Error' => 'Ошибка в валидации капчи',
|
||||||
|
'Verified' => 'Успешно проверено',
|
||||||
|
'ReadVerified' => 'Завершена проверка капчи',
|
||||||
|
];
|
47
app/application/package-lock.json
generated
47
app/application/package-lock.json
generated
@ -21,7 +21,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios": "^1.1.2",
|
"axios": "^1.1.2",
|
||||||
|
"laravel-echo": "^1.16.1",
|
||||||
"laravel-vite-plugin": "^0.7.2",
|
"laravel-vite-plugin": "^0.7.2",
|
||||||
|
"pusher-js": "^8.4.0-rc2",
|
||||||
"sass-loader": "^13.3.2",
|
"sass-loader": "^13.3.2",
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
}
|
}
|
||||||
@ -1251,6 +1253,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"node_modules/laravel-vite-plugin": {
|
||||||
"version": "0.7.8",
|
"version": "0.7.8",
|
||||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz",
|
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz",
|
||||||
@ -1436,6 +1447,15 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@ -1723,6 +1743,12 @@
|
|||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
|
||||||
@ -2755,6 +2781,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"laravel-vite-plugin": {
|
||||||
"version": "0.7.8",
|
"version": "0.7.8",
|
||||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz",
|
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz",
|
||||||
@ -2883,6 +2915,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@ -3056,6 +3097,12 @@
|
|||||||
"is-number": "^7.0.0"
|
"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": {
|
"update-browserslist-db": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
|
||||||
|
@ -21,7 +21,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios": "^1.1.2",
|
"axios": "^1.1.2",
|
||||||
|
"laravel-echo": "^1.16.1",
|
||||||
"laravel-vite-plugin": "^0.7.2",
|
"laravel-vite-plugin": "^0.7.2",
|
||||||
|
"pusher-js": "^8.4.0-rc2",
|
||||||
"sass-loader": "^13.3.2",
|
"sass-loader": "^13.3.2",
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,40 @@
|
|||||||
@section('meta_title', __('sections.Dashboard'))
|
@section('meta_title', __('sections.Dashboard'))
|
||||||
@section('h1', __('sections.Dashboard'))
|
@section('h1', __('sections.Dashboard'))
|
||||||
<x-private.layout>
|
<x-private.layout>
|
||||||
|
<div class="card border-0 shadow mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
@can('viewAny', \App\Models\CaptchaToken::class)
|
||||||
|
<div id="chart-сaptcha-activity"></div>
|
||||||
|
<div class="mt-4 table-responsive">
|
||||||
|
<table class="table table-centered table-nowrap mb-0 rounded">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th class="border-0">{{ __('validation.attributes.date') }}</th>
|
||||||
|
<th class="border-0">Capctha</th>
|
||||||
|
<th class="border-0"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="captcha-log">
|
||||||
|
@foreach($items as $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $item->created_at->timezone(\App\Helpers\Helpers::getUserTimeZone())->format("d.m.Y H:i:s") }}</td>
|
||||||
|
<td>
|
||||||
|
@if($item->captcha && $item->captcha->captchaToken)
|
||||||
|
<a href="{{ route('captcha-tokens.edit', ['captcha_token' => $item->captcha->captcha_token_id]) }}">{{ $item->captcha->captchaToken->title }}</a>
|
||||||
|
@endif
|
||||||
|
<p><strong>{{ $item->type->getTitle() }}</strong></p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p><strong>IP:</strong> {{ $item->ip }}</p>
|
||||||
|
<p><strong>User Agent:</strong> {{ $item->user_agent }}</p>
|
||||||
|
<p><strong>referer:</strong> {{ $item->referer }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</x-private.layout>
|
</x-private.layout>
|
||||||
|
@ -2,4 +2,6 @@ import.meta.glob([
|
|||||||
'../images/**',
|
'../images/**',
|
||||||
]);
|
]);
|
||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
import './volt.js';
|
import './echo';
|
||||||
|
import './volt';
|
||||||
|
import './dashboard'
|
||||||
|
80
app/application/resources/volt/js/dashboard.js
Normal file
80
app/application/resources/volt/js/dashboard.js
Normal file
@ -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 = `<a href="${e.captchaLog.link}">${e.captchaLog.title}</a>`;
|
||||||
|
}
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${e.captchaLog.created_at}</td>
|
||||||
|
<td>
|
||||||
|
${link}
|
||||||
|
<p><strong>${e.captchaLog.type}</strong></p>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<p><strong>IP:</strong> ${e.captchaLog.ip}</p>
|
||||||
|
<p><strong>User Agent:</strong> ${e.captchaLog.user_agent}</p>
|
||||||
|
<p><strong>referer:</strong> ${e.captchaLog.referer}</p>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
captchaLog.prepend(tr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
49
app/application/resources/volt/js/echo.js
Normal file
49
app/application/resources/volt/js/echo.js
Normal file
@ -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();
|
@ -21,8 +21,6 @@ const d = document;
|
|||||||
import * as bootstrap from 'bootstrap';
|
import * as bootstrap from 'bootstrap';
|
||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import SmoothScroll from 'smooth-scroll';
|
import SmoothScroll from 'smooth-scroll';
|
||||||
import Chartist from 'chartist';
|
|
||||||
import 'chartist-plugin-tooltips';
|
|
||||||
|
|
||||||
d.addEventListener("DOMContentLoaded", function(event) {
|
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')) {
|
if (d.getElementById('loadOnClick')) {
|
||||||
d.getElementById('loadOnClick').addEventListener('click', function () {
|
d.getElementById('loadOnClick').addEventListener('click', function () {
|
||||||
var button = this;
|
var button = this;
|
||||||
|
@ -12,3 +12,5 @@
|
|||||||
| used to check if an authenticated user can listen to the channel.
|
| used to check if an authenticated user can listen to the channel.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Broadcast::channel('chart-captcha-activity', \App\Broadcasting\CreatedCaptchaLog::class);
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
Route::middleware(['auth', 'verified', 'user.locale'])->group(function () {
|
Route::middleware(['auth', 'verified', 'user.locale'])->group(function () {
|
||||||
Route::post('logout', [\App\Http\Controllers\AuthController::class, 'logout'])->name('logout');
|
Route::post('logout', [\App\Http\Controllers\AuthController::class, 'logout'])->name('logout');
|
||||||
Route::get('/', [\App\Http\Controllers\Private\DashboardController::class, 'index'])->name('home');
|
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.')
|
Route::prefix('profile')->as('profile.')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
Route::get('/', [\App\Http\Controllers\Private\ProfileController::class, 'profile'])->name('edit');
|
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('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::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']);
|
||||||
});
|
});
|
||||||
|
@ -73,7 +73,8 @@ STOPSIGNAL SIGTERM
|
|||||||
|
|
||||||
ENTRYPOINT ["/home/unit/docker-entrypoint.sh"]
|
ENTRYPOINT ["/home/unit/docker-entrypoint.sh"]
|
||||||
EXPOSE 9000
|
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
|
FROM BUILD AS DEVELOP
|
||||||
@ -91,9 +92,9 @@ RUN chmod 755 /home/unit/docker-entrypoint.sh \
|
|||||||
ENTRYPOINT ["/home/unit/docker-entrypoint.sh"]
|
ENTRYPOINT ["/home/unit/docker-entrypoint.sh"]
|
||||||
|
|
||||||
EXPOSE 9000
|
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
|
FROM BUILD AS ARTISAN
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
21
app/docker/start_dev.sh
Executable file
21
app/docker/start_dev.sh
Executable file
@ -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
|
21
app/docker/start_prod.sh
Executable file
21
app/docker/start_prod.sh
Executable file
@ -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
|
@ -26,9 +26,52 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
|
- reverb
|
||||||
env_file: app/application/.env
|
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:
|
swagger:
|
||||||
image: swaggerapi/swagger-ui
|
image: swaggerapi/swagger-ui
|
||||||
|
# restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- app
|
||||||
environment:
|
environment:
|
||||||
@ -36,6 +79,7 @@ services:
|
|||||||
BASE_URL: /api-docs
|
BASE_URL: /api-docs
|
||||||
redis:
|
redis:
|
||||||
image: redis:3.0-alpine
|
image: redis:3.0-alpine
|
||||||
|
# restart: always
|
||||||
db:
|
db:
|
||||||
image: docker.io/mysql:8.0.33
|
image: docker.io/mysql:8.0.33
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
|
@ -11,11 +11,41 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- ${DOCKER_NGINX_PORT}:80
|
- ${DOCKER_NGINX_PORT}:80
|
||||||
app:
|
app:
|
||||||
image: korelf/service-captcha:0.7.1
|
image: korelf/service-captcha:0.8.0
|
||||||
# restart: always
|
# restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- redis
|
- 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
|
env_file: app/.env
|
||||||
redis:
|
redis:
|
||||||
image: redis:3.0-alpine
|
image: redis:3.0-alpine
|
||||||
|
@ -10,11 +10,54 @@ services:
|
|||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
- app
|
- app
|
||||||
|
- reverb
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: app
|
context: app
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
target: DEVELOP
|
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:
|
volumes:
|
||||||
- ./app/application:/var/www/html
|
- ./app/application:/var/www/html
|
||||||
redis:
|
redis:
|
||||||
|
Loading…
Reference in New Issue
Block a user