21 Commits

Author SHA1 Message Date
995b34ead5 Fixed UNIT_SOURCE. 2024-06-12 00:44:14 +05:00
e2b9e60b36 Updated to korelf/service-captcha:0.8.2. 2024-06-11 23:19:32 +05:00
47ef821c0f Added environment UNIT_SOURCE as an example. 2024-06-11 23:02:48 +05:00
0f07c8c6ab Improved queue launch in Docker.
Now it does not die, but in a loop inside it restarts php /var/www/html/artisan queue:work --verbose --sleep=5 --tries=10 --max-time=3600.
2024-06-11 22:44:48 +05:00
af053b4d53 Добавил к Redis Volumes. 2024-06-11 22:42:58 +05:00
de69776bc8 Added php artisan storage:link. 2024-06-11 22:42:10 +05:00
cce94cd210 Added autocomplete="off" attribute to input password. 2024-06-11 22:39:15 +05:00
ed6a036d8d Deleted sweetalert2. 2024-06-11 22:34:55 +05:00
3871670c91 Removed extra code. 2024-06-11 22:30:56 +05:00
0e93b2e780 Fixed .dockerignore. 2024-05-19 21:01:26 +05:00
1b5f805195 Captcha update to 0.8.1. 2024-04-28 20:00:01 +05:00
748d05f8fc Merge pull request 'Версия 0.8.1' (#5) from develop into main
Reviewed-on: #5
2024-04-28 19:54:56 +05:00
b662a85572 Fixed the use of the UNIT_SOURCE parameter for Nginx Unit. 2024-04-28 19:48:20 +05:00
4368aec1b1 Merge pull request 'Версия 0.8.0' (#4) from develop into main
Reviewed-on: #4
2024-04-28 14:26:46 +05:00
bd91cdef0c BROADCAST_DRIVER changed to reverb. 2024-04-28 13:13:27 +05:00
1532cecedc For demo mode, I hid the display of IP and UserAgent. 2024-04-28 02:12:09 +05:00
5703015874 Revived the dashboard. 2024-04-28 01:26:30 +05:00
4bc170ed00 Added final to UsersController. 2024-04-26 20:36:32 +05:00
3f21276ec5 Merge pull request 'Update README.md.' (#3) from develop into main
Reviewed-on: #3
2023-12-13 23:28:38 +06:00
c3bfd4f920 Merge pull request 'Версия 0.7.1.' (#2) from develop into main
Reviewed-on: #2
2023-12-09 00:59:41 +06:00
5ea5d4d0ba Merge pull request 'Version 0.7.0' (#1) from develop into main
Reviewed-on: #1
2023-12-08 21:18:22 +06:00
49 changed files with 2075 additions and 322 deletions

View File

@@ -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

15
app/.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
**/.env
**/*.env
**/.env.example
**/storage/app/*
**/storage/debugbar
**/storage/framework/cache/*
**/storage/framework/sessions/*
**/storage/framework/views/*
**/storage/framework/testing/*
**/storage/logs/*
**/vendor/
**/node_modules/
**/public/build/
**/public/storage

View File

@@ -31,7 +31,7 @@ DB_DATABASE=captcha
DB_USERNAME=captcha DB_USERNAME=captcha
DB_PASSWORD=captcha_pass DB_PASSWORD=captcha_pass
BROADCAST_DRIVER=log BROADCAST_DRIVER=reverb
CACHE_DRIVER=redis CACHE_DRIVER=redis
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis QUEUE_CONNECTION=redis
@@ -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}"

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
<?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;
}
$ip = $captchaLog->ip;
$userAgent = $captchaLog->user_agent;
if (Helpers::isDemoMode()) {
$ip = __('Demo Mode');
$userAgent = __('Demo Mode');
}
$this->captchaLog = [
'created_at' => $captchaLog->created_at->format("d.m.Y H:i:s"),
'link' => $link,
'title' => $title,
'type' => $captchaLog->type->getTitle(),
'ip' => $ip,
'user_agent' => $userAgent,
'referer' => $captchaLog->referer,
];
}
public function broadcastOn(): Channel
{
return new PrivateChannel('chart-captcha-activity');
}
}

View File

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

View File

@@ -11,7 +11,7 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
class UsersController extends Controller final class UsersController extends Controller
{ {
public function __construct( public function __construct(
private readonly UserService $userService private readonly UserService $userService

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ namespace App\Models;
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 @@ final class Captcha extends Model
{ {
return $this->hasMany(CaptchaLog::class); return $this->hasMany(CaptchaLog::class);
} }
public function captchaToken(): BelongsTo
{
return $this->belongsTo(CaptchaToken::class);
}
} }

View File

@@ -6,6 +6,7 @@ use App\Enums\CaptchaLogType;
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 @@ final class CaptchaLog extends Model
{ {
return $query->orderBy('created_at', 'desc'); return $query->orderBy('created_at', 'desc');
} }
public function captcha(): BelongsTo
{
return $this->belongsTo(Captcha::class);
}
} }

View File

@@ -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 @@ final class CaptchaLogRepository
->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;
}
} }

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ namespace App\Services\CaptchaLog;
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 @@ final readonly class CaptchaLogHandler
$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;
} }
} }

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

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

