first commit

This commit is contained in:
Noor E Ilahi
2026-01-09 12:54:53 +05:30
commit 7ccf44f7da
1070 changed files with 113036 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Console\Commands;
use App\Models\TimeInterval;
use App\Models\User;
use Carbon\Carbon;
use DB;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
class CalculateEfficiency extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:calculate-efficiency';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Calculate efficiency for users';
/**
* Execute the console command.
*/
public function handle()
{
$startDate = Carbon::now()->subMonth(2);
$users = User::all();
foreach ($users as $user) {
$tasks = $user->tasks()
->select(['id', 'estimate'])
->whereHas('status', static function (Builder $query) {
$query->where('active', false);
})
->whereHas('timeIntervals', static function (Builder $query) use ($startDate) {
$query
->whereNotNull('estimate')
->where('start_at', '>', $startDate);
})
->get()
->keyBy('id')
->toArray();
if (empty($tasks)) {
$user->efficiency = null;
$user->save();
continue;
}
$taskIds = array_keys($tasks);
$timeIntervals = TimeInterval::query()
->select(['user_id', 'task_id', DB::raw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as duration')])
->where('start_at', '>', $startDate)
->where('user_id', $user->id)
->whereIn('task_id', $taskIds)
->groupBy(['user_id', 'task_id'])
->get();
$totalEfficiency = 0;
foreach ($timeIntervals as $timeInterval) {
$tasks[$timeInterval->task_id]['duration'] = (int)$timeInterval->duration;
$totalEfficiency += $tasks[$timeInterval->task_id]['duration'] / (float)$tasks[$timeInterval->task_id]['estimate'];
}
$user->efficiency = $totalEfficiency / count($tasks);
$user->save();
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ClearExpiredApps;
use App\Models\TrackedApplication;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:intervals:apps:clear-expired')]
class ClearExpiredTrackedApps extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:intervals:apps:clear-expired';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$this->info(
sprintf(
'Found %d items for deletion',
TrackedApplication::where(
'created_at',
'<=',
now()->subDay()->toIso8601String()
)->withoutGlobalScopes()->count()
)
);
ClearExpiredApps::dispatch();
$this->info('Clearance job dispatched');
return 0;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Contracts\AttachmentService;
use App\Models\Attachment;
use App\Models\SusFiles;
use Arr;
use Illuminate\Console\Command;
use Storage;
use Str;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:attachments:find-sus-files')]
class FindSusFiles extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:attachments:find-sus-files';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Find files that exist inside attachments projects folder but not in DB';
/**
* Execute the console command.
*/
public function handle(AttachmentService $service): void
{
SusFiles::query()->delete();
$this->withProgressBar(
Storage::disk('attachments')->allFiles('projects'),
function ($filePath) use ($service) {
$fileName = Str::of($filePath)->basename()->toString();
if (Attachment::whereId($fileName)->exists() === false) {
SusFiles::create([
'path' => $filePath,
'mime_type' => $service->getMimeType($filePath),
'hash' => $service->getHashSum($filePath),
]);
}
}
);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Mail;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:mail:test')]
class MailTestCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:mail:test {destination}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test mail sending';
public function handle(): void
{
$destination = $this->argument('destination');
Mail::raw('Text to e-mail', static function ($message) use ($destination) {
$message->to($destination);
});
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Validator;
use Illuminate\Validation\Rule;
use Illuminate\Contracts\Console\Isolatable;
#[AsCommand(name: 'cattr:make:admin')]
class MakeAdmin extends Command implements Isolatable
{
protected $signature = 'cattr:make:admin
{--o : Skip instance registration}
{--email= : User email}
{--name= : User name}
{--password= : User password}';
protected $description = 'Creates admin user with data from ENV or input';
public function handle(): int
{
if ($this->option('email')) {
$email = $this->option('email');
if (Validator::make([
'email' => $email
], [
'email' => ['email', Rule::unique(User::class)]
])->fails()) {
$this->error('Email is incorrect or it was already registered');
return 1;
}
}
$email = env('APP_ADMIN_EMAIL', 'admin@cattr.app');
if (Validator::make([
'email' => $email
], [
'email' => ['email', Rule::unique(User::class)]
])->fails()) {
if (env('IMAGE_VERSION', false)) {
$this->info('Admin already exists, skipping creation');
return 0;
}
$this->error('Email is incorrect or it was already registered');
return 1;
}
if ($email !== 'admin@cattr.app' && !$this->option('o') && !User::admin()->count()) {
$self = $this;
rescue(
static fn () => $self->call(
RegisterInstance::class,
[
'adminEmail' => $email
],
),
);
}
if ($this->option('password')) {
$password = $this->option('password');
if (Validator::make([
'password' => $password
], [
'password' => 'min:6'
])->fails()) {
$this->warn('Minimum length is 6 characters');
return 1;
}
}
$password = env('APP_ADMIN_PASSWORD', 'password');
if (Validator::make([
'password' => $password
], [
'password' => 'min:6'
])->fails()) {
$this->warn('Minimum length is 6 characters');
return 1;
}
User::factory()->admin()->create([
'full_name' => $this->option('name') ?: env('APP_ADMIN_NAME', 'Admin'),
'email' => $email,
'password' => $password,
'last_activity' => now(),
]);
$this->info("Administrator with email $email was created successfully");
return 0;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Helpers\Version;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:module:version')]
class ModuleVersion extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:module:version {module}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get version of provided module';
/**
* Execute the console command.
*/
public function handle(): void
{
$this->info((string)new Version($this->argument('module')));
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Console\Commands;
use App\Models\Task;
use App\Models\CronTaskWorkers;
use App\Models\TimeInterval;
use Carbon\Carbon;
use DB;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Settings;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:task:recreate-workers')]
class RecreateCronTaskWorkers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:task:recreate-workers';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Recreates materialized table for CronTaskWorkers model';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$timezone = Settings::scope('core')->get('timezone');
$reportCreatedAt = Carbon::now($timezone)->format('Y-m-d H:i:s e');
$this->withProgressBar(
Task::whereNull('deleted_at')->lazyById(),
static fn(Task $task) => rescue(static function () use ($task) {
CronTaskWorkers::whereTaskId($task->id)->delete();
CronTaskWorkers::insertUsing(
['user_id', 'task_id', 'duration', 'created_by_cron'],
TimeInterval::selectRaw('user_id, task_id, SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as duration, "1" as created_by_cron')
->where('task_id', '=', $task->id)
->groupBy(['user_id', 'task_id'])
);
})
);
Settings::scope('core.reports')->set('planned_time_report_date', $reportCreatedAt);
CronTaskWorkers::whereDoesntHave('task')
->orWhereDoesntHave('user')
->orWhereHas('task',
static fn(EloquentBuilder $query) => $query
->whereNotNull('deleted_at')
)->delete();
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Console\Commands;
use App\Helpers\ModuleHelper;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Console\Command;
use JsonException;
use Exception;
use Settings;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:register')]
class RegisterInstance extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:register {adminEmail} {--i : Interactive mode}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send instance data on the statistics server';
/**
* Execute the console command.
*
* @param Client $client
*
* @return int
* @throws JsonException|GuzzleException
*/
public function handle(Client $client): int
{
if (Settings::scope('core')->get('instance')) {
echo 'Application already registered';
return 1;
}
try {
$appVersion = config('app.version');
$response = $client->post(config('app.stats_collector_url') . '/instance', [
'json' => [
'ownerEmail' => $this->argument('adminEmail'),
'version' => $appVersion,
'modules' => ModuleHelper::getModulesInfo(),
'image' => getenv('IMAGE_VERSION')
]
]);
$responseBody = json_decode(
$response->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
);
if (isset($responseBody['instanceId'])) {
Settings::scope('core')->set('instance', $responseBody['instanceId']);
}
if (isset($responseBody['release']['flashMessage'])) {
$this->info($responseBody['release']['flashMessage']);
}
if (isset($responseBody['release']['lastVersion'])
&& $responseBody['release']['lastVersion'] > $appVersion
) {
$this->alert("New version is available: {$responseBody['release']['lastVersion']}");
}
if ($responseBody['release']['vulnerable']) {
if ($this->option('i')) {
// Interactive mode
return $this->confirm('You have a vulnerable version, are you sure you want to continue?');
}
$this->alert('You have a vulnerable version. Please update to the latest version.');
}
return 0;
} catch (Exception $e) {
if ($e->getResponse()) {
$error = json_decode(
$e->getResponse()->getBody(),
true,
512,
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
);
$this->warn($error['message']);
} else {
$this->warn('Сould not get a response from the server to check the relevance of your version.');
}
return 0;
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use App\Models\TimeInterval;
use DB;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:intervals:dedupe')]
class RemoveDuplicateIntervals extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:intervals:dedupe';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes duplicates in the Time intervals table';
/**
* Execute the console command.
*/
public function handle(): void
{
$interval_ids = DB::table('time_intervals as ti1')
->join('time_intervals as ti2', static function ($join) {
$join->on('ti2.start_at', '=', 'ti1.start_at');
$join->on('ti2.end_at', '=', 'ti1.end_at');
$join->on('ti2.user_id', '=', 'ti1.user_id');
$join->on('ti2.id', '>', 'ti1.id');
})
->whereNull('ti2.deleted_at')
->pluck('ti2.id')
->unique();
TimeInterval::destroy($interval_ids->toArray());
$count = $interval_ids->count();
$this->info("Removed $count intervals.");
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Console\Commands;
use Exception;
use Illuminate\Console\Command;
use DB;
use Illuminate\Database\Console\Seeds\SeedCommand;
use Nwidart\Modules\Commands\SeedCommand as ModuleSeedCommand;
use Settings;
use Storage;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:reset')]
class ResetCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:reset {--s|seed} {--f|force} {--i|images}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Cattr flush database';
protected array $protectedTables = ['migrations', 'jobs', 'failed_jobs'];
/**
* Execute the console command.
*
* @throws Exception
*/
public function handle(): int
{
if (!$this->option('force') && !$this->confirm('Are you sure want to drop data for your Cattr instance?')) {
return 0;
}
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
$tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
foreach ($tables as $table) {
if (!in_array($table, $this->protectedTables, true)) {
DB::table($table)->truncate();
}
}
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
if ($this->option('images')) {
Storage::deleteDirectory('uploads/screenshots');
}
$this->call(SeedCommand::class, [
'--class' => 'InitialSeeder',
'--force' => true
]);
if ($this->option('seed')) {
$this->call(SeedCommand::class, [
'--force' => true
]);
$this->call(ModuleSeedCommand::class, [
'--force' => true
]);
}
return 0;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Helpers\StorageCleaner;
use Illuminate\Contracts\Container\BindingResolutionException;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:screenshots:rotate')]
class RotateScreenshots extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:screenshots:rotate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rotate screenshots';
/**
* Execute the console command.
*
* @throws BindingResolutionException
*/
public function handle(): void
{
if (!StorageCleaner::needThinning()) {
$this->info('Time for thinning hasn\'t come');
return;
}
$this->info('For thinning available ' . StorageCleaner::countAvailableScreenshots() . ' screenshots');
$spaceBefore = StorageCleaner::getUsedSpace();
$this->info('Started thinning...');
StorageCleaner::thin();
$this->info('Totally freed ' . round(
($spaceBefore - StorageCleaner::getUsedSpace()) / 1024 / 1024,
3
) . 'MB');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Console\Commands;
use App\Models\Property;
use Illuminate\Console\Command;
use Settings;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:set:language')]
class SetLanguage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:set:language {language}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sets company language';
public function handle(): void
{
$language = $this->argument('language');
if (!in_array($language, config('app.languages'), true)) {
$this->error('Invalid or not supported language');
return;
}
Settings::scope('core')->set('language', $language);
$this->info(strtoupper($language) . ' language successfully set');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands;
use App\Models\Property;
use Illuminate\Console\Command;
use Settings;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:set:timezone')]
class SetTimeZone extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:set:timezone {timezone}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sets company timezone';
public function handle(): void
{
$timezone = $this->argument('timezone');
if (!in_array($timezone, timezone_identifiers_list(), true)) {
$this->error('Invalid time zone format');
return;
}
Settings::scope('core')->set('timezone', $timezone);
$this->info("$timezone time zone successfully set");
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Console\Commands;
use App\Contracts\AttachmentService;
use App\Models\Attachment;
use App\Models\SusFiles;
use Arr;
use Illuminate\Console\Command;
use Storage;
use Str;
use Symfony\Component\Console\Attribute\AsCommand;
use App\Jobs\VerifyAttachmentHash;
#[AsCommand(name: 'cattr:attachments:verify')]
class VerifyAttachments extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:attachments:verify';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Verify hash of all files';
/**
* Execute the console command.
*/
public function handle(AttachmentService $service): void
{
$this->withProgressBar(Attachment::lazyById(), fn($attachment) => VerifyAttachmentHash::dispatch($attachment));
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Helpers\Version;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cattr:version')]
class VersionCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cattr:version';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get Cattr version';
/**
* Execute the console command.
*/
public function handle(): void
{
$this->info((string)new Version());
}
}

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

@@ -0,0 +1,63 @@
<?php
namespace App\Console;
use App\Console\Commands\CalculateEfficiency;
use App\Console\Commands\RecreateCronTaskWorkers;
use App\Console\Commands\FindSusFiles;
use App\Console\Commands\RotateScreenshots;
use App\Jobs\ClearExpiredApps;
use App\Console\Commands\VerifyAttachments;
use App\Models\Attachment;
use Exception;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Sentry\State\Scope;
use Settings;
use function Sentry\configureScope;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
public final function bootstrap(): void
{
parent::bootstrap();
}
/**
* Define the application's command schedule.
*
* @param Schedule $schedule
* @return void
* @throws Exception
*/
protected function schedule(Schedule $schedule): void
{
$schedule->command(RotateScreenshots::class)->weekly()->when(Settings::scope('core')->get('auto_thinning'));
$schedule->job(new ClearExpiredApps)->daily();
$schedule->command(RecreateCronTaskWorkers::class)->daily()->runInBackground()->withoutOverlapping();
$schedule->command(VerifyAttachments::class)->daily()->runInBackground()->withoutOverlapping();
$schedule->command(CalculateEfficiency::class)->daily()->runInBackground()->withoutOverlapping();
$schedule->command(FindSusFiles::class)->weekly()->runInBackground()->withoutOverlapping();
}
/**
* Register the Closure based commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');
}
}