View File

@@ -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"
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -215,7 +215,7 @@ return [
*/ */
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,

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

View File

@@ -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."
} }

View File

@@ -0,0 +1,7 @@
<?php
return [
'Created' => 'Creating a captcha',
'Error' => 'Error in captcha validation',
'Verified' => 'Successfully verified',
'ReadVerified' => 'Completed captcha verification',
];

View File

@@ -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."
} }

View File

@@ -0,0 +1,7 @@
<?php
return [
'Created' => 'Создание капчи',
'Error' => 'Ошибка в валидации капчи',
'Verified' => 'Успешно проверено',
'ReadVerified' => 'Завершена проверка капчи',
];

View File

@@ -15,13 +15,14 @@
"sass": "^1.47.0", "sass": "^1.47.0",
"simplebar": "^5.3.4", "simplebar": "^5.3.4",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"sweetalert2": "^11.0.18",
"vanillajs-datepicker": "^1.2.0", "vanillajs-datepicker": "^1.2.0",
"waypoints": "^4.0.1" "waypoints": "^4.0.1"
}, },
"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 +1252,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 +1446,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",
@@ -1639,15 +1658,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/sweetalert2": {
"version": "11.7.12",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.7.12.tgz",
"integrity": "sha512-TQJy8mQymJLzqWPQOMQErd81Zd/rSYr0UL4pEc7bqEihtjS+zt7LWJXLhfPp93e+Hf3Z2FHMB6QGNskAMCsdTg==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -1723,6 +1733,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 +2771,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 +2905,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",
@@ -3009,11 +3040,6 @@
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
} }
}, },
"sweetalert2": {
"version": "11.7.12",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.7.12.tgz",
"integrity": "sha512-TQJy8mQymJLzqWPQOMQErd81Zd/rSYr0UL4pEc7bqEihtjS+zt7LWJXLhfPp93e+Hf3Z2FHMB6QGNskAMCsdTg=="
},
"tapable": { "tapable": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -3056,6 +3082,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",

View File

@@ -15,13 +15,14 @@
"sass": "^1.47.0", "sass": "^1.47.0",
"simplebar": "^5.3.4", "simplebar": "^5.3.4",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"sweetalert2": "^11.0.18",
"vanillajs-datepicker": "^1.2.0", "vanillajs-datepicker": "^1.2.0",
"waypoints": "^4.0.1" "waypoints": "^4.0.1"
}, },
"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"
} }

View File

@@ -1,5 +1,52 @@
@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>
@demo
<span style="border: 1px solid #ff1810; color: #ff1810; font-weight: bold; font-size: 18px; padding: 7px; text-align: center;">{{ __('Demo Mode') }}</span>
@else
{{ $item->ip }}
@endif
</p>
<p><strong>User Agent:</strong>
@demo
<span style="border: 1px solid #ff1810; color: #ff1810; font-weight: bold; font-size: 18px; padding: 7px; text-align: center;">{{ __('Demo Mode') }}</span>
@else
{{ $item->user_agent }}
@endif
</p>
<p><strong>referer:</strong> {{ $item->referer }}</p>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endcan
</div>
</div>
</x-private.layout> </x-private.layout>

View File

@@ -23,8 +23,8 @@
<form method="post" action="{{ route('profile.update-password') }}"> <form method="post" action="{{ route('profile.update-password') }}">
@csrf @csrf
@method('PUT') @method('PUT')
<x-private.forms.input :title="__('validation.attributes.password')" name="password" type="password" required /> <x-private.forms.input :title="__('validation.attributes.password')" name="password" type="password" autocomplete="off" required />
<x-private.forms.input :title="__('validation.attributes.password_confirmation')" name="password_confirmation" type="password" required /> <x-private.forms.input :title="__('validation.attributes.password_confirmation')" name="password_confirmation" autocomplete="off" type="password" required />
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button> <button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
</form> </form>
</div> </div>

View File

@@ -37,7 +37,7 @@
<span class="input-group-text" id="basic-addon2"> <span class="input-group-text" id="basic-addon2">
<svg class="icon icon-xs text-gray-600" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg> <svg class="icon icon-xs text-gray-600" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg>
</span> </span>
<input type="password" name="password" placeholder="Password" @demo value="{{ config('app.demo_password', null) }}" @endif class="form-control" id="password" required> <input type="password" name="password" placeholder="Password" @demo value="{{ config('app.demo_password', null) }}" @endif class="form-control" id="password" autocomplete="off" required>
</div> </div>
</div> </div>
<!-- End of Form --> <!-- End of Form -->

View File

@@ -2,4 +2,6 @@ import.meta.glob([
'../images/**', '../images/**',
]); ]);
import './bootstrap'; import './bootstrap';
import './volt.js'; import './echo';
import './volt';
import './dashboard'

View 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 => {
});
}

View 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();

View File

@@ -19,53 +19,9 @@
const d = document; const d = document;
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
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) {
const swalWithBootstrapButtons = Swal.mixin({
customClass: {
confirmButton: 'btn btn-primary me-3',
cancelButton: 'btn btn-gray'
},
buttonsStyling: false
});
var themeSettingsEl = document.getElementById('theme-settings');
var themeSettingsExpandEl = document.getElementById('theme-settings-expand');
if(themeSettingsEl) {
var themeSettingsCollapse = new bootstrap.Collapse(themeSettingsEl, {
show: true,
toggle: false
});
if (window.localStorage.getItem('settings_expanded') === 'true') {
themeSettingsCollapse.show();
themeSettingsExpandEl.classList.remove('show');
} else {
themeSettingsCollapse.hide();
themeSettingsExpandEl.classList.add('show');
}
themeSettingsEl.addEventListener('hidden.bs.collapse', function () {
themeSettingsExpandEl.classList.add('show');
window.localStorage.setItem('settings_expanded', false);
});
themeSettingsExpandEl.addEventListener('click', function () {
themeSettingsExpandEl.classList.remove('show');
window.localStorage.setItem('settings_expanded', true);
setTimeout(function() {
themeSettingsCollapse.show();
}, 300);
});
}
// options // options
const breakpoints = { const breakpoints = {
sm: 540, sm: 540,
@@ -84,13 +40,6 @@ d.addEventListener("DOMContentLoaded", function(event) {
}); });
} }
var iconNotifications = d.querySelector('.notification-bell');
if (iconNotifications) {
iconNotifications.addEventListener('shown.bs.dropdown', function () {
iconNotifications.classList.remove('unread');
});
}
[].slice.call(d.querySelectorAll('[data-background]')).map(function(el) { [].slice.call(d.querySelectorAll('[data-background]')).map(function(el) {
el.style.background = 'url(' + el.getAttribute('data-background') + ')'; el.style.background = 'url(' + el.getAttribute('data-background') + ')';
}); });
@@ -112,7 +61,7 @@ d.addEventListener("DOMContentLoaded", function(event) {
//Tooltips //Tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl) return new bootstrap.Tooltip(tooltipTriggerEl)
}) })
@@ -131,164 +80,6 @@ d.addEventListener("DOMContentLoaded", function(event) {
}); });
}) })
if(d.querySelector('.input-slider-container')) {
[].slice.call(d.querySelectorAll('.input-slider-container')).map(function(el) {
var slider = el.querySelector(':scope .input-slider');
var sliderId = slider.getAttribute('id');
var minValue = slider.getAttribute('data-range-value-min');
var maxValue = slider.getAttribute('data-range-value-max');
var sliderValue = el.querySelector(':scope .range-slider-value');
var sliderValueId = sliderValue.getAttribute('id');
var startValue = sliderValue.getAttribute('data-range-value-low');
var c = d.getElementById(sliderId),
id = d.getElementById(sliderValueId);
noUiSlider.create(c, {
start: [parseInt(startValue)],
connect: [true, false],
//step: 1000,
range: {
'min': [parseInt(minValue)],
'max': [parseInt(maxValue)]
}
});
});
}
if (d.getElementById('input-slider-range')) {
var c = d.getElementById("input-slider-range"),
low = d.getElementById("input-slider-range-value-low"),
e = d.getElementById("input-slider-range-value-high"),
f = [d, e];
noUiSlider.create(c, {
start: [parseInt(low.getAttribute('data-range-value-low')), parseInt(e.getAttribute('data-range-value-high'))],
connect: !0,
tooltips: true,
range: {
min: parseInt(c.getAttribute('data-range-value-min')),
max: parseInt(c.getAttribute('data-range-value-max'))
}
}), c.noUiSlider.on("update", function (a, b) {
f[b].textContent = a[b]
});
}
//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;
var loadContent = d.getElementById('extraContent');
var allLoaded = d.getElementById('allLoadedText');
button.classList.add('btn-loading');
button.setAttribute('disabled', 'true');
setTimeout(function () {
loadContent.style.display = 'block';
button.style.display = 'none';
allLoaded.style.display = 'block';
}, 1500);
});
}
var scroll = new SmoothScroll('a[href*="#"]', { var scroll = new SmoothScroll('a[href*="#"]', {
speed: 500, speed: 500,
speedAsDuration: true speedAsDuration: true
@@ -297,68 +88,4 @@ d.addEventListener("DOMContentLoaded", function(event) {
if(d.querySelector('.current-year')){ if(d.querySelector('.current-year')){
d.querySelector('.current-year').textContent = new Date().getFullYear(); d.querySelector('.current-year').textContent = new Date().getFullYear();
} }
// Glide JS
if (d.querySelector('.glide')) {
new Glide('.glide', {
type: 'carousel',
startAt: 0,
perView: 3
}).mount();
}
if (d.querySelector('.glide-testimonials')) {
new Glide('.glide-testimonials', {
type: 'carousel',
startAt: 0,
perView: 1,
autoplay: 2000
}).mount();
}
if (d.querySelector('.glide-clients')) {
new Glide('.glide-clients', {
type: 'carousel',
startAt: 0,
perView: 5,
autoplay: 2000
}).mount();
}
if (d.querySelector('.glide-news-widget')) {
new Glide('.glide-news-widget', {
type: 'carousel',
startAt: 0,
perView: 1,
autoplay: 2000
}).mount();
}
if (d.querySelector('.glide-autoplay')) {
new Glide('.glide-autoplay', {
type: 'carousel',
startAt: 0,
perView: 3,
autoplay: 2000
}).mount();
}
// Pricing countup
var billingSwitchEl = d.getElementById('billingSwitch');
if(billingSwitchEl) {
const countUpStandard = new countUp.CountUp('priceStandard', 99, { startVal: 199 });
const countUpPremium = new countUp.CountUp('pricePremium', 199, { startVal: 299 });
billingSwitchEl.addEventListener('change', function() {
if(billingSwitch.checked) {
countUpStandard.start();
countUpPremium.start();
} else {
countUpStandard.reset();
countUpPremium.reset();
}
});
}
}); });

View File

@@ -12,3 +12,5 @@ use Illuminate\Support\Facades\Broadcast;
| 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);

View File

@@ -20,6 +20,7 @@ Route::middleware('guest')->group(function () {
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::middleware(['auth', 'verified', 'user.locale'])->group(function () {
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']);
}); });

View File

@@ -1,8 +0,0 @@
**/*.env
**/*.env.example
application/bootstrap/cache/*
application/storage/**
application/vendor/**
application/node_modules/**
application/public/build/**
application/public/storage

View File

@@ -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

View File

@@ -5,6 +5,7 @@ set -euo pipefail
WAITLOOPS=5 WAITLOOPS=5
SLEEPSEC=1 SLEEPSEC=1
unitd="unitd" unitd="unitd"
role=${CONTAINER_ROLE:-app}
curl_put() curl_put()
{ {
@@ -22,7 +23,7 @@ curl_put()
return 0 return 0
} }
if [ "$unitd" = "unitd" ] || [ "$unitd" = "unitd-debug" ]; then if [ "$role" = "app" ]; then
echo "$0: Launching Unit daemon to perform initial configuration..." echo "$0: Launching Unit daemon to perform initial configuration..."
/usr/sbin/$unitd --control unix:/var/run/control.unit.sock /usr/sbin/$unitd --control unix:/var/run/control.unit.sock
for i in $(/usr/bin/seq $WAITLOOPS); do for i in $(/usr/bin/seq $WAITLOOPS); do
@@ -61,7 +62,7 @@ if [ "$unitd" = "unitd" ] || [ "$unitd" = "unitd-debug" ]; then
if [ ! -z ${UNIT_SOURCE+x} ] if [ ! -z ${UNIT_SOURCE+x} ]
then then
echo "[${UNIT_SOURCE}]" > /docker-entrypoint.d/unit_source.json echo $UNIT_SOURCE > /docker-entrypoint.d/unit_source.json
curl_put "/docker-entrypoint.d/unit_source.json" "config/listeners/*:9000/forwarded/source" curl_put "/docker-entrypoint.d/unit_source.json" "config/listeners/*:9000/forwarded/source"
fi fi

View File

@@ -5,6 +5,7 @@ set -euo pipefail
WAITLOOPS=5 WAITLOOPS=5
SLEEPSEC=1 SLEEPSEC=1
unitd="unitd" unitd="unitd"
role=${CONTAINER_ROLE:-app}
curl_put() curl_put()
{ {
@@ -22,7 +23,7 @@ curl_put()
return 0 return 0
} }
if [ "$unitd" = "unitd" ] || [ "$unitd" = "unitd-debug" ]; then if [ "$role" = "app" ]; then
echo "$0: Launching Unit daemon to perform initial configuration..." echo "$0: Launching Unit daemon to perform initial configuration..."
/usr/sbin/$unitd --control unix:/var/run/control.unit.sock /usr/sbin/$unitd --control unix:/var/run/control.unit.sock
for i in $(/usr/bin/seq $WAITLOOPS); do for i in $(/usr/bin/seq $WAITLOOPS); do
@@ -61,7 +62,7 @@ if [ "$unitd" = "unitd" ] || [ "$unitd" = "unitd-debug" ]; then
if [ ! -z ${UNIT_SOURCE+x} ] if [ ! -z ${UNIT_SOURCE+x} ]
then then
echo "[${UNIT_SOURCE}]" > /docker-entrypoint.d/unit_source.json echo $UNIT_SOURCE > /docker-entrypoint.d/unit_source.json
curl_put "/docker-entrypoint.d/unit_source.json" "config/listeners/*:9000/forwarded/source" curl_put "/docker-entrypoint.d/unit_source.json" "config/listeners/*:9000/forwarded/source"
fi fi
@@ -102,7 +103,10 @@ php artisan config:cache
php artisan event:cache php artisan event:cache
php artisan route:cache php artisan route:cache
php artisan view:cache php artisan view:cache
php artisan migrate --force php artisan storage:link
if [ "$role" = "app" ]; then
php artisan migrate --force
fi
chown -R unit:unit /var/www/html chown -R unit:unit /var/www/html
chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

24
app/docker/start_dev.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/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..."
while [ true ]
do
php /var/www/html/artisan queue:work --verbose --sleep=5 --tries=10 --max-time=3600
done
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

24
app/docker/start_prod.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/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..."
while [ true ]
do
php /var/www/html/artisan queue:work --verbose --sleep=5 --tries=10 --max-time=3600
done
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

View File

@@ -26,9 +26,55 @@ services:
depends_on: depends_on:
- db - db
- redis - redis
- reverb
env_file: app/application/.env env_file: app/application/.env
environment:
CONTAINER_ROLE: app
UNIT_SOURCE: '"172.16.0.0/12"'
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 +82,9 @@ services:
BASE_URL: /api-docs BASE_URL: /api-docs
redis: redis:
image: redis:3.0-alpine image: redis:3.0-alpine
# restart: always
volumes:
- ./redis/data:/data
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

View File

@@ -11,15 +11,50 @@ 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.2
# restart: always # restart: always
depends_on: depends_on:
- db - db
- redis - redis
- reverb
env_file: app/.env
environment:
CONTAINER_ROLE: app
UNIT_SOURCE: '"172.16.0.0/12"'
queue:
image: korelf/service-captcha:0.8.2
# restart: always
depends_on:
- db
- redis
environment:
CONTAINER_ROLE: queue
env_file: app/.env
reverb:
image: korelf/service-captcha:0.8.2
# 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.2
# 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
# restart: always # restart: always
volumes:
- ./redis/data:/data
swagger: swagger:
image: swaggerapi/swagger-ui image: swaggerapi/swagger-ui
depends_on: depends_on:

View File

@@ -10,15 +10,60 @@ 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:
image: redis:3.0-alpine image: redis:3.0-alpine
volumes:
- ./redis/data:/data
db: db:
image: mysql:8.0.33 image: mysql:8.0.33
command: --default-authentication-plugin=mysql_native_password command: --default-authentication-plugin=mysql_native_password

1
redis/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
data/