first commit
This commit is contained in:
75
app/Console/Commands/CalculateEfficiency.php
Normal file
75
app/Console/Commands/CalculateEfficiency.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/Console/Commands/ClearExpiredTrackedApps.php
Normal file
51
app/Console/Commands/ClearExpiredTrackedApps.php
Normal 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;
|
||||
}
|
||||
}
|
||||
53
app/Console/Commands/FindSusFiles.php
Normal file
53
app/Console/Commands/FindSusFiles.php
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
34
app/Console/Commands/MailTestCommand.php
Normal file
34
app/Console/Commands/MailTestCommand.php
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
101
app/Console/Commands/MakeAdmin.php
Normal file
101
app/Console/Commands/MakeAdmin.php
Normal 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;
|
||||
}
|
||||
}
|
||||
33
app/Console/Commands/ModuleVersion.php
Normal file
33
app/Console/Commands/ModuleVersion.php
Normal 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')));
|
||||
}
|
||||
}
|
||||
65
app/Console/Commands/RecreateCronTaskWorkers.php
Normal file
65
app/Console/Commands/RecreateCronTaskWorkers.php
Normal 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();
|
||||
}
|
||||
}
|
||||
105
app/Console/Commands/RegisterInstance.php
Normal file
105
app/Console/Commands/RegisterInstance.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
app/Console/Commands/RemoveDuplicateIntervals.php
Normal file
48
app/Console/Commands/RemoveDuplicateIntervals.php
Normal 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.");
|
||||
}
|
||||
}
|
||||
75
app/Console/Commands/ResetCommand.php
Normal file
75
app/Console/Commands/ResetCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
52
app/Console/Commands/RotateScreenshots.php
Normal file
52
app/Console/Commands/RotateScreenshots.php
Normal 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');
|
||||
}
|
||||
}
|
||||
39
app/Console/Commands/SetLanguage.php
Normal file
39
app/Console/Commands/SetLanguage.php
Normal 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');
|
||||
}
|
||||
}
|
||||
40
app/Console/Commands/SetTimeZone.php
Normal file
40
app/Console/Commands/SetTimeZone.php
Normal 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");
|
||||
}
|
||||
}
|
||||
39
app/Console/Commands/VerifyAttachments.php
Normal file
39
app/Console/Commands/VerifyAttachments.php
Normal 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));
|
||||
}
|
||||
}
|
||||
33
app/Console/Commands/VersionCommand.php
Normal file
33
app/Console/Commands/VersionCommand.php
Normal 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
63
app/Console/Kernel.php
Normal 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');
|
||||
}
|
||||
}
|
||||
17
app/Contracts/AppReport.php
Normal file
17
app/Contracts/AppReport.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
abstract class AppReport
|
||||
{
|
||||
final public static function init(...$arguments): self
|
||||
{
|
||||
return new static(...$arguments);
|
||||
}
|
||||
|
||||
abstract public function getReportId(): string;
|
||||
|
||||
abstract public function getLocalizedReportName(): string;
|
||||
|
||||
abstract public function store(string $filePath = null, string $disk = null, string $writerType = null, $diskOptions = []);
|
||||
}
|
||||
17
app/Contracts/AttachmentAble.php
Normal file
17
app/Contracts/AttachmentAble.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\Models\Attachment;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
interface AttachmentAble
|
||||
{
|
||||
/**
|
||||
* @return Attachment[]
|
||||
*/
|
||||
public function attachments();
|
||||
public function attachmentsRelation(): MorphMany;
|
||||
|
||||
public function getProjectId(): int;
|
||||
}
|
||||
43
app/Contracts/AttachmentService.php
Normal file
43
app/Contracts/AttachmentService.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\Models\Attachment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
interface AttachmentService
|
||||
{
|
||||
public function storeFile(UploadedFile $file, Attachment $attachment): string|false;
|
||||
|
||||
public function handleProjectChange(Task $parent): void;
|
||||
|
||||
public function moveAttachment(Attachment $attachment, $newProjectId): void;
|
||||
|
||||
public function deleteAttachment(Attachment $attachment): void;
|
||||
|
||||
public function deleteAttachments(array $attachmentsIds): void;
|
||||
|
||||
public function handleParentDeletion(AttachmentAble $parent): void;
|
||||
|
||||
public function handleProjectDeletion(Project $project): void;
|
||||
|
||||
public function fileExists(Attachment $attachment): bool;
|
||||
|
||||
public function attach(AttachmentAble $parent, array $idsToAttach);
|
||||
|
||||
public function getProjectPath(Project $project): string;
|
||||
|
||||
public function getPath(Attachment $attachment): string;
|
||||
|
||||
public function getFullPath(Attachment $attachment): string;
|
||||
|
||||
public function getHashAlgo(): string;
|
||||
|
||||
public function getHashSum(Attachment|string $attachment): false|string;
|
||||
|
||||
public function getMimeType(Attachment|string $attachment): false|string;
|
||||
|
||||
public function verifyHash(Attachment $attachment);
|
||||
}
|
||||
63
app/Contracts/ScreenshotService.php
Normal file
63
app/Contracts/ScreenshotService.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\Jobs\GenerateScreenshotThumbnail;
|
||||
use App\Models\TimeInterval;
|
||||
use Image;
|
||||
use Intervention\Image\Constraint;
|
||||
use Storage;
|
||||
|
||||
abstract class ScreenshotService
|
||||
{
|
||||
protected const FILE_FORMAT = 'jpg';
|
||||
public const PARENT_FOLDER = 'screenshots/';
|
||||
public const THUMBS_FOLDER = 'thumbs/';
|
||||
private const THUMB_WIDTH = 280;
|
||||
private const QUALITY = 50;
|
||||
|
||||
/** Get screenshot path by interval */
|
||||
abstract public function getScreenshotPath(TimeInterval|int $interval): string;
|
||||
/** Get screenshot thumbnail path by interval */
|
||||
abstract public function getThumbPath(TimeInterval|int $interval): string;
|
||||
|
||||
public function saveScreenshot($file, $timeInterval): void
|
||||
{
|
||||
if (!Storage::exists(self::PARENT_FOLDER)) {
|
||||
Storage::makeDirectory(self::PARENT_FOLDER);
|
||||
}
|
||||
|
||||
$path = is_string($file) ? $file : $file->path();
|
||||
|
||||
$image = Image::make($path);
|
||||
|
||||
Storage::put($this->getScreenshotPath($timeInterval), (string)$image->encode(self::FILE_FORMAT, self::QUALITY));
|
||||
|
||||
GenerateScreenshotThumbnail::dispatch($timeInterval);
|
||||
}
|
||||
|
||||
public function createThumbnail(TimeInterval|int $timeInterval): void
|
||||
{
|
||||
if (!Storage::exists(self::PARENT_FOLDER . self::THUMBS_FOLDER)) {
|
||||
Storage::makeDirectory(self::PARENT_FOLDER . self::THUMBS_FOLDER);
|
||||
}
|
||||
|
||||
$image = Image::make(Storage::path($this->getScreenshotPath($timeInterval)));
|
||||
|
||||
$thumb = $image->resize(self::THUMB_WIDTH, null, fn(Constraint $constraint) => $constraint->aspectRatio());
|
||||
|
||||
Storage::put($this->getThumbPath($timeInterval), (string)$thumb->encode(self::FILE_FORMAT, self::QUALITY));
|
||||
}
|
||||
|
||||
public function destroyScreenshot(TimeInterval|int $interval): void
|
||||
{
|
||||
Storage::delete($this->getScreenshotPath($interval));
|
||||
Storage::delete($this->getThumbPath($interval));
|
||||
}
|
||||
|
||||
public static function getFullPath(): string
|
||||
{
|
||||
$fileSystemPath = config('filesystems.default');
|
||||
return storage_path(config("filesystems.disks.$fileSystemPath.root"));
|
||||
}
|
||||
}
|
||||
41
app/Contracts/SettingsProvider.php
Normal file
41
app/Contracts/SettingsProvider.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
interface SettingsProvider
|
||||
{
|
||||
/**
|
||||
* Get all module settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function all(): array;
|
||||
|
||||
/**
|
||||
* Get the settings value by key
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed|null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* Set the setting value
|
||||
* A key value array can be passed as key
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed|null $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set(string $key, mixed $value = null): void;
|
||||
|
||||
/**
|
||||
* Flushes module settings storage
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flush(): void;
|
||||
}
|
||||
206
app/Docs/auth.php
Normal file
206
app/Docs/auth.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
/**
|
||||
* @apiDeprecated since 1.0.0 use now (#Password_Reset:Process)
|
||||
* @api {post} /api/auth/reset Reset
|
||||
* @apiDescription Get user JWT
|
||||
*
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Reset
|
||||
* @apiGroup Auth
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 1.0.0 use now (#Password_Reset:Request)
|
||||
* @api {post} /api/auth/send-reset Send reset e-mail
|
||||
* @apiDescription Get user JWT
|
||||
*
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Send reset
|
||||
* @apiGroup Auth
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {post} /auth/refresh Refresh
|
||||
* @apiDescription Refreshes JWT
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Refresh
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {String} access_token Token
|
||||
* @apiSuccess {String} token_type Token Type
|
||||
* @apiSuccess {String} expires_in Token TTL 8601String Date
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @apiDefine ParamsValidationError
|
||||
* @apiErrorExample {json} Params validation
|
||||
* HTTP/1.1 400 Bad Request
|
||||
* {
|
||||
* "message": "Invalid params",
|
||||
* "error_type": "authorization.wrong_params"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @api {post} /auth/logout Logout
|
||||
* @apiDescription Invalidate JWT
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Logout
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {String} message Message from server
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "message": "Successfully logged out"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @api {post} /auth/logout-from-all Logout from all
|
||||
* @apiDescription Invalidate all user JWT
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Logout all
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {String} message Message from server
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "message": "Successfully logged out from all sessions"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @api {get} /auth/me Me
|
||||
* @apiDescription Get authenticated User Entity
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Me
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {Object} user User Entity
|
||||
*
|
||||
* @apiUse UserObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "id": 1,
|
||||
* "full_name": "Admin",
|
||||
* "email": "admin@example.com",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_state": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": 0,
|
||||
* "web_and_app_monitoring": 1,
|
||||
* "screenshots_interval": 9,
|
||||
* "active": "active",
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2018-09-25 06:15:08",
|
||||
* "updated_at": "2018-09-25 06:15:08",
|
||||
* "timezone": null
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @api {get} /auth/desktop-key Issue key
|
||||
* @apiDescription Issues key for desktop auth
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Issue key
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {String} access_token Token
|
||||
* @apiSuccess {String} token_type Token Type
|
||||
* @apiSuccess {String} expires_in Token TTL 8601String Date
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "access_token": "r6nPiGocAWdD5ZF60dTkUboVAWSXsUScpp7m9x9R",
|
||||
* "token_type": "desktop",
|
||||
* "expires_in": "2020-12-26T14:18:32+00:00"
|
||||
* }
|
||||
*
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @api {post} /auth/login Login
|
||||
* @apiDescription Get user JWT
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Login
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiParam {String} email User email
|
||||
* @apiParam {String} password User password
|
||||
* @apiParam {String} [recaptcha] Recaptcha token
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "email": "johndoe@example.com",
|
||||
* "password": "amazingpassword",
|
||||
* "recaptcha": "03AOLTBLR5UtIoenazYWjaZ4AFZiv1OWegWV..."
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} access_token Token
|
||||
* @apiSuccess {String} token_type Token Type
|
||||
* @apiSuccess {ISO8601} expires_in Token TTL
|
||||
* @apiSuccess {Object} user User Entity
|
||||
*
|
||||
* @apiUse UserObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "access_token": "16184cf3b2510464a53c0e573c75740540fe...",
|
||||
* "token_type": "bearer",
|
||||
* "expires_in": "2020-12-26T14:18:32+00:00",
|
||||
* "user": {}
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ParamsValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse UserDeactivatedError
|
||||
* @apiUse CaptchaError
|
||||
* @apiUse LimiterError
|
||||
*/
|
||||
272
app/Docs/screenshots.php
Normal file
272
app/Docs/screenshots.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {get} /screenshot/:id Screenshot
|
||||
* @apiDescription Get Screenshot
|
||||
*
|
||||
* @apiVersion 3.5.0
|
||||
* @apiName Show
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission time_intervals_show
|
||||
* @apiPermission time_intervals_full_access
|
||||
*
|
||||
* @apiParam {Integer} id ID of the target Time interval
|
||||
*
|
||||
* @apiSuccess {Raw} data Screenshot data
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ItemNotFoundError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {get} /screenshot/:id Thumb
|
||||
* @apiDescription Get Screenshot Thumbnail
|
||||
*
|
||||
* @apiVersion 3.5.0
|
||||
* @apiName ShowThumb
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission time_intervals_show
|
||||
* @apiPermission time_intervals_full_access
|
||||
*
|
||||
* @apiParam {Integer} id ID of the target Time interval
|
||||
*
|
||||
* @apiSuccess {Raw} data Screenshot data
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ItemNotFoundError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {get,post} /screenshots/list List
|
||||
* @apiDescription Get list of Screenshots
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName List
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission screenshots_list
|
||||
* @apiPermission screenshots_full_access
|
||||
*
|
||||
* @apiUse UserParams
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": [">", 1],
|
||||
* "time_interval_id": ["=", [1,2,3]],
|
||||
* "user_id": ["=", [1,2,3]],
|
||||
* "project_id": ["=", [1,2,3]],
|
||||
* "path": ["like", "%lorem%"],
|
||||
* "created_at": [">", "2019-01-01 00:00:00"],
|
||||
* "updated_at": ["<", "2019-01-01 00:00:00"]
|
||||
* }
|
||||
*
|
||||
* @apiUse UserObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "time_interval_id": 1,
|
||||
* "path": "uploads\/screenshots\/1_1_1.png",
|
||||
* "created_at": "2020-01-23T09:42:26+00:00",
|
||||
* "updated_at": "2020-01-23T09:42:26+00:00",
|
||||
* "deleted_at": null,
|
||||
* "thumbnail_path": null,
|
||||
* "important": false,
|
||||
* "is_removed": false
|
||||
* },
|
||||
* {
|
||||
* "id": 2,
|
||||
* "time_interval_id": 2,
|
||||
* "path": "uploads\/screenshots\/1_1_2.png",
|
||||
* "created_at": "2020-01-23T09:42:26+00:00",
|
||||
* "updated_at": "2020-01-23T09:42:26+00:00",
|
||||
* "deleted_at": null,
|
||||
* "thumbnail_path": null,
|
||||
* "important": false,
|
||||
* "is_removed": false
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {post} /screenshots/create Create
|
||||
* @apiDescription Create Screenshot
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Create
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiParam {Integer} time_interval_id Time Interval ID
|
||||
* @apiParam {Binary} screenshot File
|
||||
*
|
||||
* @apiParamExample {json} Simple-Request Example
|
||||
* {
|
||||
* "time_interval_id": 1,
|
||||
* "screenshot": <binary data>
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} res User
|
||||
*
|
||||
* @apiUse ScreenshotObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "res": {
|
||||
* "id": 1,
|
||||
* "time_interval_id": 1,
|
||||
* "path": "uploads\/screenshots\/1_1_1.png",
|
||||
* "created_at": "2020-01-23T09:42:26+00:00",
|
||||
* "updated_at": "2020-01-23T09:42:26+00:00",
|
||||
* "deleted_at": null,
|
||||
* "thumbnail_path": null,
|
||||
* "important": false,
|
||||
* "is_removed": false
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {post} /screenshots/remove Destroy
|
||||
* @apiDescription Destroy Screenshot
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Destroy
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission screenshots_remove
|
||||
* @apiPermission screenshots_full_access
|
||||
*
|
||||
* @apiParam {Integer} id ID of the target screenshot
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message Destroy status
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "message": "Item has been removed"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 1.0.0
|
||||
* @api {post} /screenshots/bulk-create Bulk Create
|
||||
* @apiDescription Create Screenshot
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Bulk Create
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiPermission screenshots_bulk_create
|
||||
* @apiPermission screenshots_full_access
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {get,post} /screenshot/count Count
|
||||
* @apiDescription Count Screenshots
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Count
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {String} total Amount of projects that we have
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "total": 2
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {post} /screenshots/show Show
|
||||
* @apiDescription Show Screenshot
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Show
|
||||
* @apiGroup Screenshot
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission screenshots_show
|
||||
* @apiPermission screenshots_full_access
|
||||
*
|
||||
* @apiParam {Integer} id ID
|
||||
*
|
||||
* @apiUse ScreenshotParams
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1,
|
||||
* "time_interval_id": ["=", [1,2,3]],
|
||||
* "path": ["like", "%lorem%"],
|
||||
* "created_at": [">", "2019-01-01 00:00:00"],
|
||||
* "updated_at": ["<", "2019-01-01 00:00:00"]
|
||||
* }
|
||||
*
|
||||
* @apiUse ScreenshotObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "time_interval_id": 1,
|
||||
* "path": "uploads\/screenshots\/1_1_1.png",
|
||||
* "created_at": "2020-01-23T09:42:26+00:00",
|
||||
* "updated_at": "2020-01-23T09:42:26+00:00",
|
||||
* "deleted_at": null,
|
||||
* "thumbnail_path": null,
|
||||
* "important": false,
|
||||
* "is_removed": false
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
10
app/Enums/ActivityType.php
Normal file
10
app/Enums/ActivityType.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ActivityType: string
|
||||
{
|
||||
case ALL = 'all';
|
||||
case COMMENTS = 'comments';
|
||||
case HISTORY = 'history';
|
||||
}
|
||||
26
app/Enums/AttachmentStatus.php
Normal file
26
app/Enums/AttachmentStatus.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum AttachmentStatus: int
|
||||
{
|
||||
/**
|
||||
* file just uploaded, attachmentable not set yet, not calculating hash
|
||||
*/
|
||||
case NOT_ATTACHED = 0;
|
||||
|
||||
/**
|
||||
* moving file to correct project folder and then calculating hash
|
||||
*/
|
||||
case PROCESSING = 1;
|
||||
|
||||
/**
|
||||
* file hash matched (on cron check and initial upload)
|
||||
*/
|
||||
case GOOD = 2;
|
||||
|
||||
/**
|
||||
* file hash NOT matched (on cron check)
|
||||
*/
|
||||
case BAD = 3;
|
||||
}
|
||||
9
app/Enums/DashboardSortBy.php
Normal file
9
app/Enums/DashboardSortBy.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum DashboardSortBy: string
|
||||
{
|
||||
case USER_NAME = 'user';
|
||||
case WORKED = 'worked';
|
||||
}
|
||||
13
app/Enums/Role.php
Normal file
13
app/Enums/Role.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum Role: int
|
||||
{
|
||||
case ANY = -1;
|
||||
|
||||
case ADMIN = 0;
|
||||
case MANAGER = 1;
|
||||
case USER = 2;
|
||||
case AUDITOR = 3;
|
||||
}
|
||||
75
app/Enums/ScreenshotsState.php
Normal file
75
app/Enums/ScreenshotsState.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Settings;
|
||||
|
||||
enum ScreenshotsState: int
|
||||
{
|
||||
case ANY = -1;
|
||||
case FORBIDDEN = 0;
|
||||
case REQUIRED = 1;
|
||||
case OPTIONAL = 2;
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return strtolower($this->name);
|
||||
}
|
||||
|
||||
public function mustBeInherited(): bool
|
||||
{
|
||||
return $this === self::FORBIDDEN || $this === self::REQUIRED;
|
||||
}
|
||||
|
||||
public static function tryFromString(string $value): ?ScreenshotsState
|
||||
{
|
||||
try {
|
||||
return constant(__CLASS__ . "::" . strtoupper($value));
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'value' => $this->value,
|
||||
'name' => $this->title(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function states(): array
|
||||
{
|
||||
return array_map(fn ($case) => $case->toArray(), self::cases());
|
||||
}
|
||||
|
||||
public static function createFrom(null|int|string|ScreenshotsState $value): ?ScreenshotsState
|
||||
{
|
||||
return match (true) {
|
||||
!isset($value) => null,
|
||||
is_numeric($value) => static::tryFrom((int)$value),
|
||||
is_string($value) => static::tryFromString($value),
|
||||
$value instanceof ScreenshotsState => static::tryFrom($value->value),
|
||||
default => static::tryFrom($value),
|
||||
};
|
||||
}
|
||||
|
||||
public static function withGlobalOverrides(null|int|string|ScreenshotsState $value): ?ScreenshotsState
|
||||
{
|
||||
foreach ([
|
||||
ScreenshotsState::createFrom(config('app.screenshots_state')),
|
||||
ScreenshotsState::createFrom(Settings::scope('core')->get('screenshots_state')),
|
||||
] as $globalOverride) {
|
||||
if (isset($globalOverride) && $globalOverride->mustBeInherited()) {
|
||||
return $globalOverride;
|
||||
}
|
||||
}
|
||||
|
||||
return static::createFrom($value);
|
||||
}
|
||||
|
||||
public static function getNormalizedValue(null|int|string|ScreenshotsState $value): ?int
|
||||
{
|
||||
return static::createFrom($value)?->value;
|
||||
}
|
||||
}
|
||||
9
app/Enums/SortDirection.php
Normal file
9
app/Enums/SortDirection.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SortDirection: string
|
||||
{
|
||||
case ASC = 'asc';
|
||||
case DESC = 'desc';
|
||||
}
|
||||
9
app/Enums/TaskRelationType.php
Normal file
9
app/Enums/TaskRelationType.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TaskRelationType: string
|
||||
{
|
||||
case FOLLOWS = 'follows';
|
||||
case PRECEDES = 'precedes';
|
||||
}
|
||||
184
app/Enums/UniversalReportBase.php
Normal file
184
app/Enums/UniversalReportBase.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
|
||||
enum UniversalReportBase: string
|
||||
{
|
||||
case PROJECT = 'project';
|
||||
case USER = 'user';
|
||||
case TASK = 'task';
|
||||
|
||||
|
||||
public function fields() {
|
||||
return match($this) {
|
||||
self::PROJECT => [
|
||||
'base' => [
|
||||
'name',
|
||||
'created_at',
|
||||
'description',
|
||||
'important',
|
||||
],
|
||||
'tasks' => [
|
||||
'task_name',
|
||||
'priority',
|
||||
'status',
|
||||
'due_date',
|
||||
'estimate',
|
||||
'description',
|
||||
],
|
||||
'users' => [
|
||||
'full_name',
|
||||
'email',
|
||||
],
|
||||
// 'work_time',
|
||||
// 'work_time_user',
|
||||
'calculations' => [
|
||||
'total_spent_time',
|
||||
'total_spent_time_by_user',
|
||||
'total_spent_time_by_day',
|
||||
'total_spent_time_by_day_and_user',
|
||||
],
|
||||
|
||||
],
|
||||
self::USER => [
|
||||
'base' => [
|
||||
'full_name',
|
||||
'email',
|
||||
],
|
||||
'projects' => [
|
||||
'name',
|
||||
'created_at',
|
||||
'description',
|
||||
'important',
|
||||
],
|
||||
'tasks' => [
|
||||
'task_name',
|
||||
'priority',
|
||||
'status',
|
||||
'due_date',
|
||||
'estimate',
|
||||
'description',
|
||||
],
|
||||
'calculations' => [
|
||||
'total_spent_time',
|
||||
'total_spent_time_by_day',
|
||||
]
|
||||
],
|
||||
self::TASK => [
|
||||
'base' => [
|
||||
'task_name',
|
||||
'priority',
|
||||
'status',
|
||||
'due_date',
|
||||
'estimate',
|
||||
'description',
|
||||
// 'workers',
|
||||
],
|
||||
'users' => [
|
||||
'full_name',
|
||||
'email',
|
||||
],
|
||||
'projects' => [
|
||||
'name',
|
||||
'created_at',
|
||||
'description',
|
||||
'important',
|
||||
],
|
||||
'calculations' => [
|
||||
'total_spent_time',
|
||||
'total_spent_time_by_day',
|
||||
'total_spent_time_by_user',
|
||||
'total_spent_time_by_day_and_user'
|
||||
],
|
||||
// 'total_spent_time',
|
||||
// 'user_name',
|
||||
// 'user_time',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public function dataObjects()
|
||||
{
|
||||
return match($this) {
|
||||
self::PROJECT => (function() {
|
||||
if(request()->user()->isAdmin()) {
|
||||
return Project::select('id', 'name')->get();
|
||||
}
|
||||
|
||||
return request()->user()->projects()->select('id', 'name')->get();
|
||||
})(),
|
||||
self::USER => (function() {
|
||||
if (request()->user()->isAdmin()) {
|
||||
return User::select('id', 'full_name as name', 'email', 'full_name')->get();
|
||||
}
|
||||
|
||||
return;
|
||||
})(),
|
||||
self::TASK => (function() {
|
||||
if(request()->user()->isAdmin()) {
|
||||
return Task::select('id', 'task_name as name')->get();
|
||||
}
|
||||
|
||||
return request()->user()->tasks()->select('id', 'name')->get();
|
||||
})()
|
||||
};
|
||||
}
|
||||
public function charts() {
|
||||
// [
|
||||
// // Project
|
||||
// "worked_all_users",// "Отработанное время всеми пользователями на проекте за указанный период",
|
||||
// "worked_all_users_separately",// "Отработанное время каждым пользователем на проекте за указанный период",
|
||||
// // Task
|
||||
// 'worked_all_users',// "Отработанное время всеми пользователями на задаче за указанный период",
|
||||
// 'worked_all_users_separately',// "Отработанное время каждым пользователем на задаче за указанный период",
|
||||
// // User
|
||||
// 'total_hours',// "Всего часов за указанный период",
|
||||
// 'hours_tasks',// "Часов на каждой задаче",
|
||||
// 'hours_projects',// "Часов на каждом проекте",
|
||||
|
||||
// ];
|
||||
return match($this) {
|
||||
self::PROJECT => [
|
||||
'total_spent_time_day',
|
||||
'total_spent_time_day_and_users_separately',
|
||||
],
|
||||
self::USER => [
|
||||
'total_spent_time_day',
|
||||
'total_spent_time_day_and_tasks',
|
||||
'total_spent_time_day_and_projects',
|
||||
],
|
||||
self::TASK => [
|
||||
'total_spent_time_day',
|
||||
'total_spent_time_day_users_separately',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public static function bases()
|
||||
{
|
||||
return array_map(fn($case) => $case->value, self::cases());
|
||||
}
|
||||
|
||||
public function checkAccess(array $data_objects)
|
||||
{
|
||||
$user = request()->user();
|
||||
return match($this) {
|
||||
self::PROJECT => $user->projects()->select('id')->whereIn('id', $data_objects)->withoutGlobalScopes()->get()->count() === count($data_objects),
|
||||
self::USER => '',
|
||||
self::TASK => $user->tasks()->select('id')->whereIn('id', $data_objects)->withoutGlobalScopes()->get()->count() === count($data_objects),
|
||||
};
|
||||
}
|
||||
|
||||
public function model()
|
||||
{
|
||||
return match($this) {
|
||||
self::PROJECT => new Project(),
|
||||
self::USER => new User(),
|
||||
self::TASK => new Task(),
|
||||
};
|
||||
}
|
||||
}
|
||||
9
app/Enums/UniversalReportType.php
Normal file
9
app/Enums/UniversalReportType.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UniversalReportType: string
|
||||
{
|
||||
case COMPANY = 'company';
|
||||
case PERSONAL = 'personal';
|
||||
}
|
||||
163
app/Events/ChangeEvent.php
Normal file
163
app/Events/ChangeEvent.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskComment;
|
||||
use App\Models\TimeInterval;
|
||||
use App\Models\User;
|
||||
use App\Reports\DashboardExport;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Settings;
|
||||
use Staudenmeir\LaravelAdjacencyList\Eloquent\Builder as AdjacencyListBuilder;
|
||||
|
||||
class ChangeEvent implements ShouldBroadcast, ShouldDispatchAfterCommit
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/** @param Task|Project|TimeInterval $model */
|
||||
public function __construct(
|
||||
protected string $entityType,
|
||||
protected string $action,
|
||||
protected $model,
|
||||
protected int $userId
|
||||
) {
|
||||
}
|
||||
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return sprintf('%s.%s', $this->entityType, $this->action);
|
||||
}
|
||||
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
// TODO: [ ] optimize loaded changes - payload too big
|
||||
$model = match (true) {
|
||||
$this->model instanceof Project && $this->entityType === 'gantt' => $this
|
||||
->model
|
||||
->setPermissionsUser(User::query()->find($this->userId))
|
||||
->load([
|
||||
'tasks' => fn(HasMany $queue) => $queue
|
||||
->orderBy('start_date')
|
||||
->select([
|
||||
'id',
|
||||
'task_name',
|
||||
'priority_id',
|
||||
'status_id',
|
||||
'estimate',
|
||||
'start_date',
|
||||
'due_date',
|
||||
'project_phase_id',
|
||||
'project_id'
|
||||
])->with(['status', 'priority'])
|
||||
->withSum(['workers as total_spent_time'], 'duration')
|
||||
->withSum(['workers as total_offset'], 'offset')
|
||||
->withCasts(['start_date' => 'string', 'due_date' => 'string'])
|
||||
->whereNotNull('start_date')->whereNotNull('due_date'),
|
||||
'phases' => fn(HasMany $queue) => $queue
|
||||
->select(['id', 'name', 'project_id'])
|
||||
->withMin([
|
||||
'tasks as start_date' => fn(AdjacencyListBuilder $q) => $q
|
||||
->whereNotNull('start_date')
|
||||
->whereNotNull('due_date')
|
||||
], 'start_date')
|
||||
->withMax([
|
||||
'tasks as due_date' => fn(AdjacencyListBuilder $q) => $q
|
||||
->whereNotNull('start_date')
|
||||
->whereNotNull('due_date')
|
||||
], 'due_date'),
|
||||
])
|
||||
->append('tasks_relations'),
|
||||
$this->model instanceof Task => $this->model->setPermissionsUser(User::query()->find($this->userId))
|
||||
->load([
|
||||
'priority',
|
||||
'project',
|
||||
'users',
|
||||
'status',
|
||||
'parents',
|
||||
'children',
|
||||
'phase:id,name',
|
||||
'workers',
|
||||
'workers.user:id,full_name',
|
||||
'attachmentsRelation',
|
||||
'attachmentsRelation.user:id,full_name',
|
||||
])
|
||||
->append(['can'])
|
||||
->loadSum('workers as total_spent_time', 'duration')
|
||||
->loadSum('workers as total_offset', 'offset')
|
||||
->makeVisible('can'),
|
||||
$this->model instanceof TaskComment => $this->model
|
||||
->load(
|
||||
'user',
|
||||
'attachmentsRelation',
|
||||
'attachmentsRelation.user:id,full_name'
|
||||
),
|
||||
$this->model instanceof Project => $this->model->setPermissionsUser(User::query()->find($this->userId))
|
||||
->load([
|
||||
'users',
|
||||
'defaultPriority',
|
||||
'statuses',
|
||||
'workers',
|
||||
'workers.user:id,full_name',
|
||||
'workers.task:id,task_name',
|
||||
'phases' => fn($q) => $q->withCount('tasks'),
|
||||
'group',
|
||||
])
|
||||
->loadCount('tasks')
|
||||
->append(['can'])
|
||||
->loadSum('workers as total_spent_time', 'duration')
|
||||
->loadSum('workers as total_offset', 'offset')
|
||||
->makeVisible('can'),
|
||||
// Format a time interval as in the dashboard report
|
||||
$this->model instanceof TimeInterval => DashboardExport::init(
|
||||
[$this->model->user_id],
|
||||
[$this->model->task->project_id],
|
||||
Carbon::parse($this->model->start_at)->startOfDay(),
|
||||
Carbon::parse($this->model->end_at)->endOfDay(),
|
||||
Settings::scope('core')->get('timezone', 'UTC'),
|
||||
$this->model->user->timezone ?? Settings::scope('core')->get('timezone', 'UTC'),
|
||||
)->collection(['time_intervals.id' => $this->model->id])->first()->first()->toArray(),
|
||||
default => $this->model,
|
||||
};
|
||||
|
||||
return ['model' => $model];
|
||||
}
|
||||
|
||||
/** @return Channel[] */
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [new PrivateChannel(sprintf('%s.%s', $this->entityType, $this->userId))];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Task|Project|TimeInterval $model
|
||||
* @return int[]
|
||||
*/
|
||||
public static function getRelatedUserIds($model): array
|
||||
{
|
||||
$userRelation = match (true) {
|
||||
$model instanceof Task => 'tasks',
|
||||
$model instanceof Project => 'projects',
|
||||
$model instanceof TimeInterval => 'timeIntervals',
|
||||
default => null,
|
||||
};
|
||||
|
||||
$query = User::query()->where('role_id', Role::ADMIN->value)->orWhere('role_id', Role::MANAGER->value);
|
||||
if (isset($userRelation)) {
|
||||
$query = $query->orWhereHas($userRelation, fn(Builder $builder) => $builder->where('id', $model->id));
|
||||
}
|
||||
|
||||
return $query->pluck('id')->toArray();
|
||||
}
|
||||
}
|
||||
12
app/Events/InvitationCreated.php
Normal file
12
app/Events/InvitationCreated.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Invitation;
|
||||
|
||||
class InvitationCreated
|
||||
{
|
||||
public function __construct(public Invitation $invitation)
|
||||
{
|
||||
}
|
||||
}
|
||||
14
app/Exceptions/Entities/AppAlreadyInstalledException.php
Normal file
14
app/Exceptions/Entities/AppAlreadyInstalledException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class AppAlreadyInstalledException extends HttpException
|
||||
{
|
||||
protected $status = 400;
|
||||
|
||||
protected $errorCode = 'app.installation';
|
||||
|
||||
protected $message = 'App has been already installed';
|
||||
}
|
||||
172
app/Exceptions/Entities/AuthorizationException.php
Normal file
172
app/Exceptions/Entities/AuthorizationException.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class AuthorizationException extends HttpException
|
||||
{
|
||||
/**
|
||||
* @apiDefine 400Error
|
||||
* @apiError (Error 4xx) {String} message Message from server
|
||||
* @apiError (Error 4xx) {Boolean} success Indicates erroneous response when `FALSE`
|
||||
* @apiError (Error 4xx) {String} error_type Error type
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine UnauthorizedError
|
||||
* @apiErrorExample {json} Unauthorized
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "message": "Not authorized",
|
||||
* "error_type": "authorization.unauthorized"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_UNAUTHORIZED = 'authorization.unauthorized';
|
||||
|
||||
/**
|
||||
* @apiDefine CaptchaError
|
||||
* @apiError (Error 429) {Object} info Additional info from server
|
||||
* @apiError (Error 429) {String} info.site_key Public site key for rendering reCaptcha
|
||||
*
|
||||
* @apiErrorExample {json} Captcha
|
||||
* HTTP/1.1 429 Too Many Requests
|
||||
* {
|
||||
* "message": "Invalid captcha",
|
||||
* "error_type": "authorization.captcha"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_CAPTCHA = 'authorization.captcha';
|
||||
|
||||
/**
|
||||
* @apiDefine LimiterError
|
||||
* @apiErrorExample {json} Limiter
|
||||
* HTTP/1.1 423 Locked
|
||||
* {
|
||||
* "message": "Enhance Your Calm",
|
||||
* "error_type": "authorization.banned_enhance_your_calm"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_BANNED = 'authorization.banned';
|
||||
|
||||
/**
|
||||
* @apiDefine TokenMismatchError
|
||||
* @apiErrorExample {json} Token mismatch
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "message": "Token mismatch",
|
||||
* "error_type": "authorization.token_mismatch"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_TOKEN_MISMATCH = 'authorization.token_mismatch';
|
||||
|
||||
/**
|
||||
* @apiDefine TokenExpiredError
|
||||
* @apiErrorExample {json} Token expired
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "message": "Token expired",
|
||||
* "error_type": "authorization.token_expired"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_TOKEN_EXPIRED = 'authorization.token_expired';
|
||||
|
||||
/**
|
||||
* @apiDefine UserDeactivatedError
|
||||
* @apiErrorExample {json} User deactivated
|
||||
* HTTP/1.1 403 Forbidden
|
||||
* {
|
||||
* "message": "User deactivated",
|
||||
* "error_type": "authorization.user_disabled"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_USER_DISABLED = 'authorization.user_disabled';
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @apiDefine ParamsValidationError
|
||||
* @apiErrorExample {json} Params validation
|
||||
* HTTP/1.1 400 Bad Request
|
||||
* {
|
||||
* "message": "Invalid params",
|
||||
* "error_type": "authorization.wrong_params"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_VALIDATION_FAILED = 'authorization.wrong_params';
|
||||
|
||||
/**
|
||||
* @apiDefine NoSuchUserError
|
||||
* @apiErrorExample {json} No such user
|
||||
* HTTP/1.1 404 Not Found
|
||||
* {
|
||||
* "message": "User with such email isn’t found",
|
||||
* "error_type": "authorization.user_not_found"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_USER_NOT_FOUND = 'authorization.user_not_found';
|
||||
|
||||
/**
|
||||
* @apiDefine InvalidPasswordResetDataError
|
||||
* @apiErrorExample {json} Invalid password reset data
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "message": "Invalid password reset data",
|
||||
* "error_type": "authorization.invalid_password_data"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_INVALID_PASSWORD_RESET_DATA = 'authorization.invalid_password_data';
|
||||
|
||||
/**
|
||||
* @apiDefine ForbiddenError
|
||||
* @apiErrorExample {json} Forbidden
|
||||
* HTTP/1.1 403 Forbidden
|
||||
* {
|
||||
* "message": "Access denied to this item",
|
||||
* "error_type": "authorization.forbidden"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
public const ERROR_TYPE_FORBIDDEN = 'authorization.forbidden';
|
||||
|
||||
protected const ERRORS = [
|
||||
self::ERROR_TYPE_UNAUTHORIZED => ['code' => 401, 'message' => 'Not authorized'],
|
||||
self::ERROR_TYPE_CAPTCHA => ['code' => 429, 'message' => 'Invalid captcha',],
|
||||
self::ERROR_TYPE_BANNED => ['code' => 423, 'message' => 'Enhance Your Calm'],
|
||||
self::ERROR_TYPE_TOKEN_MISMATCH => ['code' => 401, 'message' => 'Token mismatch'],
|
||||
self::ERROR_TYPE_TOKEN_EXPIRED => ['code' => 401, 'message' => 'Token expired'],
|
||||
self::ERROR_TYPE_USER_DISABLED => ['code' => 403, 'message' => 'User deactivated'],
|
||||
self::ERROR_TYPE_VALIDATION_FAILED => ['code' => 400, 'message' => 'Invalid params'],
|
||||
self::ERROR_TYPE_USER_NOT_FOUND => ['code' => 404, 'message' => 'User with such email isn’t found'],
|
||||
self::ERROR_TYPE_INVALID_PASSWORD_RESET_DATA => ['code' => 401, 'message' => 'Invalid password reset data'],
|
||||
self::ERROR_TYPE_FORBIDDEN => ['code' => 403, 'message' => 'This action is unauthorized']
|
||||
];
|
||||
|
||||
public function __construct($type = self::ERROR_TYPE_UNAUTHORIZED)
|
||||
{
|
||||
$this->errorCode = $type;
|
||||
$this->status = self::ERRORS[$type]['code'];
|
||||
|
||||
parent::__construct(self::ERRORS[$type]['message']);
|
||||
}
|
||||
}
|
||||
23
app/Exceptions/Entities/DeprecatedApiException.php
Normal file
23
app/Exceptions/Entities/DeprecatedApiException.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class DeprecatedApiException extends HttpException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$lastCalledMethod = $this->getTrace()[0];
|
||||
$deprecatedMethod = "{$lastCalledMethod['class']}@{$lastCalledMethod['function']}";
|
||||
|
||||
\Log::warning("Deprecated method {$deprecatedMethod} called, update Cattr client", [
|
||||
'user_id' => auth()->user()->id ?? null
|
||||
]);
|
||||
|
||||
$this->errorCode = 'deprecation.api';
|
||||
$this->status = 400;
|
||||
|
||||
parent::__construct("Deprecated method {$deprecatedMethod} called, update Cattr client");
|
||||
}
|
||||
}
|
||||
14
app/Exceptions/Entities/InstallationException.php
Normal file
14
app/Exceptions/Entities/InstallationException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class InstallationException extends HttpException
|
||||
{
|
||||
protected $status = 400;
|
||||
|
||||
protected $errorCode = 'app.installation';
|
||||
|
||||
protected $message = 'You need to run installation';
|
||||
}
|
||||
11
app/Exceptions/Entities/IntervalAlreadyDeletedException.php
Normal file
11
app/Exceptions/Entities/IntervalAlreadyDeletedException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class IntervalAlreadyDeletedException extends HttpException
|
||||
{
|
||||
protected $errorCode = 'interval_already_deleted';
|
||||
protected $status = 409;
|
||||
}
|
||||
12
app/Exceptions/Entities/InvalidMainException.php
Normal file
12
app/Exceptions/Entities/InvalidMainException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class InvalidMainException extends HttpException
|
||||
{
|
||||
protected $status = 422;
|
||||
|
||||
protected $message = 'Base mistranslation detected';
|
||||
}
|
||||
12
app/Exceptions/Entities/MethodNotAllowedException.php
Normal file
12
app/Exceptions/Entities/MethodNotAllowedException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class MethodNotAllowedException extends HttpException
|
||||
{
|
||||
protected $status = 405;
|
||||
|
||||
protected $errorCode = 'http.request.wrong_method';
|
||||
}
|
||||
15
app/Exceptions/Entities/NotEnoughRightsException.php
Normal file
15
app/Exceptions/Entities/NotEnoughRightsException.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class NotEnoughRightsException extends HttpException
|
||||
{
|
||||
public function __construct(string $message = 'Not enoughs rights')
|
||||
{
|
||||
$this->status = 403;
|
||||
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
40
app/Exceptions/Entities/TaskRelationException.php
Normal file
40
app/Exceptions/Entities/TaskRelationException.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Entities;
|
||||
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
|
||||
class TaskRelationException extends HttpException
|
||||
{
|
||||
public const NOT_SAME_PROJECT = 'task_relation.not_same_project';
|
||||
public const CYCLIC = 'task_relation.cyclic';
|
||||
public const ALREADY_EXISTS = 'task_relation.already_exists';
|
||||
public const CANNOT_START_BEFORE_PARENT_ENDS = 'task_relation.cannot_start_before_parent_ends';
|
||||
|
||||
|
||||
public function __construct($type)
|
||||
{
|
||||
$ERRORS = [
|
||||
self::NOT_SAME_PROJECT => [
|
||||
'code' => 409,
|
||||
'message' => __("validation.tasks-relations.must_have_same_project")
|
||||
],
|
||||
self::CYCLIC => [
|
||||
'code' => 409,
|
||||
'message' => __("validation.tasks-relations.cyclic_relation_detected")
|
||||
],
|
||||
self::ALREADY_EXISTS => [
|
||||
'code' => 409,
|
||||
'message' => __("validation.tasks-relations.already_exists")
|
||||
],
|
||||
self::CANNOT_START_BEFORE_PARENT_ENDS => [
|
||||
'code' => 409,
|
||||
'message' => __("validation.tasks-relations.cannot_start_before_parent_ends")
|
||||
]
|
||||
];
|
||||
$this->errorCode = $type;
|
||||
$this->status = $ERRORS[$type]['code'];
|
||||
|
||||
parent::__construct($ERRORS[$type]['message']);
|
||||
}
|
||||
}
|
||||
132
app/Exceptions/Handler.php
Normal file
132
app/Exceptions/Handler.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Exceptions\Entities\MethodNotAllowedException;
|
||||
use Crypt;
|
||||
use Filter;
|
||||
use Flugg\Responder\Exceptions\ConvertsExceptions;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use PDOException;
|
||||
use Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Flugg\Responder\Exceptions\Http\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class Handler
|
||||
*/
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
use ConvertsExceptions;
|
||||
|
||||
/**
|
||||
* A list of exception types with their corresponding custom log levels.
|
||||
*
|
||||
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
|
||||
*/
|
||||
protected $levels = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array<int, class-string<\Throwable>>
|
||||
*/
|
||||
protected $dontReport
|
||||
= [
|
||||
AuthenticationException::class,
|
||||
AuthorizationException::class,
|
||||
Entities\AuthorizationException::class,
|
||||
HttpException::class,
|
||||
ModelNotFoundException::class,
|
||||
TokenMismatchException::class,
|
||||
ValidationException::class,
|
||||
PDOException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
if (app()->bound('sentry')) {
|
||||
app('sentry')->captureException($e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default context variables for logging.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function context(): array
|
||||
{
|
||||
$traceId = Str::uuid()->toString();
|
||||
try {
|
||||
// Only add trace_id to error response if Filter::getErrorResponseFilterName() method exists
|
||||
Filter::listen(Filter::getErrorResponseFilterName(), static function (array|null $data = []) use ($traceId) {
|
||||
$data['trace_id'] = $traceId;
|
||||
return $data;
|
||||
});
|
||||
} catch (Throwable $exception) {
|
||||
}
|
||||
|
||||
$requestContent = collect(rescue(fn() => request()->all(), [], false))
|
||||
->map(function ($item, string $key) {
|
||||
if (Str::contains($key, ['screenshot', 'password', 'secret', 'token', 'api_key'], true)) {
|
||||
return '***';
|
||||
}
|
||||
return $item;
|
||||
})->toArray();
|
||||
|
||||
if (config('app.debug') === false){
|
||||
try {
|
||||
$requestContent = Crypt::encryptString(json_encode($requestContent, JSON_THROW_ON_ERROR));
|
||||
} catch (Throwable $exception) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return array_merge(parent::context(), [
|
||||
'trace_id' => $traceId,
|
||||
'request_uri' => request()->getRequestUri(),
|
||||
'request_content' => $requestContent
|
||||
]);
|
||||
}
|
||||
|
||||
public function render($request, $e): Response
|
||||
{
|
||||
$this->convert($e, [
|
||||
MethodNotAllowedHttpException::class => fn($e) => throw new MethodNotAllowedException($e->getMessage()),
|
||||
AuthenticationException::class => fn($e
|
||||
) => throw new Entities\AuthorizationException(Entities\AuthorizationException::ERROR_TYPE_UNAUTHORIZED),
|
||||
]);
|
||||
|
||||
$this->convertDefaultException($e);
|
||||
|
||||
if ($e instanceof HttpException) {
|
||||
return $this->renderResponse($e);
|
||||
}
|
||||
|
||||
return responder()->error($e->getCode(), $e->getMessage())->respond();
|
||||
}
|
||||
}
|
||||
16
app/Exceptions/Interfaces/InfoExtendedException.php
Normal file
16
app/Exceptions/Interfaces/InfoExtendedException.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Interfaces;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Interface ReasonableException
|
||||
*/
|
||||
interface InfoExtendedException extends Throwable
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getInfo();
|
||||
}
|
||||
16
app/Exceptions/Interfaces/TypedException.php
Normal file
16
app/Exceptions/Interfaces/TypedException.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Interfaces;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Interface TypedException
|
||||
*/
|
||||
interface TypedException extends Throwable
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string;
|
||||
}
|
||||
33
app/Facades/EventFacade.php
Normal file
33
app/Facades/EventFacade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use Illuminate\Support\Testing\Fakes\EventFake;
|
||||
|
||||
class EventFacade extends Facade
|
||||
{
|
||||
/**
|
||||
* Replace the bound instance with a fake.
|
||||
*
|
||||
* @param array|string $eventsToFake
|
||||
* @return void
|
||||
*/
|
||||
public static function fake(array|string $eventsToFake = []): void
|
||||
{
|
||||
static::swap($fake = new EventFake(static::getFacadeRoot(), $eventsToFake));
|
||||
|
||||
Model::setEventDispatcher($fake);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'catevent';
|
||||
}
|
||||
}
|
||||
42
app/Facades/FilterFacade.php
Normal file
42
app/Facades/FilterFacade.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use Illuminate\Support\Testing\Fakes\EventFake;
|
||||
|
||||
/**
|
||||
* @template T of mixed
|
||||
* @see Dispatcher
|
||||
* @method static subscribe(object|string $subscriber)
|
||||
* @method static listen(string|array $events, mixed $listener)
|
||||
* @method static T process(string $event, T $payload)
|
||||
* @mixin Dispatcher
|
||||
*/
|
||||
class FilterFacade extends Facade
|
||||
{
|
||||
/**
|
||||
* Replace the bound instance with a fake.
|
||||
*
|
||||
* @param array|string $eventsToFake
|
||||
* @return void
|
||||
*/
|
||||
public static function fake(array|string $eventsToFake = []): void
|
||||
{
|
||||
static::swap($fake = new EventFake(static::getFacadeRoot(), $eventsToFake));
|
||||
|
||||
Model::setEventDispatcher($fake);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'filter';
|
||||
}
|
||||
}
|
||||
13
app/Facades/SettingsFacade.php
Normal file
13
app/Facades/SettingsFacade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class SettingsFacade extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'settings';
|
||||
}
|
||||
}
|
||||
67
app/Filters/AttachmentFilter.php
Normal file
67
app/Filters/AttachmentFilter.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use App\Enums\AttachmentStatus;
|
||||
use App\Helpers\AttachmentHelper;
|
||||
use App\Models\Attachment;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AttachmentFilter
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds request rules to attachment parent
|
||||
* @param array $rules
|
||||
* @return array
|
||||
*/
|
||||
public static function addRequestRulesForParent(array $rules): array
|
||||
{
|
||||
$rules['attachmentsRelation'] = 'sometimes|required|array';
|
||||
$rules['attachmentsRelation.*'] = [
|
||||
'sometimes',
|
||||
'required',
|
||||
'uuid',
|
||||
Rule::exists(Attachment::class, 'id')
|
||||
];
|
||||
$rules['attachmentsToRemove'] = 'sometimes|required|array';
|
||||
$rules['attachmentsToRemove.*'] = [
|
||||
'sometimes',
|
||||
'required',
|
||||
'uuid',
|
||||
Rule::exists(Attachment::class, 'id')
|
||||
];
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public static function prepareAttachmentCreateRequest($request) {
|
||||
/**
|
||||
* @var $file UploadedFile
|
||||
*/
|
||||
$file = $request['attachment'];
|
||||
|
||||
$request['user_id'] = auth()->user()->id;
|
||||
$request['status'] = AttachmentStatus::NOT_ATTACHED;
|
||||
$request['original_name'] = AttachmentHelper::getFileName($file);
|
||||
$request['mime_type'] = $file->getMimeType();
|
||||
$request['extension'] = $file->clientExtension();
|
||||
$request['size'] = $file->getSize();
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function subscribe(): array
|
||||
{
|
||||
return array_merge([
|
||||
'filter.request.attachment.create' => [[__CLASS__, 'prepareAttachmentCreateRequest']]
|
||||
], AttachmentHelper::getFilters(
|
||||
addRequestRulesForParent: [[__CLASS__, 'addRequestRulesForParent']]
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
96
app/Helpers/AttachmentHelper.php
Normal file
96
app/Helpers/AttachmentHelper.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskComment;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Str;
|
||||
|
||||
class AttachmentHelper
|
||||
{
|
||||
protected const TYPE_PLACEHOLDER = '__ABLE_TYPE__';
|
||||
|
||||
protected const MAX_FILE_NAME_LENGTH = 255; // name.extension
|
||||
|
||||
public const ABLE_BY = [
|
||||
Task::TYPE => Task::class,
|
||||
TaskComment::TYPE => TaskComment::class
|
||||
];
|
||||
|
||||
public static function isAble(string $able_type): bool
|
||||
{
|
||||
return array_key_exists($able_type, self::ABLE_BY);
|
||||
}
|
||||
|
||||
public static function getAbles(): array
|
||||
{
|
||||
return array_keys(self::ABLE_BY);
|
||||
}
|
||||
|
||||
public static function getFileName(UploadedFile $file): string
|
||||
{
|
||||
$fileName = $file->getClientOriginalName();
|
||||
$extension = ".{$file->clientExtension()}";
|
||||
$maxNameLength = (self::MAX_FILE_NAME_LENGTH - Str::length($extension));
|
||||
if (Str::length($fileName) > $maxNameLength || Str::endsWith($fileName, $extension) === false){
|
||||
$fileName = Str::substr($fileName, 0, $maxNameLength) . $extension;
|
||||
}
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
public static function getEvents($parentCreated, $parentUpdated, $parentDeleted): array
|
||||
{
|
||||
return self::getMappedEvents([
|
||||
'event.after.action.' . self::TYPE_PLACEHOLDER . '.create' => $parentCreated,
|
||||
'event.after.action.' . self::TYPE_PLACEHOLDER . '.edit' => $parentUpdated,
|
||||
'event.after.action.' . self::TYPE_PLACEHOLDER . '.destroy' => $parentDeleted,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getFilters(array $addRequestRulesForParent): array
|
||||
{
|
||||
return self::getMappedEvents([
|
||||
'filter.validation.' . self::TYPE_PLACEHOLDER . '.edit' => $addRequestRulesForParent,
|
||||
'filter.validation.' . self::TYPE_PLACEHOLDER . '.create' => $addRequestRulesForParent,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function getMappedEvents(array $events): array
|
||||
{
|
||||
return array_merge(...array_map(
|
||||
static fn($event, $function) => self::eventMapper($event, $function),
|
||||
array_keys($events),
|
||||
array_values($events)
|
||||
));
|
||||
}
|
||||
|
||||
private static function eventMapper($event, $function): array
|
||||
{
|
||||
return array_reduce(self::getAbles(), static function ($carry, $class) use ($function, $event) {
|
||||
$carry[str_replace(self::TYPE_PLACEHOLDER, $class, $event)] = $function;
|
||||
return $carry;
|
||||
}, []);
|
||||
}
|
||||
|
||||
public static function getMaxAllowedFileSize(): int|string
|
||||
{
|
||||
$size = trim(ini_get("upload_max_filesize") ?? '2M');
|
||||
$last = strtolower($size[strlen($size)-1]);
|
||||
$size = (float)$size;
|
||||
switch($last) {
|
||||
case 'g':
|
||||
$size *= (1024 * 1024 * 1024); //1073741824
|
||||
break;
|
||||
case 'm':
|
||||
$size *= (1024 * 1024); //1048576
|
||||
break;
|
||||
case 'k':
|
||||
$size *= 1024;
|
||||
break;
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
}
|
||||
31
app/Helpers/CatHelper.php
Normal file
31
app/Helpers/CatHelper.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class CatHelper
|
||||
{
|
||||
public const CATS = [
|
||||
'=^._.^=',
|
||||
'(=`ェ´=)',
|
||||
'(=^ ◡ ^=)',
|
||||
'/ᐠ。ꞈ。ᐟ\\',
|
||||
'/ᐠ.ꞈ.ᐟ\\',
|
||||
'✧/ᐠ-ꞈ-ᐟ\\',
|
||||
'(ミචᆽචミ)',
|
||||
'(=චᆽච=)',
|
||||
'(=ㅇᆽㅇ=)',
|
||||
'(=ㅇ༝ㅇ=)',
|
||||
'₍⸍⸌̣ʷ̣̫⸍̣⸌₎',
|
||||
'=^ᵒ⋏ᵒ^=',
|
||||
'( ⓛ ﻌ ⓛ *)',
|
||||
'(=ↀωↀ=)',
|
||||
'(=^・ω・^=)',
|
||||
'(=^・ェ・^=)',
|
||||
'ㅇㅅㅇ'
|
||||
];
|
||||
|
||||
public static function getCat(): string
|
||||
{
|
||||
return self::CATS[array_rand(self::CATS)];
|
||||
}
|
||||
}
|
||||
37
app/Helpers/EnvUpdater.php
Normal file
37
app/Helpers/EnvUpdater.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App;
|
||||
use Config;
|
||||
|
||||
class EnvUpdater
|
||||
{
|
||||
public static function bulkSet(array $values): void
|
||||
{
|
||||
foreach ($values as $key => $value) {
|
||||
self::set($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public static function set(string $key, mixed $value): void
|
||||
{
|
||||
$currentContents = file_get_contents(App::environmentFilePath());
|
||||
|
||||
$keyPosition = strpos($currentContents, "/^{$key}=.*/m");
|
||||
|
||||
if ($keyPosition === false) {
|
||||
$currentContents .= "\n{$key}={$value}";
|
||||
} else {
|
||||
$currentContents = preg_replace(
|
||||
"/^{$key}=.*/m",
|
||||
$key . '=' . $value,
|
||||
$currentContents
|
||||
);
|
||||
}
|
||||
|
||||
file_put_contents(App::environmentFilePath(), $currentContents);
|
||||
|
||||
Config::set($key, $value);
|
||||
}
|
||||
}
|
||||
35
app/Helpers/FakeScreenshotGenerator.php
Normal file
35
app/Helpers/FakeScreenshotGenerator.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Contracts\ScreenshotService;
|
||||
use App\Models\TimeInterval;
|
||||
use Faker\Factory;
|
||||
|
||||
class FakeScreenshotGenerator
|
||||
{
|
||||
private const SCREENSHOT_WIDTH = 1920;
|
||||
private const SCREENSHOT_HEIGHT = 1080;
|
||||
|
||||
public static function runForTimeInterval(TimeInterval|int $timeInterval): void
|
||||
{
|
||||
$service = app()->make(ScreenshotService::class);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'cattr_screenshot');
|
||||
|
||||
$image = imagecreatetruecolor(self::SCREENSHOT_WIDTH, self::SCREENSHOT_HEIGHT);
|
||||
$background = imagecolorallocate($image, random_int(0, 255), random_int(0, 255), random_int(0, 255));
|
||||
|
||||
imagefill($image, 0, 0, $background);
|
||||
|
||||
\imagejpeg($image, $tmpFile);
|
||||
imagedestroy($image);
|
||||
|
||||
$service->saveScreenshot(
|
||||
$tmpFile,
|
||||
$timeInterval
|
||||
);
|
||||
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
150
app/Helpers/FilterDispatcher.php
Normal file
150
app/Helpers/FilterDispatcher.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Events\Dispatcher as LaravelDispatcher;
|
||||
|
||||
class FilterDispatcher extends LaravelDispatcher
|
||||
{
|
||||
public static function getRequestFilterName(): string
|
||||
{
|
||||
return 'filter.request.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getQueryFilterName(): string
|
||||
{
|
||||
return 'filter.query.get.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getQueryAdditionalRelationsFilterName(): string
|
||||
{
|
||||
return 'filter.query.with.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getQueryAdditionalRelationsSumFilterName(): string
|
||||
{
|
||||
return 'filter.query.withSum.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getSuccessResponseFilterName(): string
|
||||
{
|
||||
return 'filter.response.success.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getErrorResponseFilterName(): string
|
||||
{
|
||||
return 'filter.response.error.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getValidationFilterName(): string
|
||||
{
|
||||
return 'filter.validation.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getAuthFilterName(): string
|
||||
{
|
||||
return 'filter.authorize.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getAuthValidationFilterName(): string
|
||||
{
|
||||
return 'filter.authorize.validated' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getBeforeActionEventName(): string
|
||||
{
|
||||
return 'event.before.action.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getAfterActionEventName(): string
|
||||
{
|
||||
return 'event.after.action.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
public static function getActionFilterName(): string
|
||||
{
|
||||
return 'filter.action.' . request()?->route()?->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inerhitDoc
|
||||
* @param $event
|
||||
* @param mixed $payload
|
||||
* @return mixed
|
||||
*/
|
||||
public function process($event, mixed $payload): mixed
|
||||
{
|
||||
return $this->dispatch($event, [$payload]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inerhitDoc
|
||||
* @param object|string $event
|
||||
* @param array $payload
|
||||
* @param bool $halt
|
||||
* @return mixed
|
||||
*/
|
||||
public function dispatch($event, mixed $payload = [], $halt = false): mixed
|
||||
{
|
||||
foreach ($this->getListeners($event) as $listener) {
|
||||
$response = $listener($event, $payload);
|
||||
|
||||
if ($halt && null !== $response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($response === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$payload[0] = $response;
|
||||
}
|
||||
|
||||
return $halt ? null : ($payload[0] ?? null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register an event listener with the dispatcher.
|
||||
*
|
||||
* @param Closure|string $listener
|
||||
* @param bool $wildcard
|
||||
* @return Closure
|
||||
*/
|
||||
public function makeListener($listener, $wildcard = false): callable
|
||||
{
|
||||
if (is_string($listener)) {
|
||||
return $this->createClassListener($listener, $wildcard);
|
||||
}
|
||||
|
||||
return static function ($event, $payload) use ($listener, $wildcard) {
|
||||
if ($wildcard) {
|
||||
return $listener($event, $payload[0]);
|
||||
}
|
||||
|
||||
return $listener($payload[0]);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a class based listener using the IoC container.
|
||||
*
|
||||
* @param string $listener
|
||||
* @param bool $wildcard
|
||||
* @return Closure
|
||||
*/
|
||||
public function createClassListener($listener, $wildcard = false): callable
|
||||
{
|
||||
return function ($event, $payload) use ($listener, $wildcard) {
|
||||
if ($wildcard) {
|
||||
return call_user_func($this->createClassCallable($listener), $event, $payload[0]);
|
||||
}
|
||||
|
||||
return call_user_func_array(
|
||||
$this->createClassCallable($listener),
|
||||
$payload
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
26
app/Helpers/ModuleHelper.php
Normal file
26
app/Helpers/ModuleHelper.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use JsonException;
|
||||
use Module;
|
||||
|
||||
class ModuleHelper
|
||||
{
|
||||
/**
|
||||
* Returns information about installed modules.
|
||||
* @throws JsonException
|
||||
*/
|
||||
public static function getModulesInfo(): array
|
||||
{
|
||||
foreach (Module::all() as $name => $module) {
|
||||
$info[] = [
|
||||
'name' => $name,
|
||||
'version' => (string)(new Version($name)),
|
||||
'enabled' => $module->isEnabled()
|
||||
];
|
||||
}
|
||||
|
||||
return $info ?? [];
|
||||
}
|
||||
}
|
||||
202
app/Helpers/QueryHelper.php
Normal file
202
app/Helpers/QueryHelper.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Exception;
|
||||
use Filter;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Contracts\Database\Query\Builder;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use RuntimeException;
|
||||
|
||||
class QueryHelper
|
||||
{
|
||||
private const RESERVED_REQUEST_KEYWORDS = [
|
||||
'with',
|
||||
'withCount',
|
||||
'paginate',
|
||||
'perPage',
|
||||
'page',
|
||||
'with_deleted',
|
||||
'search',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function apply(Builder $query, Model $model, array $filter = [], bool $first = true): void
|
||||
{
|
||||
$table = $model->getTable();
|
||||
$relations = [];
|
||||
|
||||
if (isset($filter['limit'])) {
|
||||
$query->limit((int)$filter['limit']);
|
||||
unset($filter['limit']);
|
||||
}
|
||||
|
||||
if (isset($filter['offset'])) {
|
||||
$query->offset((int)$filter['offset']);
|
||||
unset($filter['offset']);
|
||||
}
|
||||
|
||||
if (isset($filter['orderBy'])) {
|
||||
$order_by = $filter['orderBy'];
|
||||
[$column, $dir] = isset($order_by[1]) ? array_values($order_by) : [$order_by[0], 'asc'];
|
||||
if (Schema::hasColumn($table, $column)) {
|
||||
$query->orderBy($column, $dir);
|
||||
}
|
||||
|
||||
unset($filter['orderBy']);
|
||||
}
|
||||
|
||||
if (isset($filter['with'])) {
|
||||
$query->with(self::getRelationsFilter($filter['with']));
|
||||
}
|
||||
|
||||
if (isset($filter['withCount'])) {
|
||||
$query->withCount($filter['withCount']);
|
||||
}
|
||||
|
||||
if (isset($filter['withSum'])) {
|
||||
foreach ($filter['withSum'] as $withSum) {
|
||||
$query->withSum(...$withSum);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($filter['search']['query'], $filter['search']['fields'])) {
|
||||
$query = self::buildSearchQuery($query, $filter['search']['query'], $filter['search']['fields']);
|
||||
}
|
||||
|
||||
if (isset($filter['where'])) {
|
||||
foreach ($filter['where'] as $key => $param) {
|
||||
if (str_contains($key, '.')) {
|
||||
$params = explode('.', $key);
|
||||
$domain = array_shift($params);
|
||||
$filterParam = implode('.', $params);
|
||||
|
||||
if (!isset($relations[$domain])) {
|
||||
$relations[$domain] = [];
|
||||
}
|
||||
|
||||
$relations[$domain][$filterParam] = $param;
|
||||
} elseif (Schema::hasColumn($table, $key) &&
|
||||
!in_array($key, static::RESERVED_REQUEST_KEYWORDS, true) &&
|
||||
!in_array($key, $model->getHidden(), true)
|
||||
) {
|
||||
[$operator, $value] = is_array($param) ? array_values($param) : ['=', $param];
|
||||
|
||||
if (is_array($value) && $operator !== 'in') {
|
||||
if ($operator === '=') {
|
||||
$query->whereIn("$table.$key", $value);
|
||||
} elseif ($operator === 'between' && count($value) >= 2) {
|
||||
$query->whereBetween("$table.$key", [$value[0], $value[1]]);
|
||||
}
|
||||
} elseif ($operator === 'in') {
|
||||
$inArgs = is_array($value) ? $value : [$value];
|
||||
$query->whereIn("$table.$key", $inArgs);
|
||||
} else {
|
||||
$query->where("$table.$key", $operator, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($relations)) {
|
||||
foreach ($relations as $domain => $filters) {
|
||||
if (!method_exists($model, $domain)) {
|
||||
$cls = get_class($model);
|
||||
throw new RuntimeException("Unknown relation $cls::$domain()");
|
||||
}
|
||||
|
||||
/** @var Relation $relationQuery */
|
||||
$relationQuery = $model->{$domain}();
|
||||
|
||||
$query->whereHas($domain, static function ($q) use ($filters, $relationQuery, $first) {
|
||||
QueryHelper::apply($q, $relationQuery->getModel(), ['where' => $filters], $first);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function getRelationsFilter(array $filter): array
|
||||
{
|
||||
$key = array_search('can', $filter, true);
|
||||
if ($key !== false) {
|
||||
array_splice($filter, $key, 1);
|
||||
|
||||
Filter::listen(Filter::getActionFilterName(), static function ($data) {
|
||||
if ($data instanceof Model) {
|
||||
$data->append('can');
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ($data instanceof Collection) {
|
||||
return $data->map(static fn(Model $el) => $el->append('can'));
|
||||
}
|
||||
|
||||
if ($data instanceof AbstractPaginator) {
|
||||
$data->setCollection($data->getCollection()->map(static fn(Model $el) => $el->append('can')));
|
||||
return $data;
|
||||
}
|
||||
|
||||
return $data;
|
||||
});
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder $query
|
||||
* @param string $search
|
||||
* @param string[] $fields
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
private static function buildSearchQuery(Builder $query, string $search, array $fields): Builder
|
||||
{
|
||||
$value = "%$search%";
|
||||
|
||||
return $query->where(static function ($query) use ($value, $fields) {
|
||||
$field = array_shift($fields);
|
||||
if (str_contains($field, '.')) {
|
||||
[$relation, $relationField] = explode('.', $field);
|
||||
$query->whereHas($relation, static fn(Builder $query) => $query->where($relationField, 'like', $value)
|
||||
);
|
||||
} else {
|
||||
$query->where($field, 'like', $value);
|
||||
}
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (str_contains($field, '.')) {
|
||||
[$relation, $relationField] = explode('.', $field);
|
||||
$query->orWhereHas($relation, static fn(Builder $query) => $query->where($relationField, 'like', $value)
|
||||
);
|
||||
} else {
|
||||
$query->orWhere($field, 'like', $value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function getValidationRules(): array
|
||||
{
|
||||
return [
|
||||
'limit' => 'sometimes|int',
|
||||
'offset' => 'sometimes|int',
|
||||
'orderBy' => 'sometimes|array',
|
||||
'with.*' => 'sometimes|string',
|
||||
'withCount.*' => 'sometimes|string',
|
||||
'withSum.*.*' => 'sometimes|string',
|
||||
'search.query' => 'sometimes|string|nullable',
|
||||
'search.fields.*' => 'sometimes|string',
|
||||
'where' => 'sometimes|array',
|
||||
];
|
||||
}
|
||||
}
|
||||
237
app/Helpers/Recaptcha.php
Normal file
237
app/Helpers/Recaptcha.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Exceptions\Entities\AuthorizationException;
|
||||
use Cache;
|
||||
use GuzzleHttp\Client;
|
||||
use Request;
|
||||
use Throwable;
|
||||
|
||||
class Recaptcha
|
||||
{
|
||||
private string $userEmail;
|
||||
|
||||
private bool $solved = false;
|
||||
|
||||
/**
|
||||
* Increment amount of login tries for ip and login
|
||||
*/
|
||||
public function incrementCaptchaAmounts(): void
|
||||
{
|
||||
if (!$this->captchaEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCaptchaCacheKey();
|
||||
|
||||
if (!Cache::store('octane')->has($cacheKey)) {
|
||||
Cache::store('octane')->put($cacheKey, 1, config('recaptcha.ttl'));
|
||||
} else {
|
||||
Cache::store('octane')->increment($cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows captcha status from config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function captchaEnabled(): bool
|
||||
{
|
||||
return config('recaptcha.enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unique for ip and user login key
|
||||
* @return string
|
||||
*/
|
||||
private function getCaptchaCacheKey(): string
|
||||
{
|
||||
$ip = Request::ip();
|
||||
$email = $this->userEmail;
|
||||
return "AUTH_RECAPTCHA_LIMITER_{$ip}_{$email}_ATTEMPTS";
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget about tries of user to login
|
||||
*/
|
||||
public function clearCaptchaAmounts(): void
|
||||
{
|
||||
if (!$this->captchaEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cache::store('octane')->forget($this->getCaptchaCacheKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $credentials
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function check(array $credentials): void
|
||||
{
|
||||
if ($this->isBanned()) {
|
||||
$this->incrementBanAmounts();
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_BANNED);
|
||||
}
|
||||
|
||||
$this->userEmail = $credentials['email'];
|
||||
|
||||
if (isset($credentials['recaptcha'])) {
|
||||
$this->solve($credentials['recaptcha']);
|
||||
}
|
||||
|
||||
if ($this->needsCaptcha()) {
|
||||
$this->incrementBanAmounts();
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_CAPTCHA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if user on ban list
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isBanned(): bool
|
||||
{
|
||||
if (!$this->banEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheKey = $this->getBanCacheKey();
|
||||
|
||||
$banData = Cache::store('octane')->get($cacheKey, null);
|
||||
|
||||
if ($banData === null || !isset($banData['amounts'], $banData['time'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($banData['amounts'] < config('recaptcha.ban_attempts')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($banData['time'] + config('recaptcha.rate_limiter_ttl') < time()) {
|
||||
Cache::store('octane')->forget($cacheKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows ban limiter status from config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function banEnabled(): bool
|
||||
{
|
||||
return $this->captchaEnabled() && config('recaptcha.rate_limiter_enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unique for ip key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getBanCacheKey(): string
|
||||
{
|
||||
$ip = Request::ip();
|
||||
return "AUTH_RATE_LIMITER_{$ip}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment amount of tries for ip
|
||||
*/
|
||||
private function incrementBanAmounts(): void
|
||||
{
|
||||
if (!$this->banEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = $this->getBanCacheKey();
|
||||
|
||||
$banData = Cache::store('octane')->get($cacheKey);
|
||||
|
||||
if ($banData === null || !isset($banData['amounts'])) {
|
||||
$banData = ['amounts' => 1, 'time' => time()];
|
||||
} else {
|
||||
$banData['amounts']++;
|
||||
}
|
||||
|
||||
Cache::store('octane')->put($cacheKey, $banData, config('recaptcha.rate_limiter_ttl'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends request to google with captcha token for verify
|
||||
*
|
||||
* @param string $captchaToken
|
||||
*/
|
||||
private function solve(string $captchaToken = ''): void
|
||||
{
|
||||
if (!$this->captchaEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->solved) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = (new Client())->post(config('recaptcha.google_url'), [
|
||||
'form_params' => [
|
||||
'secret' => config('recaptcha.secret_key'),
|
||||
'response' => $captchaToken,
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $response->getBody();
|
||||
|
||||
if ($response === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $throwable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($data['success']) && $data['success'] === true) {
|
||||
$this->solved = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if we need to show captcha to user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function needsCaptcha(): bool
|
||||
{
|
||||
if (!$this->captchaEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->solved) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCaptchaCacheKey();
|
||||
|
||||
$attempts = Cache::store('octane')->get($cacheKey, null);
|
||||
|
||||
if ($attempts === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($attempts <= config('recaptcha.failed_attempts')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
126
app/Helpers/ReportHelper.php
Normal file
126
app/Helpers/ReportHelper.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use App\Models\TimeInterval;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use JsonException;
|
||||
use Maatwebsite\Excel\Excel;
|
||||
use RuntimeException;
|
||||
|
||||
class ReportHelper
|
||||
{
|
||||
public static string $dateFormat = 'Y-m-d';
|
||||
|
||||
public static function getReportFormat(Request $request)
|
||||
{
|
||||
return array_flip(self::getAvailableReportFormats())[$request->header('accept')] ?? null;
|
||||
}
|
||||
|
||||
public static function getAvailableReportFormats(): array
|
||||
{
|
||||
return [
|
||||
'csv' => 'text/csv',
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'pdf' => 'application/pdf',
|
||||
'xls' => 'application/vnd.ms-excel',
|
||||
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'html' => 'text/html',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $users
|
||||
* @param Carbon $startAt
|
||||
* @param Carbon $endAt
|
||||
* @param string[] $select
|
||||
* @return Builder
|
||||
*/
|
||||
public static function getBaseQuery(
|
||||
array $users,
|
||||
Carbon $startAt,
|
||||
Carbon $endAt,
|
||||
array $select = []
|
||||
): Builder
|
||||
{
|
||||
return Project::join('tasks', 'tasks.project_id', '=', 'projects.id')
|
||||
->join('time_intervals', 'time_intervals.task_id', '=', 'tasks.id')
|
||||
->join('users', 'time_intervals.user_id', '=', 'users.id')
|
||||
->select(array_unique(
|
||||
array_merge($select, [
|
||||
'time_intervals.id',
|
||||
'projects.id as project_id',
|
||||
'projects.name as project_name',
|
||||
'tasks.id as task_id',
|
||||
'tasks.task_name as task_name',
|
||||
'users.id as user_id',
|
||||
'users.full_name as full_name',
|
||||
'time_intervals.start_at',
|
||||
])
|
||||
))
|
||||
->where(fn($query) => $query->whereBetween('time_intervals.start_at', [$startAt, $endAt])
|
||||
->orWhereBetween('time_intervals.end_at', [$startAt, $endAt]))
|
||||
->whereNull('time_intervals.deleted_at')
|
||||
->whereIn('users.id', $users)
|
||||
->groupBy(['tasks.id', 'users.id', 'time_intervals.start_at'])
|
||||
->orderBy('time_intervals.start_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate interval duration in period
|
||||
* @param CarbonPeriod $period
|
||||
* @param array $durationByDay
|
||||
* @return int
|
||||
*/
|
||||
public static function getIntervalDurationInPeriod(CarbonPeriod $period, array $durationByDay): int
|
||||
{
|
||||
$durationInPeriod = 0;
|
||||
foreach ($durationByDay as $date => $duration) {
|
||||
$period->contains($date) && $durationInPeriod += $duration;
|
||||
}
|
||||
return $durationInPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits interval by days on which it exists on. Considering timezone of a user if provided.
|
||||
* @param $interval
|
||||
* @param $companyTimezone
|
||||
* @param $userTimezone
|
||||
* @return array
|
||||
*/
|
||||
public static function getIntervalDurationByDay($interval, $companyTimezone, $userTimezone = null): array
|
||||
{
|
||||
if ($userTimezone === null) {
|
||||
$userTimezone = $companyTimezone;
|
||||
}
|
||||
|
||||
$startAt = Carbon::parse($interval->start_at)->shiftTimezone($companyTimezone)->setTimezone($userTimezone);
|
||||
$endAt = Carbon::parse($interval->end_at)->shiftTimezone($companyTimezone)->setTimezone($userTimezone);
|
||||
|
||||
$startDate = $startAt->format(self::$dateFormat);
|
||||
$endDate = $endAt->format(self::$dateFormat);
|
||||
|
||||
$durationByDay = [];
|
||||
if ($startDate === $endDate) {
|
||||
$durationByDay[$startDate] = $interval->duration;
|
||||
} else {
|
||||
// If interval spans over midnight, divide it at midnight
|
||||
$startOfDay = $endAt->copy()->startOfDay();
|
||||
$startDateDuration = $startOfDay->diffInSeconds($startAt);
|
||||
|
||||
$durationByDay[$startDate] = $startDateDuration;
|
||||
|
||||
$endDateDuration = $endAt->diffInSeconds($startOfDay);
|
||||
|
||||
$durationByDay[$endDate] = $endDateDuration;
|
||||
}
|
||||
return $durationByDay;
|
||||
}
|
||||
}
|
||||
124
app/Helpers/StorageCleaner.php
Normal file
124
app/Helpers/StorageCleaner.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Contracts\ScreenshotService;
|
||||
use App\Models\TimeInterval;
|
||||
use Cache;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Storage;
|
||||
|
||||
class StorageCleaner
|
||||
{
|
||||
|
||||
public static function getFreeSpace(): float
|
||||
{
|
||||
return disk_free_space(Storage::path(ScreenshotService::PARENT_FOLDER));
|
||||
}
|
||||
|
||||
public static function getUsedSpace(): float
|
||||
{
|
||||
return config('cleaner.total_space') - self::getFreeSpace();
|
||||
}
|
||||
|
||||
public static function needThinning(): bool
|
||||
{
|
||||
return
|
||||
self::getUsedSpace() * 100 / config('cleaner.total_space')
|
||||
>= config('cleaner.threshold');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public static function thin($force = false): void
|
||||
{
|
||||
if ((!$force && !self::needThinning()) || Cache::store('octane')->get('thinning_now')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cache::store('octane')->set('thinning_now', true);
|
||||
|
||||
$service = app()->make(ScreenshotService::class);
|
||||
|
||||
while (self::getUsedSpace() > self::getWaterlineBorder()) {
|
||||
$availableScreenshots = self::getAvailableScreenshots();
|
||||
|
||||
if (count($availableScreenshots) === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($availableScreenshots as $screenshot) {
|
||||
$service->destroyScreenshot($screenshot);
|
||||
}
|
||||
}
|
||||
|
||||
Cache::store('octane')->set('thinning_now',false);
|
||||
Cache::store('octane')->set('last_thin',now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public static function getAvailableScreenshots(): array
|
||||
{
|
||||
$service = app()->make(ScreenshotService::class);
|
||||
|
||||
$collection = self::getScreenshotsCollection();
|
||||
|
||||
$result = [];
|
||||
$i = 0;
|
||||
|
||||
foreach ($collection->cursor() as $interval) {
|
||||
if (Storage::exists($service->getScreenshotPath($interval))) {
|
||||
$result[] = $interval->id;
|
||||
}
|
||||
|
||||
if ($i >= config('cleaner.page_size')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public static function countAvailableScreenshots(): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
$service = app()->make(ScreenshotService::class);
|
||||
|
||||
$collection = self::getScreenshotsCollection();
|
||||
|
||||
foreach ($collection->cursor() as $interval) {
|
||||
$count += (int)Storage::exists($service->getScreenshotPath($interval));
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private static function getWaterlineBorder(): float
|
||||
{
|
||||
return config('cleaner.total_space')
|
||||
* (config('cleaner.threshold') * 0.01)
|
||||
* (1 - config('cleaner.waterline') * 0.01);
|
||||
}
|
||||
|
||||
private static function getScreenshotsCollection(): Builder|TimeInterval|\Illuminate\Database\Query\Builder
|
||||
{
|
||||
return TimeInterval::whereHas('task', function (Builder $query) {
|
||||
$query->where('important', '=', 0);
|
||||
})
|
||||
->whereHas('task.project', function (Builder $query) {
|
||||
$query->where('important', '=', 0);
|
||||
})->whereHas('user', function (Builder $query) {
|
||||
$query->where('permanent_screenshots', '=', 0);
|
||||
})->orderBy('id');
|
||||
}
|
||||
}
|
||||
114
app/Helpers/Version.php
Normal file
114
app/Helpers/Version.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use CzProject\GitPhp\Git;
|
||||
use CzProject\GitPhp\GitException;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
//use Module;
|
||||
|
||||
class Version
|
||||
{
|
||||
private string $version;
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(protected ?string $module = null)
|
||||
{
|
||||
throw_if($module && !isset(self::getModules()[$module]), new RuntimeException('No such module'));
|
||||
|
||||
if ($module) {
|
||||
$this->version = self::getModules()[$module];
|
||||
return;
|
||||
}
|
||||
|
||||
$this->version = env('APP_VERSION', 'dev') ?: self::getComposerVersion(base_path());
|
||||
}
|
||||
|
||||
public static function getModules(): array
|
||||
{
|
||||
return cache()->rememberForever('app.modules', static function() {
|
||||
try {
|
||||
return Module::toCollection()->map(static function (\Nwidart\Modules\Laravel\Module $module) {
|
||||
$modulePath = $module->getPath();
|
||||
|
||||
try {
|
||||
return self::getComposerVersion($modulePath);
|
||||
} catch (Throwable) {
|
||||
try {
|
||||
return self::getFileVersion($modulePath);
|
||||
} catch (Throwable) {
|
||||
try {
|
||||
return self::getGitVersion($modulePath);
|
||||
} catch (Throwable) {
|
||||
return 'dev';
|
||||
}
|
||||
}
|
||||
}
|
||||
})->toArray();
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
private static function getComposerVersion(string $path): string
|
||||
{
|
||||
throw_unless(file_exists("$path/composer.json"));
|
||||
|
||||
$composerConfig = file_get_contents("$path/composer.json");
|
||||
|
||||
throw_unless(Str::isJson($composerConfig));
|
||||
|
||||
$package = json_decode($composerConfig, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
throw_unless(InstalledVersions::isInstalled($package['name']) || isset($package['version']));
|
||||
|
||||
return $package['version'] ?? InstalledVersions::getVersion($package['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
private static function getFileVersion(string $path): string
|
||||
{
|
||||
throw_unless(file_exists("$path/module.json"));
|
||||
|
||||
$moduleConfig = file_get_contents("$path/module.json");
|
||||
|
||||
throw_unless(Str::isJson($moduleConfig));
|
||||
|
||||
$module = json_decode($moduleConfig, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
throw_unless(isset($module['version']));
|
||||
|
||||
return $module['version'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws GitException
|
||||
*/
|
||||
private static function getGitVersion(string $path): string
|
||||
{
|
||||
$repo = (new Git())->open($path);
|
||||
|
||||
$tags = $repo->getTags();
|
||||
|
||||
return array_pop($tags);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return preg_replace('/^v(.*)$/', '$1', $this->version);
|
||||
}
|
||||
}
|
||||
14
app/Http/Controllers/ActuatorController.php
Normal file
14
app/Http/Controllers/ActuatorController.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
final class ActuatorController
|
||||
{
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
}
|
||||
301
app/Http/Controllers/Api/AboutController.php
Normal file
301
app/Http/Controllers/Api/AboutController.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Console\Commands\RotateScreenshots;
|
||||
use App\Helpers\ModuleHelper;
|
||||
use App\Helpers\ReportHelper;
|
||||
use App\Helpers\StorageCleaner;
|
||||
use App\Helpers\Version;
|
||||
use Artisan;
|
||||
use Cache;
|
||||
use Exception;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Routing\Controller;
|
||||
use JsonException;
|
||||
use Settings;
|
||||
|
||||
class AboutController extends Controller
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => config('app.stats_collector_url') . '/v2/',
|
||||
'headers' => ['x-cattr-instance' => Settings::scope('core')->get('instance')],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /about/reports Get Available Report Formats
|
||||
* @apiDescription Retrieves the list of available report formats and their corresponding MIME types.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetReportFormats
|
||||
* @apiGroup About
|
||||
*
|
||||
* @apiSuccess {Object} types List of available report formats and their MIME types.
|
||||
* @apiSuccess {String} types.csv MIME type for CSV format.
|
||||
* @apiSuccess {String} types.xlsx MIME type for XLSX format.
|
||||
* @apiSuccess {String} types.pdf MIME type for PDF format.
|
||||
* @apiSuccess {String} types.xls MIME type for XLS format.
|
||||
* @apiSuccess {String} types.ods MIME type for ODS format.
|
||||
* @apiSuccess {String} types.html MIME type for HTML format.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "types": {
|
||||
* "csv": "text/csv",
|
||||
* "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
* "pdf": "application/pdf",
|
||||
* "xls": "application/vnd.ms-excel",
|
||||
* "ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
* "html": "text/html"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
|
||||
public function reports(): JsonResponse
|
||||
{
|
||||
return responder()->success([
|
||||
'types' => ReportHelper::getAvailableReportFormats(),
|
||||
])->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about this instance.
|
||||
* @throws JsonException
|
||||
*/
|
||||
/**
|
||||
* @api {get} /about Get Application and Module Information
|
||||
* @apiDescription Retrieves information about the application instance, modules, and image version details.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetAppInfo
|
||||
* @apiGroup About
|
||||
*
|
||||
* @apiSuccess {Object} app Application information.
|
||||
* @apiSuccess {String} app.version The current version of the application.
|
||||
* @apiSuccess {String} app.instance_id The unique identifier of the application instance.
|
||||
* @apiSuccess {Boolean} app.vulnerable Indicates if the application is vulnerable.
|
||||
* @apiSuccess {String} app.last_version The latest version available for the application.
|
||||
* @apiSuccess {String} app.message Any important message related to the application.
|
||||
*
|
||||
* @apiSuccess {Object[]} modules List of modules integrated into the application.
|
||||
* @apiSuccess {String} modules.name Name of the module.
|
||||
* @apiSuccess {String} modules.version Current version of the module.
|
||||
* @apiSuccess {Boolean} modules.enabled Indicates if the module is enabled.
|
||||
*
|
||||
* @apiSuccess {Object} image Information about the image version.
|
||||
* @apiSuccess {String} image.version The version of the image (if available).
|
||||
* @apiSuccess {Boolean} image.vulnerable Indicates if the image is vulnerable.
|
||||
* @apiSuccess {String} image.last_version The latest available version for the image.
|
||||
* @apiSuccess {String} image.message Any important message related to the image.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "app": {
|
||||
* "version": "dev",
|
||||
* "instance_id": null,
|
||||
* "vulnerable": null,
|
||||
* "last_version": null,
|
||||
* "message": null
|
||||
* },
|
||||
* "modules": [
|
||||
* {
|
||||
* "name": "JiraIntegration",
|
||||
* "version": "3.0.0",
|
||||
* "enabled": false
|
||||
* }
|
||||
* ],
|
||||
* "image": {
|
||||
* "version": null,
|
||||
* "vulnerable": null,
|
||||
* "last_version": null,
|
||||
* "message": null
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
$imageVersion = getenv('IMAGE_VERSION', true) ?: null;
|
||||
|
||||
$releaseInfo = $this->requestReleaseInfo();
|
||||
$modulesInfo = $this->requestModulesInfo();
|
||||
$imageInfo = ($imageVersion) ? $this->requestImageInfo($imageVersion) : false;
|
||||
|
||||
return responder()->success([
|
||||
'app' => [
|
||||
'version' => config('app.version'),
|
||||
'instance_id' => Settings::scope('core')->get('instance'),
|
||||
'vulnerable' => optional($releaseInfo)->vulnerable,
|
||||
'last_version' => optional($releaseInfo)->lastVersion,
|
||||
'message' => optional($releaseInfo)->flashMessage,
|
||||
],
|
||||
'modules' => $modulesInfo,
|
||||
'image' => [
|
||||
'version' => $imageVersion,
|
||||
'vulnerable' => optional($imageInfo)->vulnerable,
|
||||
'last_version' => optional($imageInfo)->lastVersion,
|
||||
'message' => optional($imageInfo)->flashMessage,
|
||||
]
|
||||
])->respond();
|
||||
}
|
||||
|
||||
private function requestReleaseInfo(): ?object
|
||||
{
|
||||
try {
|
||||
return json_decode(
|
||||
$this->client->get('release/' . config('app.version'))->getBody()->getContents(),
|
||||
false,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function requestModulesInfo(): array
|
||||
{
|
||||
$options = [
|
||||
'json' => ModuleHelper::getModulesInfo(),
|
||||
];
|
||||
|
||||
try {
|
||||
return array_map(static function ($el) {
|
||||
$el['version'] = (string)(new Version($el['name']));
|
||||
|
||||
return $el;
|
||||
}, json_decode(
|
||||
$this->client->post('modules', $options)->getBody()->getContents(),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
|
||||
)['modules']);
|
||||
} catch (Exception) {
|
||||
return ModuleHelper::getModulesInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private function requestImageInfo(string $imageVersion): ?object
|
||||
{
|
||||
try {
|
||||
return json_decode(
|
||||
$this->client->get("image/$imageVersion")->getBody()->getContents(),
|
||||
false,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR | JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
/**
|
||||
* @api {get} /storage Get Storage Information
|
||||
* @apiDescription Retrieves information about the storage space, thinning status, and available screenshots.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetStorageInfo
|
||||
* @apiGroup Storage
|
||||
*
|
||||
* @apiSuccess {Object} space Information about the storage space.
|
||||
* @apiSuccess {Number} space.left The amount of free space left (in bytes).
|
||||
* @apiSuccess {Number} space.used The amount of space currently used (in bytes).
|
||||
* @apiSuccess {Number} space.total The total amount of storage space available (in bytes).
|
||||
*
|
||||
* @apiSuccess {Number} threshold The storage usage threshold percentage before thinning is needed.
|
||||
* @apiSuccess {Boolean} need_thinning Indicates if the storage requires thinning.
|
||||
* @apiSuccess {Number} screenshots_available The number of available screenshots in the storage.
|
||||
*
|
||||
* @apiSuccess {Object} thinning Information about the thinning process.
|
||||
* @apiSuccess {Boolean} thinning.now Indicates if the thinning process is currently ongoing.
|
||||
* @apiSuccess {String} thinning.last Timestamp of the last thinning process.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "space": {
|
||||
* "left": 26579533824,
|
||||
* "used": 178705711104,
|
||||
* "total": 205285244928
|
||||
* },
|
||||
* "threshold": 75,
|
||||
* "need_thinning": true,
|
||||
* "screenshots_available": 0,
|
||||
* "thinning": {
|
||||
* "now": null,
|
||||
* "last": null
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
|
||||
public function storage(): JsonResponse
|
||||
{
|
||||
return responder()->success([
|
||||
'space' => [
|
||||
'left' => StorageCleaner::getFreeSpace(),
|
||||
'used' => StorageCleaner::getUsedSpace(),
|
||||
'total' => config('cleaner.total_space'),
|
||||
],
|
||||
'threshold' => config('cleaner.threshold'),
|
||||
'need_thinning' => StorageCleaner::needThinning(),
|
||||
'screenshots_available' => StorageCleaner::countAvailableScreenshots(),
|
||||
'thinning' => [
|
||||
'now' => Cache::store('octane')->get('thinning_now'),
|
||||
'last' => Cache::store('octane')->get('last_thin'),
|
||||
]
|
||||
])->respond();
|
||||
}
|
||||
/**
|
||||
* @api {get} /storage Get Storage Information
|
||||
* @apiDescription Retrieves information about the storage space, thinning status, and available screenshots.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetStorageInfo
|
||||
* @apiGroup Storage
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 No Content
|
||||
* {
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function startStorageClean(): JsonResponse
|
||||
{
|
||||
Artisan::queue(RotateScreenshots::class);
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
}
|
||||
92
app/Http/Controllers/Api/AttachmentController.php
Normal file
92
app/Http/Controllers/Api/AttachmentController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Contracts\AttachmentService;
|
||||
use App\Enums\AttachmentStatus;
|
||||
use App\Http\Requests\Attachment\CreateAttachmentRequest;
|
||||
use App\Http\Requests\Attachment\DownloadAttachmentRequest;
|
||||
use App\Models\Attachment;
|
||||
use Filter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Throwable;
|
||||
use URL;
|
||||
|
||||
class AttachmentController extends ItemController
|
||||
{
|
||||
protected const MODEL = Attachment::class;
|
||||
|
||||
public function __construct(protected AttachmentService $attachmentService)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CreateAttachmentRequest $request
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function create(CreateAttachmentRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(Filter::getActionFilterName(), static function (Attachment $attachment) {
|
||||
$attachment->load('user');
|
||||
return $attachment;
|
||||
});
|
||||
return $this->_create($request);
|
||||
}
|
||||
|
||||
public function tmpDownload(Request $request, Attachment $attachment): ?StreamedResponse
|
||||
{
|
||||
if (! $request->hasValidSignature()) {
|
||||
abort(401);
|
||||
}
|
||||
|
||||
return $this->streamDownloadLogic($attachment);
|
||||
}
|
||||
|
||||
public function createTemporaryUrl(DownloadAttachmentRequest $request, Attachment $attachment): string
|
||||
{
|
||||
// we do this because signature breaks if we use built in method for creating relative path
|
||||
// also cannot determine request scheme when ran inside docker
|
||||
$url = URL::temporarySignedRoute(
|
||||
'attachment.temporary-download',
|
||||
now()->addSeconds($request->validated('seconds') ?? 3600),
|
||||
$attachment
|
||||
);
|
||||
$parsedUrl = parse_url($url);
|
||||
|
||||
// Combine the path and query string
|
||||
return $parsedUrl['path'] . (isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Attachment $attachment
|
||||
* @return StreamedResponse|void
|
||||
*/
|
||||
protected function streamDownloadLogic(Attachment $attachment)
|
||||
{
|
||||
if ($attachment->status === AttachmentStatus::GOOD && $this->attachmentService->fileExists($attachment)) {
|
||||
$headers = [];
|
||||
if ($attachment->mime_type !== '') {
|
||||
$headers['Content-Type'] = $attachment->mime_type;
|
||||
}
|
||||
|
||||
return response()->streamDownload(
|
||||
function () use ($attachment) {
|
||||
$stream = $this->attachmentService->readStream($attachment);
|
||||
|
||||
while (!feof($stream)) {
|
||||
echo fread($stream, 2048);
|
||||
}
|
||||
|
||||
fclose($stream);
|
||||
},
|
||||
$attachment->original_name,
|
||||
$headers
|
||||
);
|
||||
}
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
229
app/Http/Controllers/Api/CompanySettingsController.php
Normal file
229
app/Http/Controllers/Api/CompanySettingsController.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\CompanySettings\UpdateCompanySettingsRequest;
|
||||
use App\Http\Transformers\CompanySettingsTransformer;
|
||||
use App\Models\Priority;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Settings;
|
||||
|
||||
class CompanySettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* @api {get} /company-settings/ List
|
||||
* @apiDescription Returns all company settings.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName ListCompanySettings
|
||||
* @apiGroup Company Settings
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName ListCompanySettings
|
||||
* @apiGroup Company Settings
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {String} timezone The timezone setting for the company.
|
||||
* @apiSuccess {String} language The language setting for the company.
|
||||
* @apiSuccess {Integer} work_time The configured work time in hours.
|
||||
* @apiSuccess {Array} color Array of colors configured for the company settings.
|
||||
* @apiSuccess {Array} internal_priorities Array of internal priorities.
|
||||
* @apiSuccess {Integer} internal_priorities.id The unique ID of the priority.
|
||||
* @apiSuccess {String} internal_priorities.name The name of the priority.
|
||||
* @apiSuccess {String} internal_priorities.created_at The creation timestamp of the priority.
|
||||
* @apiSuccess {String} internal_priorities.updated_at The last update timestamp of the priority.
|
||||
* @apiSuccess {String} internal_priorities.color The color associated with the priority.
|
||||
* @apiSuccess {Integer} heartbeat_period The period for heartbeat checks in seconds.
|
||||
* @apiSuccess {Boolean} auto_thinning Indicates if automatic thinning of old data is enabled.
|
||||
* @apiSuccess {Integer} screenshots_state The current state of screenshot monitoring.
|
||||
* @apiSuccess {Integer} env_screenshots_state The environmental screenshot state.
|
||||
* @apiSuccess {Integer} default_priority_id The default priority ID.
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* {
|
||||
* "timezone": "UTC",
|
||||
* "language": "ru",
|
||||
* "work_time": 0,
|
||||
* "color": [],
|
||||
* "internal_priorities": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "name": "Normal",
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-07-12T17:57:40.000000Z",
|
||||
* "color": null
|
||||
* },
|
||||
* {
|
||||
* "id": 2,
|
||||
* "name": "Normal",
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-06-21T10:06:50.000000Z",
|
||||
* "color": "#49E637"
|
||||
* },
|
||||
* {
|
||||
* "id": 3,
|
||||
* "name": "High",
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-06-21T10:07:00.000000Z",
|
||||
* "color": "#D40C0C"
|
||||
* },
|
||||
* {
|
||||
* "id": 5,
|
||||
* "name": "Normal",
|
||||
* "created_at": "2024-07-12T17:10:54.000000Z",
|
||||
* "updated_at": "2024-07-12T17:10:54.000000Z",
|
||||
* "color": null
|
||||
* }
|
||||
*],
|
||||
* "heartbeat_period": 60,
|
||||
* "auto_thinning": true,
|
||||
* "screenshots_state": 1,
|
||||
* "env_screenshots_state": -1,
|
||||
* "default_priority_id": 2
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return responder()->success(
|
||||
array_merge(
|
||||
Settings::scope('core')->all(),
|
||||
['internal_priorities' => Priority::all()]
|
||||
),
|
||||
new CompanySettingsTransformer
|
||||
)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UpdateCompanySettingsRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*
|
||||
* @api {patch} /company-settings/ Update
|
||||
* @apiDescription Updates the specified company settings.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName UpdateCompanySettings
|
||||
* @apiGroup Company Settings
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {String} timezone The timezone setting for the company.
|
||||
* @apiParam {String} language The language setting for the company.
|
||||
* @apiParam {Integer} work_time The configured work time in hours.
|
||||
* @apiParam {Array} color Array of colors configured for the company settings.
|
||||
* @apiParam {Array} internal_priorities Array of internal priorities.
|
||||
* @apiParam {Integer} internal_priorities.id The unique ID of the priority.
|
||||
* @apiParam {String} internal_priorities.name The name of the priority.
|
||||
* @apiParam {String} internal_priorities.created_at The creation timestamp of the priority.
|
||||
* @apiParam {String} internal_priorities.updated_at The last update timestamp of the priority.
|
||||
* @apiParam {String} internal_priorities.color The color associated with the priority.
|
||||
* @apiParam {Integer} heartbeat_period The period for heartbeat checks in seconds.
|
||||
* @apiParam {Boolean} auto_thinning Indicates if automatic thinning of old data is enabled.
|
||||
* @apiParam {Integer} screenshots_state The current state of screenshot monitoring.
|
||||
* @apiParam {Integer} env_screenshots_state The environmental screenshot state.
|
||||
* @apiParam {Integer} default_priority_id The default priority ID.
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* { "timezone" : "Europe/Moscow",
|
||||
* "language" : "en",
|
||||
* "work_time" : 0,
|
||||
* "color" : [],
|
||||
* "internal_priorities" : [
|
||||
* {
|
||||
* "id" : 1,
|
||||
* "name" : "Normal",
|
||||
* "created_at" : "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at" : "2024-07-12T17:57:40.000000Z",
|
||||
* "color" : null
|
||||
* },
|
||||
* {
|
||||
* "id" : 2,
|
||||
* "name" : "Normal",
|
||||
* "created_at" : "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at" : "2024-06-21T10:06:50.000000Z",
|
||||
* "color" : "#49E637"},
|
||||
* {
|
||||
* "id" : 3,
|
||||
* "name" : "High",
|
||||
* "created_at" : "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at" : "2024-06-21T10:07:00.000000Z",
|
||||
* "color" : "#D40C0C"
|
||||
* },{
|
||||
* "id" : 5,
|
||||
* "name" : "Normal",
|
||||
* "created_at" : "2024-07-12T17:10:54.000000Z",
|
||||
* "updated_at" : "2024-07-12T17:10:54.000000Z",
|
||||
* "color" : null
|
||||
* }
|
||||
* ],
|
||||
* "heartbeat_period" : 60,
|
||||
* "auto_thinning" : true,
|
||||
* "screenshots_state" : 1,
|
||||
* "env_screenshots_state" : -1,
|
||||
* "default_priority_id" : 2
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Array} data Contains an array of settings that changes were applied to
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 No Content
|
||||
* {
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function update(UpdateCompanySettingsRequest $request): JsonResponse
|
||||
{
|
||||
Settings::scope('core')->set($request->validated());
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
/**
|
||||
* @api {get} /offline-sync/public-key Get Offline Sync Public Key
|
||||
* @apiDescription Retrieves the public key for offline synchronization.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetOfflineSyncPublicKey
|
||||
* @apiGroup OfflineSync
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission offline_sync_view
|
||||
* @apiPermission offline_sync_full_access
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates if the operation was successful.
|
||||
* @apiSuccess {Object} data The response data.
|
||||
* @apiSuccess {String} data.key The public key for offline synchronization.
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "key": null
|
||||
* }
|
||||
* @apiError (Error 401) Unauthorized The user is not authorized to access this resource.
|
||||
* @apiError (Error 403) Forbidden The user does not have the necessary permissions.
|
||||
* @apiError (Error 404) NotFound The requested resource was not found.
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
public function getOfflineSyncPublicKey(): JsonResponse
|
||||
{
|
||||
return responder()->success(
|
||||
[ 'key' => Settings::scope('core.offline-sync')->get('public_key')],
|
||||
)->respond();
|
||||
}
|
||||
}
|
||||
1046
app/Http/Controllers/Api/IntervalController.php
Normal file
1046
app/Http/Controllers/Api/IntervalController.php
Normal file
File diff suppressed because it is too large
Load Diff
306
app/Http/Controllers/Api/InvitationController.php
Normal file
306
app/Http/Controllers/Api/InvitationController.php
Normal file
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Requests\Invitation\CreateInvitationRequest;
|
||||
use App\Http\Requests\Invitation\ListInvitationRequest;
|
||||
use App\Http\Requests\Invitation\DestroyInvitationRequest;
|
||||
use App\Http\Requests\Invitation\ShowInvitationRequest;
|
||||
use App\Http\Requests\Invitation\UpdateInvitationRequest;
|
||||
use App\Models\Invitation;
|
||||
use App\Services\InvitationService;
|
||||
use Exception;
|
||||
use Filter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Throwable;
|
||||
|
||||
class InvitationController extends ItemController
|
||||
{
|
||||
protected const MODEL = Invitation::class;
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /invitations/show Show
|
||||
* @apiDescription Show invitation.
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Show Invitation
|
||||
* @apiGroup Invitation
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id Invitation ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Array} res Array of records containing the id, email, key, expiration date and role id
|
||||
*
|
||||
* @apiUse InvitationObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "res": [
|
||||
* {
|
||||
* "id": 1
|
||||
* "email": "test@example.com",
|
||||
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
|
||||
* "expires_at": "2020-01-01T00:00:00.000000Z",
|
||||
* "role_id": 1
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function show(ShowInvitationRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @api {get} /invitations/list List
|
||||
* @apiDescription Get list of invitations.
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Invitation List
|
||||
* @apiGroup Invitation
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {Array} res Array of records containing the id, email, key, expiration date and role id
|
||||
*
|
||||
* @apiUse InvitationObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "res": [
|
||||
* {
|
||||
* "id": 1
|
||||
* "email": "test@example.com",
|
||||
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
|
||||
* "expires_at": "2020-01-01T00:00:00.000000Z",
|
||||
* "role_id": 1
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function index(ListInvitationRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CreateInvitationRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
* @api {post} /invitations/create Create
|
||||
* @apiDescription Creates a unique invitation token and sends an email to the users
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Create Invitation
|
||||
* @apiGroup Invitation
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Array} users List of users to send an invitation to
|
||||
* @apiParam {String} users.email User email
|
||||
* @apiParam {Integer} users.role_id ID of the role that will be assigned to the created user
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "users": [
|
||||
* {
|
||||
* "email": "test@example.com",
|
||||
* "role_id": 1
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} res Array of records containing the id, email, key, expiration date and role id
|
||||
*
|
||||
* @apiUse InvitationObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "res": [
|
||||
* {
|
||||
* "id": 1
|
||||
* "email": "test@example.com",
|
||||
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
|
||||
* "expires_at": "2020-01-01T00:00:00.000000Z",
|
||||
* "role_id": 1
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Email is not specified
|
||||
* HTTP/1.1 400 Bad Request
|
||||
* {
|
||||
* "error_type": "validation",
|
||||
* "message": "Validation error",
|
||||
* "info": {
|
||||
* "users.0.email": [
|
||||
* "The email field is required."
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Email already exists
|
||||
* HTTP/1.1 400 Bad Request
|
||||
* {
|
||||
* "error_type": "validation",
|
||||
* "message": "Validation error",
|
||||
* "info": {
|
||||
* "users.0.email": [
|
||||
* "The email test@example.com has already been taken."
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function create(CreateInvitationRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
$invitations = [];
|
||||
|
||||
foreach ($requestData['users'] as $user) {
|
||||
$invitations[] = InvitationService::create($user);
|
||||
}
|
||||
|
||||
return responder()->success($invitations)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UpdateInvitationRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*
|
||||
* @api {post} /invitations/resend Resend
|
||||
* @apiDescription Updates the token expiration date and sends an email to the user's email address.
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Update Invitation
|
||||
* @apiGroup Invitation
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id Invitation ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Array} res Invitation data
|
||||
*
|
||||
* @apiUse InvitationObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "res": {
|
||||
* "id": 1
|
||||
* "email": "test@example.com",
|
||||
* "key": "06d4a090-9675-11ea-bf39-5f84c549e29c",
|
||||
* "expires_at": "2020-01-01T00:00:00.000000Z",
|
||||
* "role_id": 1
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} The id does not exist
|
||||
* HTTP/1.1 400 Bad Request
|
||||
* {
|
||||
* "error_type": "validation",
|
||||
* "message": "Validation error",
|
||||
* "info": {
|
||||
* "id": [
|
||||
* "The selected id is invalid."
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function resend(UpdateInvitationRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
$invitation = InvitationService::update($requestData['id']);
|
||||
|
||||
return responder()->success($invitation)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /invitations/remove Destroy
|
||||
* @apiDescription Destroy User
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Destroy Invitation
|
||||
* @apiGroup Invitation
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id ID of the target invitation
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message Destroy status
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "message": "Item has been removed"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function destroy(DestroyInvitationRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_destroy($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ListInvitationRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
/**
|
||||
* @api {get} /api/invitations/count Count Invitations
|
||||
* @apiDescription Returns the total count of invitations
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName CountInvitations
|
||||
* @apiGroup Invitations
|
||||
*
|
||||
* @apiUse TotalSuccess
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function count(ListInvitationRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_count($request);
|
||||
}
|
||||
}
|
||||
648
app/Http/Controllers/Api/ItemController.php
Normal file
648
app/Http/Controllers/Api/ItemController.php
Normal file
@@ -0,0 +1,648 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Requests\CattrFormRequest;
|
||||
use Filter;
|
||||
use App\Helpers\QueryHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use CatEvent;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
abstract class ItemController extends Controller
|
||||
{
|
||||
protected const MODEL = Model::class;
|
||||
|
||||
/**
|
||||
* @apiDefine ItemNotFoundError
|
||||
* @apiErrorExample {json} No such item
|
||||
* HTTP/1.1 404 Not Found
|
||||
* {
|
||||
* "message": "Item not found",
|
||||
* "error_type": "query.item_not_found"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine ValidationError
|
||||
* @apiErrorExample {json} Validation error
|
||||
* HTTP/1.1 400 Bad Request
|
||||
* {
|
||||
* "message": "Validation error",
|
||||
* "error_type": "validation",
|
||||
* "info": "Invalid id"
|
||||
* }
|
||||
*
|
||||
* @apiError (Error 400) {String} info Validation errors
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
/**
|
||||
* @apiDefine ProjectIDParam
|
||||
* @apiParam {Integer} id Project ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @apiDefine ProjectObject
|
||||
*
|
||||
* @apiSuccess {Integer} id Project ID
|
||||
* @apiSuccess {Integer} company_id Company ID
|
||||
* @apiSuccess {String} name Project name
|
||||
* @apiSuccess {String} description Project description
|
||||
* @apiSuccess {String} deleted_at Project deletion date or null
|
||||
* @apiSuccess {String} created_at Project creation date
|
||||
* @apiSuccess {String} updated_at Project update date
|
||||
* @apiSuccess {Boolean} important Project importance
|
||||
* @apiSuccess {String} source Project source
|
||||
* @apiSuccess {Integer} default_priority_id Default priority ID or null
|
||||
* @apiSuccess {Object[]} phases List of project phases
|
||||
* @apiSuccess {Integer} phases.id Phase ID
|
||||
* @apiSuccess {String} phases.name Phase name
|
||||
* @apiSuccess {String} phases.description Phase description
|
||||
* @apiSuccess {Integer} phases.tasks_count Number of tasks in the phase
|
||||
* @apiSuccess {String} phases.created_at Phase creation date
|
||||
* @apiSuccess {String} phases.updated_at Phase update date
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "company_id": 1,
|
||||
* "name": "Dolores voluptates.",
|
||||
* "description": "Deleniti maxime fugit nesciunt. Ut maiores deleniti tempora vel. Nisi aut doloremque accusantium tempore aut.",
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "important": 1,
|
||||
* "source": "internal",
|
||||
* "default_priority_id": null,
|
||||
* "phases": []
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @apiDefine UserObject
|
||||
* @apiSuccess {Integer} id User ID.
|
||||
* @apiSuccess {String} full_name Full name of the user.
|
||||
* @apiSuccess {String} email Email address of the user.
|
||||
* @apiSuccess {String} url URL associated with the user (if any).
|
||||
* @apiSuccess {Integer} company_id ID of the company the user belongs to.
|
||||
* @apiSuccess {String} avatar URL of the user's avatar (if any).
|
||||
* @apiSuccess {Boolean} screenshots_state State of screenshot capturing.
|
||||
* @apiSuccess {Boolean} manual_time Indicates whether manual time tracking is enabled.
|
||||
* @apiSuccess {Integer} computer_time_popup Time in seconds for the computer time popup.
|
||||
* @apiSuccess {Boolean} blur_screenshots Indicates if screenshots are blurred.
|
||||
* @apiSuccess {Boolean} web_and_app_monitoring Indicates if web and app monitoring is enabled.
|
||||
* @apiSuccess {Integer} screenshots_interval Interval for capturing screenshots in minutes.
|
||||
* @apiSuccess {Boolean} active Indicates if the user account is active.
|
||||
* @apiSuccess {String} created_at Timestamp of when the user was created.
|
||||
* @apiSuccess {String} updated_at Timestamp of when the user was last updated.
|
||||
* @apiSuccess {String} deleted_at Timestamp of when the user was deleted (if applicable).
|
||||
* @apiSuccess {String} timezone User's timezone.
|
||||
* @apiSuccess {Boolean} important Indicates if the user is marked as important.
|
||||
* @apiSuccess {Boolean} change_password Indicates if the user is required to change their password.
|
||||
* @apiSuccess {Integer} role_id Role ID associated with the user.
|
||||
* @apiSuccess {String} user_language Language preference of the user.
|
||||
* @apiSuccess {String} type User type (e.g., employee, admin).
|
||||
* @apiSuccess {Boolean} invitation_sent Indicates if an invitation has been sent to the user.
|
||||
* @apiSuccess {Boolean} client_installed Indicates if the tracking client is installed.
|
||||
* @apiSuccess {Boolean} permanent_screenshots Indicates if permanent screenshots are enabled.
|
||||
* @apiSuccess {String} last_activity Timestamp of the user's last activity.
|
||||
* @apiSuccess {Boolean} screenshots_state_locked Indicates if the screenshot state is locked.
|
||||
* @apiSuccess {Boolean} online Indicates if the user is currently online.
|
||||
* @apiSuccess {Boolean} can_view_team_tab Indicates if the user can view the team tab.
|
||||
* @apiSuccess {Boolean} can_create_task Indicates if the user can create tasks.
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "full_name": "Admin",
|
||||
* "email": "admin@cattr.app",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_state": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-08-20T09:22:02.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 0,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2024-08-20 09:22:02",
|
||||
* "screenshots_state_locked": false,
|
||||
* "online": false,
|
||||
* "can_view_team_tab": true,
|
||||
* "can_create_task": true
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine UserParams
|
||||
*/
|
||||
/**
|
||||
* @apiDefine User
|
||||
* @apiSuccess {String} access_token Token
|
||||
* @apiSuccess {String} token_type Token Type
|
||||
* @apiSuccess {String} expires_in Token TTL (ISO 8601 Date)
|
||||
* @apiSuccess {Object} user User Entity
|
||||
* @apiSuccess {Integer} user.id ID of the user
|
||||
* @apiSuccess {String} user.full_name Full name of the user
|
||||
* @apiSuccess {String} user.email Email of the user
|
||||
* @apiSuccess {String} [user.url] URL of the user (optional)
|
||||
* @apiSuccess {Integer} user.company_id Company ID of the user
|
||||
* @apiSuccess {String} [user.avatar] Avatar URL of the user (optional)
|
||||
* @apiSuccess {Boolean} user.screenshots_active Indicates if screenshots are active
|
||||
* @apiSuccess {Boolean} user.manual_time Indicates if manual time tracking is allowed
|
||||
* @apiSuccess {Integer} user.computer_time_popup Time interval for computer time popup
|
||||
* @apiSuccess {Boolean} user.blur_screenshots Indicates if screenshots are blurred
|
||||
* @apiSuccess {Boolean} user.web_and_app_monitoring Indicates if web and app monitoring is enabled
|
||||
* @apiSuccess {Integer} user.screenshots_interval Interval for taking screenshots
|
||||
* @apiSuccess {Boolean} user.active Indicates if the user is active
|
||||
* @apiSuccess {String} [user.deleted_at] Deletion timestamp (if applicable, otherwise null)
|
||||
* @apiSuccess {String} user.created_at Creation timestamp
|
||||
* @apiSuccess {String} user.updated_at Last update timestamp
|
||||
* @apiSuccess {String} [user.timezone] Timezone of the user (optional)
|
||||
* @apiSuccess {Boolean} user.important Indicates if the user is marked as important
|
||||
* @apiSuccess {Boolean} user.change_password Indicates if the user needs to change password
|
||||
* @apiSuccess {Integer} user.role_id Role ID of the user
|
||||
* @apiSuccess {String} user.user_language Language of the user
|
||||
* @apiSuccess {String} user.type Type of the user (e.g., "employee")
|
||||
* @apiSuccess {Boolean} user.invitation_sent Indicates if invitation is sent to the user
|
||||
* @apiSuccess {Integer} user.nonce Nonce value of the user
|
||||
* @apiSuccess {Boolean} user.client_installed Indicates if client is installed
|
||||
* @apiSuccess {Boolean} user.permanent_screenshots Indicates if screenshots are permanent
|
||||
* @apiSuccess {String} user.last_activity Last activity timestamp of the user
|
||||
* @apiSuccess {Boolean} user.online Indicates if the user is online
|
||||
* @apiSuccess {Boolean} user.can_view_team_tab Indicates if the user can view team tab
|
||||
* @apiSuccess {Boolean} user.can_create_task Indicates if the user can create tasks
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "access_token": "51|d6HvWGk6zY1aqqRms5pkp6Pb6leBs7zaW4IAWGvQ5d00b8be",
|
||||
* "token_type": "bearer",
|
||||
* "expires_in": "2024-07-12T11:59:31+00:00",
|
||||
* "user": {
|
||||
* "id": 1,
|
||||
* "full_name": "Admin",
|
||||
* "email": "johndoe@example.com",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_active": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-02-15T19:06:42.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 0,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2023-10-26 10:26:17",
|
||||
* "online": false,
|
||||
* "can_view_team_tab": true,
|
||||
* "can_create_task": true
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @apiDefine ScreenshotObject
|
||||
* @apiSuccess {Integer} id ID of the screenshot
|
||||
* @apiSuccess {Integer} time_interval_id Time interval ID to which the screenshot belongs
|
||||
* @apiSuccess {String} path Path to the screenshot
|
||||
* @apiSuccess {String} created_at Timestamp when the screenshot was created
|
||||
* @apiSuccess {String} updated_at Timestamp when the screenshot was last updated
|
||||
* @apiSuccess {String} [deleted_at] Timestamp when the screenshot was deleted (if applicable)
|
||||
* @apiSuccess {String} [thumbnail_path] Path to the thumbnail of the screenshot (if applicable)
|
||||
* @apiSuccess {Boolean} important Indicates if the screenshot is marked as important
|
||||
* @apiSuccess {Boolean} is_removed Indicates if the screenshot is removed
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "data": {
|
||||
* "id": 1,
|
||||
* "time_interval_id": 1,
|
||||
* "path": "uploads\/screenshots\/1_1_1.png",
|
||||
* "created_at": "2020-01-23T09:42:26+00:00",
|
||||
* "updated_at": "2020-01-23T09:42:26+00:00",
|
||||
* "deleted_at": null,
|
||||
* "thumbnail_path": null,
|
||||
* "important": false,
|
||||
* "is_removed": false
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine ScreenshotParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine TimeIntervalObject
|
||||
* @apiSuccess {Integer} id ID of the time interval
|
||||
* @apiSuccess {Integer} task_id ID of the task
|
||||
* @apiSuccess {String} start_at Start timestamp of the time interval
|
||||
* @apiSuccess {String} end_at End timestamp of the time interval
|
||||
* @apiSuccess {String} created_at Creation timestamp
|
||||
* @apiSuccess {String} updated_at Last update timestamp
|
||||
* @apiSuccess {String} [deleted_at] Deletion timestamp (if applicable)
|
||||
* @apiSuccess {String} user_id The ID of the user
|
||||
* @apiSuccess {Boolean} is_manual Indicates whether the time was logged manually (true) or automatically
|
||||
* @apiSuccess {Integer} activity_fill Activity fill percentage
|
||||
* @apiSuccess {Integer} mouse_fill Mouse activity fill percentage
|
||||
* @apiSuccess {Integer} keyboard_fill Keyboard activity fill percentage
|
||||
* @apiSuccess {Integer} location Additional location information, if available
|
||||
* @apiSuccess {Integer} screenshot_id The ID of the screenshot associated with this interval
|
||||
* @apiSuccess {Integer} has_screenshot Indicates if there is a screenshot for this interval
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* {
|
||||
* "id": 1,
|
||||
* "task_id": 1,
|
||||
* "start_at": "2023-10-26 10:21:17",
|
||||
* "end_at": "2023-10-26 10:26:17",
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "deleted_at": null,
|
||||
* "user_id": 2,
|
||||
* "is_manual": false,
|
||||
* "activity_fill": 60,
|
||||
* "mouse_fill": 47,
|
||||
* "keyboard_fill": 13,
|
||||
* "location": null,
|
||||
* "screenshot_id": null,
|
||||
* "has_screenshot": true
|
||||
* },...
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine TimeIntervalParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine InvitationObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine PriorityObject
|
||||
* @apiSuccess {Boolean} status Indicates if the request was successful
|
||||
* @apiSuccess {Boolean} success Indicates if the request was successful
|
||||
* @apiSuccess {Object} data Response object
|
||||
* @apiSuccess {Integer} data.id Priority ID
|
||||
* @apiSuccess {String} data.name Priority name
|
||||
* @apiSuccess {String} data.color Priority color (can be null)
|
||||
* @apiSuccess {String} data.created_at Creation timestamp
|
||||
* @apiSuccess {String} data.updated_at Update timestamp
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "id": 1,
|
||||
* "name": "Normal",
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-07-12T17:57:40.000000Z",
|
||||
* "color": null
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine ProjectParams
|
||||
* @apiParam {Object} [filters] Filters to apply to the project list.
|
||||
* @apiParam {Object} [filters.id] Filter by project ID.
|
||||
* @apiParam {String} [filters.name] Filter by project name.
|
||||
* @apiParam {String} [filters.description] Filter by project description.
|
||||
* @apiParam {String} [filters.created_at] Filter by project creation date.
|
||||
* @apiParam {String} [filters.updated_at] Filter by project update date.
|
||||
* @apiParam {Integer} [page=1] Page number for pagination.
|
||||
* @apiParam {Integer} [perPage=15] Number of items per page.
|
||||
*/
|
||||
/**
|
||||
* @apiDefine StatusObject
|
||||
* @apiSuccess {Number} id The ID of the status.
|
||||
* @apiSuccess {String} name The name of the status.
|
||||
* @apiSuccess {Boolean} active Indicates if the status is active.
|
||||
* @apiSuccess {String} color The color of the status (in HEX).
|
||||
* @apiSuccess {Number} order The sort order of the status.
|
||||
* @apiSuccess {String} created_at The creation timestamp.
|
||||
* @apiSuccess {String} updated_at The last update timestamp.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response Example:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "name": "Normal",
|
||||
* "active": true,
|
||||
* "color": "#363334",
|
||||
* "order": 1,
|
||||
* "created_at": "2024-08-26T10:47:30.000000Z",
|
||||
* "updated_at": "2024-08-26T10:48:35.000000Z"
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @apiDefine ParamTimeInterval
|
||||
*
|
||||
* @apiParam {String} start_at The start datetime for the interval
|
||||
* @apiParam {String} end_at The end datetime for the interval
|
||||
* @apiParam {Integer} user_id The ID of the user
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "start_at": "2024-08-16T12:32:11.000000Z",
|
||||
* "end_at": "2024-08-17T12:32:11.000000Z",
|
||||
* "user_id": 1
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @apiDefine AuthHeader
|
||||
* @apiHeader {String} Authorization Token for user auth
|
||||
* @apiHeaderExample {json} Authorization Header Example
|
||||
* {
|
||||
* "Authorization": "bearer 16184cf3b2510464a53c0e573c75740540fe..."
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @apiDefine 400Error
|
||||
* @apiError (Error 4xx) {String} message Message from server
|
||||
* @apiError (Error 4xx) {Boolean} success Indicates erroneous response when `FALSE`
|
||||
* @apiError (Error 4xx) {String} error_type Error type
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @apiDefine UnauthorizedError
|
||||
* @apiErrorExample {json} Unauthorized
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "message": "Not authorized",
|
||||
* "error_type": "authorization.unauthorized"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
/**
|
||||
* @apiDefine ForbiddenError
|
||||
* @apiErrorExample {json} Forbidden
|
||||
* HTTP/1.1 403 Forbidden
|
||||
* {
|
||||
* "message": "Access denied to this item",
|
||||
* "error_type": "authorization.forbidden"
|
||||
* }
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
*/
|
||||
/**
|
||||
* @apiDefine TotalSuccess
|
||||
* @apiSuccess {Boolean} success Indicates if the request was successful
|
||||
* @apiSuccess {Object} data The response data
|
||||
* @apiSuccess {Integer} data.total The total count of items
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "total": 0
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function _index(CattrFormRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
$itemsQuery = $this->getQuery($requestData);
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
|
||||
|
||||
$items = $request->header('X-Paginate', true) !== 'false' ? $itemsQuery->paginate() : $itemsQuery->get();
|
||||
|
||||
Filter::process(
|
||||
Filter::getActionFilterName(),
|
||||
$items,
|
||||
);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items, $requestData]);
|
||||
|
||||
return responder()->success($items)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getQuery(array $filter = []): Builder
|
||||
{
|
||||
$model = static::MODEL;
|
||||
$model = new $model;
|
||||
|
||||
$query = new Builder($model::getQuery());
|
||||
$query->setModel($model);
|
||||
|
||||
$modelScopes = $model->getGlobalScopes();
|
||||
|
||||
foreach ($modelScopes as $key => $value) {
|
||||
$query->withGlobalScope($key, $value);
|
||||
}
|
||||
|
||||
foreach (Filter::process(Filter::getQueryAdditionalRelationsFilterName(), []) as $with) {
|
||||
$query->with($with);
|
||||
}
|
||||
|
||||
foreach (Filter::process(Filter::getQueryAdditionalRelationsSumFilterName(), []) as $withSum) {
|
||||
$query->withSum(...$withSum);
|
||||
}
|
||||
|
||||
QueryHelper::apply($query, $model, $filter);
|
||||
|
||||
return Filter::process(
|
||||
Filter::getQueryFilterName(),
|
||||
$query
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function _create(CattrFormRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$requestData]);
|
||||
|
||||
/** @var Model $cls */
|
||||
$cls = static::MODEL;
|
||||
|
||||
$item = Filter::process(
|
||||
Filter::getActionFilterName(),
|
||||
$cls::create($requestData),
|
||||
);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$item, $requestData]);
|
||||
|
||||
return responder()->success($item)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function _edit(CattrFormRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
throw_unless(is_int($request->get('id')), ValidationException::withMessages(['Invalid id']));
|
||||
|
||||
$itemsQuery = $this->getQuery();
|
||||
|
||||
/** @var Model $item */
|
||||
$item = $itemsQuery->get()->collect()->firstWhere('id', $request->get('id'));
|
||||
|
||||
if (!$item) {
|
||||
/** @var Model $cls */
|
||||
$cls = static::MODEL;
|
||||
throw_if($cls::find($request->get('id'))?->count(), new AccessDeniedHttpException);
|
||||
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$item, $requestData]);
|
||||
|
||||
$item = Filter::process(Filter::getActionFilterName(), $item->fill($requestData));
|
||||
$item->save();
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$item, $requestData]);
|
||||
|
||||
return responder()->success($item)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function _destroy(CattrFormRequest $request): JsonResponse
|
||||
{
|
||||
$requestId = Filter::process(Filter::getRequestFilterName(), $request->validated('id'));
|
||||
|
||||
throw_unless(is_int($requestId), ValidationException::withMessages(['Invalid id']));
|
||||
|
||||
$itemsQuery = $this->getQuery(['where' => ['id' => $requestId]]);
|
||||
|
||||
/** @var Model $item */
|
||||
$item = $itemsQuery->first();
|
||||
if (!$item) {
|
||||
/** @var Model $cls */
|
||||
$cls = static::MODEL;
|
||||
throw_if($cls::find($requestId)?->count(), new AccessDeniedHttpException);
|
||||
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestId);
|
||||
|
||||
CatEvent::dispatch(
|
||||
Filter::getAfterActionEventName(),
|
||||
tap(
|
||||
Filter::process(Filter::getActionFilterName(), $item),
|
||||
static fn ($item) => $item->delete(),
|
||||
)
|
||||
);
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function _count(CattrFormRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
|
||||
|
||||
$itemsQuery = $this->getQuery($requestData);
|
||||
|
||||
$count = Filter::process(Filter::getActionFilterName(), $itemsQuery->count());
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$count, $requestData]);
|
||||
|
||||
return responder()->success(['total' => $count])->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function _show(CattrFormRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
$itemId = (int)$requestData['id'];
|
||||
|
||||
throw_unless($itemId, ValidationException::withMessages(['Invalid id']));
|
||||
|
||||
$filters = [
|
||||
'where' => ['id' => $itemId]
|
||||
];
|
||||
|
||||
if (!empty($requestData['with'])) {
|
||||
$filters['with'] = $requestData['with'];
|
||||
}
|
||||
|
||||
if (!empty($requestData['withSum'])) {
|
||||
$filters['withSum'] = $requestData['withSum'];
|
||||
}
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $filters);
|
||||
|
||||
$itemsQuery = $this->getQuery($filters ?: []);
|
||||
|
||||
$item = Filter::process(Filter::getActionFilterName(), $itemsQuery->first());
|
||||
|
||||
throw_unless($item, new NotFoundHttpException);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$item, $filters]);
|
||||
|
||||
return responder()->success($item)->respond();
|
||||
}
|
||||
}
|
||||
220
app/Http/Controllers/Api/PriorityController.php
Normal file
220
app/Http/Controllers/Api/PriorityController.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Requests\Priority\CreatePriorityRequest;
|
||||
use App\Http\Requests\Priority\DestroyPriorityRequest;
|
||||
use App\Http\Requests\Priority\ListPriorityRequest;
|
||||
use App\Http\Requests\Priority\ShowPriorityRequest;
|
||||
use App\Http\Requests\Priority\UpdatePriorityRequest;
|
||||
use App\Models\Priority;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Throwable;
|
||||
|
||||
class PriorityController extends ItemController
|
||||
{
|
||||
protected const MODEL = Priority::class;
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /priorities/show Show
|
||||
* @apiDescription Show priority.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Show Priority
|
||||
* @apiGroup Priority
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id Priority ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} res Priority
|
||||
*
|
||||
* @apiUse PriorityObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "res": {
|
||||
* "id": 1
|
||||
* "name": "Normal",
|
||||
* "color": null
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function show(ShowPriorityRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @api {get} /priorities/list List
|
||||
* @apiDescription Get list of priorities.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Priority List
|
||||
* @apiGroup Priority
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {Object} res Priority
|
||||
*
|
||||
* @apiUse PriorityObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "res": [{
|
||||
* "id": 1
|
||||
* "name": "Normal",
|
||||
* "color": null
|
||||
* }]
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function index(ListPriorityRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CreatePriorityRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
* @api {post} /priorities/create Create
|
||||
* @apiDescription Creates priority
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Create Priority
|
||||
* @apiGroup Priority
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {String} name Priority name
|
||||
* @apiParam {String} color Priority color
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "name": "Normal",
|
||||
* "color": null
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} res Priority
|
||||
*
|
||||
* @apiUse PriorityObject
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function create(CreatePriorityRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_create($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /priorities/edit Edit
|
||||
* @apiDescription Edit Priority
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Edit
|
||||
* @apiGroup Priority
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id ID
|
||||
* @apiParam {String} name Priority name
|
||||
* @apiParam {String} color Priority color
|
||||
*
|
||||
* @apiParamExample {json} Simple Request Example
|
||||
* {
|
||||
* "id": 1,
|
||||
* "name": "Normal",
|
||||
* "color": null
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} res Priority
|
||||
*
|
||||
* @apiUse PriorityObject
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
public function edit(UpdatePriorityRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_edit($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /priorities/remove Destroy
|
||||
* @apiDescription Destroy User
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Destroy Priority
|
||||
* @apiGroup Priority
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id ID of the target priority
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message Destroy status
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "message": "Item has been removed"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function destroy(DestroyPriorityRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_destroy($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /api/priorities/count Count Priorities
|
||||
* @apiDescription Returns the total count of priorities
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName CountPriorities
|
||||
* @apiGroup Priorities
|
||||
*
|
||||
* @apiUse TotalSuccess
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @param ListPriorityRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function count(ListPriorityRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_count($request);
|
||||
}
|
||||
}
|
||||
521
app/Http/Controllers/Api/ProjectController.php
Normal file
521
app/Http/Controllers/Api/ProjectController.php
Normal file
@@ -0,0 +1,521 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Requests\Project\CreateProjectRequest;
|
||||
use App\Http\Requests\Project\EditProjectRequest;
|
||||
use App\Http\Requests\Project\DestroyProjectRequest;
|
||||
use App\Http\Requests\Project\GanttDataRequest;
|
||||
use App\Http\Requests\Project\ListProjectRequest;
|
||||
use App\Http\Requests\Project\PhasesRequest;
|
||||
use App\Http\Requests\Project\ShowProjectRequest;
|
||||
use CatEvent;
|
||||
use Filter;
|
||||
use App\Models\Project;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use DB;
|
||||
use Staudenmeir\LaravelAdjacencyList\Eloquent\Builder as AdjacencyListBuilder;
|
||||
use Throwable;
|
||||
|
||||
class ProjectController extends ItemController
|
||||
{
|
||||
protected const MODEL = Project::class;
|
||||
|
||||
/**
|
||||
* @api {get, post} /projects/list List
|
||||
* @apiDescription Get list of Projects
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetProjectList
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission projects_list
|
||||
* @apiPermission projects_full_access
|
||||
*
|
||||
* @apiUse ProjectParams
|
||||
*
|
||||
* @apiParamExample {json} Simple Request Example
|
||||
* {
|
||||
* "id": [">", 1],
|
||||
* "user_id": ["=", [1,2,3]],
|
||||
* "name": ["like", "%lorem%"],
|
||||
* "description": ["like", "%lorem%"],
|
||||
* "created_at": [">", "2019-01-01 00:00:00"],
|
||||
* "updated_at": ["<", "2019-01-01 00:00:00"]
|
||||
* }
|
||||
*
|
||||
* @apiUse ProjectObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "company_id": 1,
|
||||
* "name": "Dolores voluptates.",
|
||||
* "description": "Deleniti maxime fugit nesciunt. Ut maiores deleniti tempora vel. Nisi aut doloremque accusantium tempore aut.",
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "important": 1,
|
||||
* "source": "internal",
|
||||
* "default_priority_id": null
|
||||
* },
|
||||
* {
|
||||
* "id": 2,
|
||||
* "company_id": 5,
|
||||
* "name": "Et veniam velit tempore.",
|
||||
* "description": "Consequatur nulla distinctio reprehenderit rerum omnis debitis. Fugit illum ratione quia harum. Optio porro consequatur enim esse.",
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:42.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:42.000000Z",
|
||||
* "important": 1,
|
||||
* "source": "internal",
|
||||
* "default_priority_id": null
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
/**
|
||||
* @param ListProjectRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function index(ListProjectRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /projects/gantt-data Gantt Data
|
||||
* @apiDescription Получение данных для диаграммы Ганта по проекту
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetGanttData
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
* @apiUse ProjectIDParam
|
||||
*
|
||||
* @apiSuccess {Integer} id Project ID
|
||||
* @apiSuccess {Integer} company_id Company ID
|
||||
* @apiSuccess {String} name Project name
|
||||
* @apiSuccess {String} description Project description
|
||||
* @apiSuccess {String} deleted_at Deletion date (null if not deleted)
|
||||
* @apiSuccess {String} created_at Creation date
|
||||
* @apiSuccess {String} updated_at Update date
|
||||
* @apiSuccess {Integer} important Project importance (1 - important, 0 - not important)
|
||||
* @apiSuccess {String} source Project source (internal/external)
|
||||
* @apiSuccess {Integer} default_priority_id Default priority ID (null if not set)
|
||||
* @apiSuccess {Object[]} tasks_relations Task relations
|
||||
* @apiSuccess {Integer} tasks_relations.parent_id Parent task ID
|
||||
* @apiSuccess {Integer} tasks_relations.child_id Child task ID
|
||||
* @apiSuccess {Object[]} tasks List of tasks
|
||||
* @apiSuccess {Object[]} phases List of project phases
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "company_id": 1,
|
||||
* "name": "Dolores voluptates.",
|
||||
* "description": "Deleniti maxime fugit nesciunt. Ut maiores deleniti tempora vel. Nisi aut doloremque accusantium tempore aut.",
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "important": 1,
|
||||
* "source": "internal",
|
||||
* "default_priority_id": null,
|
||||
* "tasks_relations": [
|
||||
* {
|
||||
* "parent_id": 5,
|
||||
* "child_id": 1
|
||||
* }
|
||||
* ],
|
||||
* "tasks": [],
|
||||
* "phases": []
|
||||
*
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @param GanttDataRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function ganttData(GanttDataRequest $request): JsonResponse
|
||||
{
|
||||
|
||||
Filter::listen(Filter::getQueryFilterName(), static fn(Builder $query) => $query->with([
|
||||
'tasks' => fn(HasMany $queue) => $queue
|
||||
->orderBy('start_date')
|
||||
->select([
|
||||
'id',
|
||||
'task_name',
|
||||
'priority_id',
|
||||
'status_id',
|
||||
'estimate',
|
||||
'start_date',
|
||||
'due_date',
|
||||
'project_phase_id',
|
||||
'project_id'
|
||||
])->with(['status', 'priority'])
|
||||
->withSum(['workers as total_spent_time'], 'duration')
|
||||
->withSum(['workers as total_offset'], 'offset')
|
||||
->withCasts(['start_date' => 'string', 'due_date' => 'string'])
|
||||
->whereNotNull('start_date')->whereNotNull('due_date'),
|
||||
'phases' => fn(HasMany $queue) => $queue
|
||||
->select(['id', 'name', 'project_id'])
|
||||
->withMin([
|
||||
'tasks as start_date' => fn(AdjacencyListBuilder $q) => $q
|
||||
->whereNotNull('start_date')
|
||||
->whereNotNull('due_date')
|
||||
], 'start_date')
|
||||
->withMax([
|
||||
'tasks as due_date' => fn(AdjacencyListBuilder $q) => $q
|
||||
->whereNotNull('start_date')
|
||||
->whereNotNull('due_date')
|
||||
], 'due_date'),
|
||||
]));
|
||||
|
||||
Filter::listen(Filter::getActionFilterName(), static function (Project $item) {
|
||||
$item->append('tasks_relations');
|
||||
return $item;
|
||||
});
|
||||
|
||||
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /projects/phases Project Phases
|
||||
* @apiDescription Retrieve project phases along with the number of tasks in each phase.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetProjectPhases
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
* @apiUse ProjectIDParam
|
||||
* @apiUse ProjectObject
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param PhasesRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function phases(PhasesRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(
|
||||
Filter::getQueryFilterName(),
|
||||
static fn(Builder $query) => $query
|
||||
->with([
|
||||
'phases'=> fn(HasMany $q) => $q->withCount('tasks')
|
||||
])
|
||||
);
|
||||
|
||||
return $this->_show($request);
|
||||
}
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {get} /projects/show Project Show
|
||||
* @apiDescription Retrieve project show along with the number of tasks in each phase.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetProjectShow
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
* @apiUse ProjectIDParam
|
||||
* @apiUse ProjectObject
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function show(ShowProjectRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(
|
||||
Filter::getQueryFilterName(),
|
||||
static fn(Builder $query) => $query
|
||||
->with([
|
||||
'phases'=> fn(HasMany $q) => $q->withCount('tasks')
|
||||
])
|
||||
);
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /projects/create Create Project
|
||||
* @apiDescription Creates a new project
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName CreateProject
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Boolean} important Project importance
|
||||
* @apiParam {Integer} screenshots_state State of the screenshots
|
||||
* @apiParam {String} name Project name
|
||||
* @apiParam {String} description Project description
|
||||
* @apiParam {Integer} default_priority_id Default priority ID
|
||||
* @apiParam {Object[]} statuses Project statuses
|
||||
* @apiParam {Integer} statuses.id Status ID
|
||||
* @apiParam {String} statuses.color Status color
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "important": true,
|
||||
* "screenshots_state": 1,
|
||||
* "name": "test",
|
||||
* "description": "test",
|
||||
* "default_priority_id": 2,
|
||||
* "statuses": [
|
||||
* {
|
||||
* "id": 2,
|
||||
* "color": null
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} name Project name
|
||||
* @apiSuccess {String} description Project description
|
||||
* @apiSuccess {Boolean} important Project importance
|
||||
* @apiSuccess {Integer} default_priority_id Default priority ID
|
||||
* @apiSuccess {Integer} screenshots_state State of the screenshots
|
||||
* @apiSuccess {String} created_at Creation timestamp
|
||||
* @apiSuccess {String} updated_at Update timestamp
|
||||
* @apiSuccess {Object[]} statuses Project statuses
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "name": "test",
|
||||
* "description": "test",
|
||||
* "important": 1,
|
||||
* "default_priority_id": 2,
|
||||
* "screenshots_state": 1,
|
||||
* "updated_at": "2024-08-06T12:28:07.000000Z",
|
||||
* "created_at": "2024-08-06T12:28:07.000000Z",
|
||||
* "id": 161,
|
||||
* "statuses": []
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
public function create(CreateProjectRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(Filter::getRequestFilterName(), static function ($requestData) {
|
||||
if (isset($requestData['group']) && is_array($requestData['group'])) {
|
||||
$requestData['group'] = $requestData['group']['id'];
|
||||
}
|
||||
|
||||
return $requestData;
|
||||
});
|
||||
|
||||
CatEvent::listen(Filter::getAfterActionEventName(), static function (Project $project, $requestData) use ($request) {
|
||||
if ($request->has('statuses')) {
|
||||
$statuses = [];
|
||||
foreach ($request->get('statuses') as $status) {
|
||||
$statuses[$status['id']] = ['color' => $status['color']];
|
||||
}
|
||||
|
||||
$project->statuses()->sync($statuses);
|
||||
}
|
||||
|
||||
if (isset($requestData['phases'])) {
|
||||
$project->phases()->createMany($requestData['phases']);
|
||||
}
|
||||
});
|
||||
|
||||
Filter::listen(Filter::getActionFilterName(), static fn($data) => $data->load('statuses'));
|
||||
|
||||
return $this->_create($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /projects/edit Edit
|
||||
* @apiDescription Edit Project
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName EditProject
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission projects_edit
|
||||
* @apiPermission projects_full_access
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1,
|
||||
* "name": "test",
|
||||
* "description": "test"
|
||||
* }
|
||||
*
|
||||
* @apiParam {String} id Project id
|
||||
* @apiParam {String} name Project name
|
||||
* @apiParam {String} description Project description
|
||||
*
|
||||
* @apiSuccess {Integer} id Project ID
|
||||
* @apiSuccess {Integer} company_id Company ID
|
||||
* @apiSuccess {String} name Project name
|
||||
* @apiSuccess {String} description Project description
|
||||
* @apiSuccess {String} deleted_at Deletion timestamp
|
||||
* @apiSuccess {String} created_at Creation timestamp
|
||||
* @apiSuccess {String} updated_at Update timestamp
|
||||
* @apiSuccess {Boolean} important Project importance
|
||||
* @apiSuccess {String} source Project source
|
||||
* @apiSuccess {Integer} default_priority_id Default priority ID
|
||||
* @apiSuccess {Integer} screenshots_state State of the screenshots
|
||||
* @apiSuccess {Object[]} statuses Project statuses
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* {
|
||||
* "id": 1,
|
||||
* "company_id": 1,
|
||||
* "name": "test",
|
||||
* "description": "test",
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-08-07T16:47:01.000000Z",
|
||||
* "important": 1,
|
||||
* "source": "internal",
|
||||
* "default_priority_id": null,
|
||||
* "screenshots_state": 1,
|
||||
* "statuses": []
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
public function edit(EditProjectRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(Filter::getRequestFilterName(), static function ($requestData) {
|
||||
if (isset($requestData['group']) && is_array($requestData['group'])) {
|
||||
$requestData['group'] = $requestData['group']['id'];
|
||||
}
|
||||
|
||||
return $requestData;
|
||||
});
|
||||
|
||||
CatEvent::listen(Filter::getAfterActionEventName(), static function (Project $project, $requestData) use ($request) {
|
||||
if ($request->has('statuses')) {
|
||||
$statuses = [];
|
||||
foreach ($request->get('statuses') as $status) {
|
||||
$statuses[$status['id']] = ['color' => $status['color']];
|
||||
}
|
||||
|
||||
$project->statuses()->sync($statuses);
|
||||
}
|
||||
|
||||
if (isset($requestData['phases'])) {
|
||||
$phases = collect($requestData['phases']);
|
||||
$project->phases()
|
||||
->whereNotIn('id', $phases->pluck('id')->filter())
|
||||
->delete();
|
||||
$project->phases()->upsert(
|
||||
$phases->filter(fn (array $val) => isset($val['id']))->toArray(),
|
||||
['id'],
|
||||
['name']
|
||||
);
|
||||
$project->phases()->createMany($phases->filter(fn (array $val) => !isset($val['id'])));
|
||||
}
|
||||
});
|
||||
|
||||
Filter::listen(Filter::getActionFilterName(), static fn($data) => $data->load('statuses'));
|
||||
|
||||
return $this->_edit($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /projects/remove Destroy
|
||||
* @apiDescription Destroy Project
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName DestroyProject
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission projects_remove
|
||||
* @apiPermission projects_full_access
|
||||
*
|
||||
* @apiParam {String} id Project id
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message Destroy status
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 No Content
|
||||
* {
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
public function destroy(DestroyProjectRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_destroy($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @api {get,post} /projects/count Count
|
||||
* @apiDescription Count Projects
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Count
|
||||
* @apiGroup Project
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission projects_count
|
||||
* @apiPermission projects_full_access
|
||||
*
|
||||
* @apiSuccess {Integer} total Amount of projects that we have
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "total": 159
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function count(ListProjectRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_count($request);
|
||||
}
|
||||
}
|
||||
155
app/Http/Controllers/Api/ProjectGroupController.php
Normal file
155
app/Http/Controllers/Api/ProjectGroupController.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\QueryHelper;
|
||||
use App\Http\Requests\ProjectGroup\CreateProjectGroupRequest;
|
||||
use App\Http\Requests\ProjectGroup\DestroyProjectGroupRequest;
|
||||
use App\Http\Requests\ProjectGroup\EditProjectGroupRequest;
|
||||
use App\Http\Requests\ProjectGroup\ListProjectGroupRequest;
|
||||
use App\Http\Requests\ProjectGroup\ShowProjectGroupRequest;
|
||||
use App\Models\ProjectGroup;
|
||||
use CatEvent;
|
||||
use Exception;
|
||||
use Filter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Kalnoy\Nestedset\QueryBuilder;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class ProjectGroupController extends ItemController
|
||||
{
|
||||
protected const MODEL = ProjectGroup::class;
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @param ListProjectGroupRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function index(ListProjectGroupRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
$itemsQuery = $this->getQuery($requestData);
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
|
||||
|
||||
$itemsQuery->withDepth()->withCount('projects')->defaultOrder();
|
||||
|
||||
$items = $request->header('X-Paginate', true) !== 'false' ? $itemsQuery->paginate($request->input('limit', null)) : $itemsQuery->get();
|
||||
|
||||
Filter::process(
|
||||
Filter::getActionFilterName(),
|
||||
$items,
|
||||
);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items, $requestData]);
|
||||
|
||||
return responder()->success($items)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getQuery(array $filter = []): QueryBuilder
|
||||
{
|
||||
$model = static::MODEL;
|
||||
$model = new $model;
|
||||
|
||||
$query = new QueryBuilder($model::getQuery());
|
||||
$query->setModel($model);
|
||||
|
||||
$modelScopes = $model->getGlobalScopes();
|
||||
|
||||
foreach ($modelScopes as $key => $value) {
|
||||
$query->withGlobalScope($key, $value);
|
||||
}
|
||||
|
||||
foreach (Filter::process(Filter::getQueryAdditionalRelationsFilterName(), []) as $with) {
|
||||
$query->with($with);
|
||||
}
|
||||
|
||||
QueryHelper::apply($query, $model, $filter);
|
||||
|
||||
return Filter::process(
|
||||
Filter::getQueryFilterName(),
|
||||
$query
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @param CreateProjectGroupRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function create(CreateProjectGroupRequest $request): JsonResponse
|
||||
{
|
||||
if ($parent_id = $request->safe(['parent_id'])['parent_id'] ?? null) {
|
||||
CatEvent::listen(
|
||||
Filter::getAfterActionEventName(),
|
||||
static fn(ProjectGroup $group) => $group->parent()->associate($parent_id)->save(),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->_create($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param ShowProjectGroupRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function show(ShowProjectGroupRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param EditProjectGroupRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function edit(EditProjectGroupRequest $request): JsonResponse
|
||||
{
|
||||
CatEvent::listen(
|
||||
Filter::getAfterActionEventName(),
|
||||
static function (ProjectGroup $group) use ($request) {
|
||||
if ($parent_id = $request->input('parent_id', null)) {
|
||||
$group->parent()->associate($parent_id)->save();
|
||||
} else {
|
||||
$group->saveAsRoot();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return $this->_edit($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param DestroyProjectGroupRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function destroy(DestroyProjectGroupRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_destroy($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function count(ListProjectGroupRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_count($request);
|
||||
}
|
||||
}
|
||||
237
app/Http/Controllers/Api/ProjectMemberController.php
Normal file
237
app/Http/Controllers/Api/ProjectMemberController.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ProjectMember\BulkEditProjectMemberRequest;
|
||||
use App\Http\Requests\ProjectMember\ShowProjectMemberRequest;
|
||||
use App\Services\ProjectMemberService;
|
||||
use CatEvent;
|
||||
use Filter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
class ProjectMemberController extends Controller
|
||||
{
|
||||
/**
|
||||
* @api {post} /project-members/list List Project Members
|
||||
* @apiDescription Get list of project members
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName ListProjectMembers
|
||||
* @apiGroup ProjectMember
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} project_id ID of the project
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "project_id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Integer} id Project ID
|
||||
* @apiSuccess {Object[]} users List of users
|
||||
* @apiSuccess {Integer} users.id User ID
|
||||
* @apiSuccess {String} users.full_name User full name
|
||||
* @apiSuccess {String} users.email User email
|
||||
* @apiSuccess {String} users.url User URL
|
||||
* @apiSuccess {Integer} users.company_id Company ID
|
||||
* @apiSuccess {String} users.avatar User avatar
|
||||
* @apiSuccess {Integer} users.screenshots_active Screenshots active status
|
||||
* @apiSuccess {Integer} users.manual_time Manual time status
|
||||
* @apiSuccess {Integer} users.computer_time_popup Computer time popup interval
|
||||
* @apiSuccess {Boolean} users.blur_screenshots Blur screenshots status
|
||||
* @apiSuccess {Boolean} users.web_and_app_monitoring Web and app monitoring status
|
||||
* @apiSuccess {Integer} users.screenshots_interval Screenshots interval
|
||||
* @apiSuccess {Boolean} users.active User active status
|
||||
* @apiSuccess {String} users.deleted_at Deletion timestamp
|
||||
* @apiSuccess {String} users.created_at Creation timestamp
|
||||
* @apiSuccess {String} users.updated_at Last update timestamp
|
||||
* @apiSuccess {String} users.timezone User timezone
|
||||
* @apiSuccess {Boolean} users.important User importance status
|
||||
* @apiSuccess {Boolean} users.change_password Change password status
|
||||
* @apiSuccess {Integer} users.role_id User role ID
|
||||
* @apiSuccess {String} users.user_language User language
|
||||
* @apiSuccess {String} users.type User type
|
||||
* @apiSuccess {Boolean} users.invitation_sent Invitation sent status
|
||||
* @apiSuccess {Integer} users.nonce User nonce
|
||||
* @apiSuccess {Boolean} users.client_installed Client installed status
|
||||
* @apiSuccess {Boolean} users.permanent_screenshots Permanent screenshots status
|
||||
* @apiSuccess {String} users.last_activity Last activity timestamp
|
||||
* @apiSuccess {Boolean} users.online Online status
|
||||
* @apiSuccess {Boolean} users.can_view_team_tab Can view team tab status
|
||||
* @apiSuccess {Boolean} users.can_create_task Can create task status
|
||||
* @apiSuccess {Object} users.pivot Pivot data
|
||||
* @apiSuccess {Integer} users.pivot.project_id Project ID in pivot
|
||||
* @apiSuccess {Integer} users.pivot.user_id User ID in pivot
|
||||
* @apiSuccess {Integer} users.pivot.role_id Role ID in pivot
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "users": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "full_name": "Admin",
|
||||
* "email": "admin@cattr.app",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_active": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-02-15T19:06:42.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 0,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2023-10-26 10:26:17",
|
||||
* "online": false,
|
||||
* "can_view_team_tab": true,
|
||||
* "can_create_task": true,
|
||||
* "pivot": {
|
||||
* "project_id": 1,
|
||||
* "user_id": 1,
|
||||
* "role_id": 2
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* "id": 2,
|
||||
* "full_name": "Fabiola Mertz",
|
||||
* "email": "projectManager@example.com",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_active": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 2,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2023-10-26 09:44:17",
|
||||
* "online": false,
|
||||
* "can_view_team_tab": false,
|
||||
* "can_create_task": false,
|
||||
* "pivot": {
|
||||
* "project_id": 1,
|
||||
* "user_id": 2,
|
||||
* "role_id": 2
|
||||
* }
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* @param ShowProjectMemberRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function list(ShowProjectMemberRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
throw_unless($data, ValidationException::withMessages([]));
|
||||
|
||||
$projectMembers = ProjectMemberService::getMembers($data['project_id']);
|
||||
|
||||
$projectMembers['users'] = $projectMembers['users'] ?? [];
|
||||
|
||||
return responder()->success($projectMembers)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /api/project-members/bulk-edit Bulk Edit Project Members
|
||||
* @apiDescription Edit roles of multiple project members
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName BulkEditProjectMembers
|
||||
* @apiGroup ProjectMember
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} project_id Project ID
|
||||
* @apiParam {Object[]} user_roles Array of user roles
|
||||
* @apiParam {Integer} user_roles.user_id User ID
|
||||
* @apiParam {Integer} user_roles.role_id Role ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "project_id": 1,
|
||||
* "user_roles": [
|
||||
* {
|
||||
* "user_id": 1,
|
||||
* "role_id": 2
|
||||
* },
|
||||
* {
|
||||
* "user_id": 2,
|
||||
* "role_id": 3
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 No Content
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @param BulkEditProjectMemberRequest $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function bulkEdit(BulkEditProjectMemberRequest $request): JsonResponse
|
||||
{
|
||||
$data = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
|
||||
$userRoles = [];
|
||||
|
||||
foreach ($data['user_roles'] as $value) {
|
||||
$userRoles[$value['user_id']] = ['role_id' => $value['role_id']];
|
||||
}
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$data['project_id'], $userRoles]);
|
||||
|
||||
ProjectMemberService::syncMembers($data['project_id'], $userRoles);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$data['project_id'], $userRoles]);
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
}
|
||||
193
app/Http/Controllers/Api/Reports/DashboardController.php
Normal file
193
app/Http/Controllers/Api/Reports/DashboardController.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Reports;
|
||||
|
||||
use App\Enums\DashboardSortBy;
|
||||
use App\Enums\SortDirection;
|
||||
use App\Helpers\ReportHelper;
|
||||
use App\Http\Requests\Reports\DashboardRequest;
|
||||
use App\Jobs\GenerateAndSendReport;
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use App\Reports\DashboardExport;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Settings;
|
||||
use Throwable;
|
||||
|
||||
class DashboardController
|
||||
{
|
||||
/**
|
||||
* @api {post} /report/dashboard Dashboard Data
|
||||
* @apiDescription Retrieve dashboard data based on provided parameters.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName DashboardData
|
||||
* @apiGroup Dashboard
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {String} start_at Start date-time in "Y-m-d H:i:s" format.
|
||||
* @apiParam {String} end_at End date-time in "Y-m-d H:i:s" format.
|
||||
* @apiParam {String} user_timezone User's timezone.
|
||||
* @apiParam {Array} [users] Array of user IDs. If not provided, all users are considered.
|
||||
* @apiParam {Array} [projects] Array of project IDs. If not provided, all projects are considered.
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "start_at": "2006-05-31 16:15:09",
|
||||
* "end_at": "2006-05-31 16:20:07",
|
||||
* "user_timezone": "Asia/Omsk"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data Dashboard data keyed by user ID.
|
||||
* @apiSuccess {Array} data.7 Array of records for user with ID 7.
|
||||
* @apiSuccess {String} data.7.start_at Start date-time of the record.
|
||||
* @apiSuccess {Integer} data.7.activity_fill Activity fill percentage.
|
||||
* @apiSuccess {Integer} data.7.mouse_fill Mouse activity fill percentage.
|
||||
* @apiSuccess {Integer} data.7.keyboard_fill Keyboard activity fill percentage.
|
||||
* @apiSuccess {String} data.7.end_at End date-time of the record.
|
||||
* @apiSuccess {Boolean} data.7.is_manual Indicates if the record is manual.
|
||||
* @apiSuccess {String} data.7.user_email User's email address.
|
||||
* @apiSuccess {Integer} data.7.id Record ID.
|
||||
* @apiSuccess {Integer} data.7.project_id Project ID.
|
||||
* @apiSuccess {String} data.7.project_name Project name.
|
||||
* @apiSuccess {Integer} data.7.task_id Task ID.
|
||||
* @apiSuccess {String} data.7.task_name Task name.
|
||||
* @apiSuccess {Integer} data.7.user_id User ID.
|
||||
* @apiSuccess {String} data.7.full_name User's full name.
|
||||
* @apiSuccess {Integer} data.7.duration Duration in seconds.
|
||||
* @apiSuccess {Integer} data.7.from_midnight Time from midnight in seconds.
|
||||
* @apiSuccess {Object} data.7.durationByDay Duration grouped by day.
|
||||
* @apiSuccess {Integer} data.7.durationByDay.2018-05-31 Duration for the day 2018-05-31.
|
||||
* @apiSuccess {Integer} data.7.durationByDay.2018-06-04 Duration for the day 2018-06-04.
|
||||
* @apiSuccess {Integer} data.7.durationAtSelectedPeriod Duration for the selected period.
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "7": [
|
||||
* {
|
||||
* "start_at": "2018-05-31 10:43:45",
|
||||
* "activity_fill": 111,
|
||||
* "mouse_fill": 64,
|
||||
* "keyboard_fill": 47,
|
||||
* "end_at": "2018-06-03 22:03:45",
|
||||
* "is_manual": 0,
|
||||
* "user_email": "projectManager1231@example.com",
|
||||
* "id": 2109,
|
||||
* "project_id": 159,
|
||||
* "project_name": "Voluptas ab et ea.",
|
||||
* "task_id": 54,
|
||||
* "task_name": "Quo consequatur mollitia nam.",
|
||||
* "user_id": 7,
|
||||
* "full_name": "Dr. Adaline Toy",
|
||||
* "duration": 300000,
|
||||
* "from_midnight": 38625,
|
||||
* "durationByDay": {
|
||||
* "2018-05-31": 285375,
|
||||
* "2018-06-04": 14625
|
||||
* },
|
||||
* "durationAtSelectedPeriod": 14625
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function __invoke(DashboardRequest $request): JsonResponse
|
||||
{
|
||||
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
|
||||
|
||||
return responder()->success(
|
||||
DashboardExport::init(
|
||||
$request->input('users') ?? User::all()->pluck('id')->toArray(),
|
||||
$request->input('projects') ?? Project::all()->pluck('id')->toArray(),
|
||||
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
|
||||
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
|
||||
$companyTimezone,
|
||||
$request->input('user_timezone'),
|
||||
)->collection()->all(),
|
||||
)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /report/dashboard/download Download Dashboard Report
|
||||
* @apiDescription Generate and download a dashboard report
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName DownloadDashboardReport
|
||||
* @apiGroup Report
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiHeader {String} Accept Specifies the content type of the response. (Example: `text/csv`)
|
||||
* @apiHeader {String} Authorization Bearer token for API access. (Example: `82|LosbyrFljFDJqUcqMNG6UveCgrclt6OzTrCWdnJBEZ1fee08e6`)
|
||||
* @apiPermission report_generate
|
||||
* @apiPermission report_full_access
|
||||
*
|
||||
* @apiParam {String} start_at Start date and time (ISO 8601 format)
|
||||
* @apiParam {String} end_at End date and time (ISO 8601 format)
|
||||
* @apiParam {String} user_timezone User's timezone
|
||||
* @apiParam {String} sort_column Column to sort by
|
||||
* @apiParam {String} sort_direction Direction to sort (asc/desc)
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "start_at": "2024-08-06T18:00:00.000Z",
|
||||
* "end_at": "2024-08-07T17:59:59.999Z",
|
||||
* "user_timezone": "Asia/Omsk",
|
||||
* "sort_column": "user",
|
||||
* "sort_direction": "asc",
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} url URL to download the generated report
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "url": "/storage/reports/f7ac500e-a741-47ee-9e61-1b62a341fb8d/Dashboard_Report.csv"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
public function download(DashboardRequest $request): JsonResponse
|
||||
{
|
||||
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
|
||||
|
||||
$job = new GenerateAndSendReport(
|
||||
DashboardExport::init(
|
||||
$request->input('users') ?? User::all()->pluck('id')->toArray(),
|
||||
$request->input('projects') ?? Project::all()->pluck('id')->toArray(),
|
||||
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
|
||||
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
|
||||
$companyTimezone,
|
||||
$request->input('user_timezone'),
|
||||
DashboardSortBy::tryFrom($request->input('sort_column')),
|
||||
SortDirection::tryFrom($request->input('sort_direction')),
|
||||
),
|
||||
$request->user(),
|
||||
ReportHelper::getReportFormat($request),
|
||||
);
|
||||
|
||||
app(Dispatcher::class)->dispatchSync($job);
|
||||
|
||||
return responder()->success(['url' => $job->getPublicPath()])->respond();
|
||||
}
|
||||
}
|
||||
159
app/Http/Controllers/Api/Reports/PlannedTimeReportController.php
Normal file
159
app/Http/Controllers/Api/Reports/PlannedTimeReportController.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Reports;
|
||||
|
||||
use App\Helpers\ReportHelper;
|
||||
use App\Http\Requests\Reports\PlannedTimeReportRequest;
|
||||
use App\Jobs\GenerateAndSendReport;
|
||||
use App\Models\Project;
|
||||
use App\Reports\PlannedTimeReportExport;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Throwable;
|
||||
|
||||
class PlannedTimeReportController
|
||||
{
|
||||
/**
|
||||
* @api {post} /report/planned-time Planned Time Report
|
||||
* @apiDescription Generate a report on planned tasks and associated time for a given project
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName PlannedTimeReport
|
||||
* @apiGroup Report
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission report_generate
|
||||
* @apiPermission report_full_access
|
||||
*
|
||||
* @apiParam {Integer} id Project ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} reportData Report data object
|
||||
* @apiSuccess {Integer} reportData.id Project ID
|
||||
* @apiSuccess {Integer} reportData.company_id Company ID
|
||||
* @apiSuccess {String} reportData.name Project name
|
||||
* @apiSuccess {String} reportData.description Project description
|
||||
* @apiSuccess {String} reportData.deleted_at Deleted timestamp (null if not deleted)
|
||||
* @apiSuccess {String} reportData.created_at Creation timestamp
|
||||
* @apiSuccess {String} reportData.updated_at Update timestamp
|
||||
* @apiSuccess {Boolean} reportData.important Whether the project is marked as important
|
||||
* @apiSuccess {String} reportData.source Source of the project (e.g., "internal")
|
||||
* @apiSuccess {Integer} reportData.screenshots_state Screenshots state (1 if active)
|
||||
* @apiSuccess {Integer} reportData.default_priority_id Default priority ID (null if not set)
|
||||
* @apiSuccess {Integer} reportData.total_spent_time Total time spent on the project
|
||||
* @apiSuccess {Object[]} reportData.tasks List of tasks under the project
|
||||
* @apiSuccess {Integer} reportData.tasks.id Task ID
|
||||
* @apiSuccess {String} reportData.tasks.task_name Task name
|
||||
* @apiSuccess {String} reportData.tasks.due_date Task due date (null if not set)
|
||||
* @apiSuccess {String} reportData.tasks.estimate Estimated time for the task (null if not set)
|
||||
* @apiSuccess {Integer} reportData.tasks.project_id Project ID to which the task belongs
|
||||
* @apiSuccess {Integer} reportData.tasks.total_spent_time Total time spent on the task
|
||||
* @apiSuccess {Object[]} reportData.tasks.workers List of workers assigned to the task
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "reportData": [
|
||||
* {
|
||||
* "id": 2,
|
||||
* "company_id": 5,
|
||||
* "name": "Et veniam velit tempore.",
|
||||
* "description": "Consequatur nulla distinctio reprehenderit rerum omnis debitis. Fugit illum ratione quia harum. Optio porro consequatur enim esse.",
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:42.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:42.000000Z",
|
||||
* "important": 1,
|
||||
* "source": "internal",
|
||||
* "default_priority_id": null,
|
||||
* "screenshots_state": 1,
|
||||
* "total_spent_time": null,
|
||||
* "tasks": [
|
||||
* {
|
||||
* "id": 11,
|
||||
* "task_name": "Qui velit fugiat magni accusantium.",
|
||||
* "due_date": null,
|
||||
* "estimate": null,
|
||||
* "project_id": 2,
|
||||
* "total_spent_time": null,
|
||||
* "workers": []
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
public function __invoke(PlannedTimeReportRequest $request): JsonResponse
|
||||
{
|
||||
return responder()->success(
|
||||
PlannedTimeReportExport::init(
|
||||
$request->input('projects', Project::all()->pluck('id')->toArray()),
|
||||
)->collection()->all(),
|
||||
)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /report/planned-time/download Download Planned Time Report
|
||||
* @apiDescription Generate and download a report on planned time for specific projects.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName DownloadPlannedTimeReport
|
||||
* @apiGroup Report
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiHeader {String} Accept Specifies the content type of the response. (Example: `text/csv`)
|
||||
* @apiHeader {String} Authorization Bearer token for API access. (Example: `82|LosbyrFljFDJqUcqMNG6UveCgrclt6OzTrCWdnJBEZ1fee08e6`)
|
||||
* @apiPermission report_generate
|
||||
* @apiPermission report_full_access
|
||||
*
|
||||
* @apiParam {Array} projects Array of project IDs to include in the report. If not provided, all projects will be included.
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "projects": [2]
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} url URL to the generated report file.
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "url": "/storage/reports/0611766a-2807-4524-9add-2e8be33c3e58/PlannedTime_Report.csv"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function download(PlannedTimeReportRequest $request): JsonResponse
|
||||
{
|
||||
$job = new GenerateAndSendReport(
|
||||
PlannedTimeReportExport::init(
|
||||
$request->input('projects', Project::all()->pluck('id')->toArray()),
|
||||
),
|
||||
$request->user(),
|
||||
ReportHelper::getReportFormat($request),
|
||||
);
|
||||
|
||||
app(Dispatcher::class)->dispatchSync($job);
|
||||
|
||||
return responder()->success(['url' => $job->getPublicPath()])->respond();
|
||||
}
|
||||
}
|
||||
228
app/Http/Controllers/Api/Reports/ProjectReportController.php
Normal file
228
app/Http/Controllers/Api/Reports/ProjectReportController.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Reports;
|
||||
|
||||
use App\Helpers\ReportHelper;
|
||||
use App\Http\Requests\Reports\ProjectReportRequest;
|
||||
use App\Jobs\GenerateAndSendReport;
|
||||
use App\Models\User;
|
||||
use App\Models\Project;
|
||||
use App\Reports\ProjectReportExport;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Settings;
|
||||
use Throwable;
|
||||
|
||||
class ProjectReportController
|
||||
{
|
||||
/**
|
||||
* @api {post} /report/project Project Report
|
||||
* @apiDescription Retrieve detailed project report data including user tasks and time intervals.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName ProjectReport
|
||||
* @apiGroup Report
|
||||
* @apiUse AuthHeader
|
||||
* @apiPermission report_view
|
||||
* @apiPermission report_full_access
|
||||
*
|
||||
* @apiParam {String} start_at The start date and time for the report period (ISO 8601 format).
|
||||
* @apiParam {String} end_at The end date and time for the report period (ISO 8601 format).
|
||||
* @apiParam {String} user_timezone The timezone of the user making the request.
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "start_at": "2023-05-31 16:15:09",
|
||||
* "end_at": "2023-11-30 16:20:07",
|
||||
* "user_timezone": "Asia/Omsk"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object[]} data List of projects.
|
||||
* @apiSuccess {Integer} data.id Project ID.
|
||||
* @apiSuccess {String} data.name Project name.
|
||||
* @apiSuccess {Integer} data.time Total time spent on the project.
|
||||
* @apiSuccess {Object[]} data.users List of users associated with the project.
|
||||
* @apiSuccess {Integer} data.users.id User ID.
|
||||
* @apiSuccess {String} data.users.full_name User's full name.
|
||||
* @apiSuccess {String} data.users.email User's email address.
|
||||
* @apiSuccess {Integer} data.users.time Total time spent by the user on the project.
|
||||
* @apiSuccess {Object[]} data.users.tasks List of tasks associated with the user.
|
||||
* @apiSuccess {Integer} data.users.tasks.id Task ID.
|
||||
* @apiSuccess {String} data.users.tasks.task_name Task name.
|
||||
* @apiSuccess {Integer} data.users.tasks.time Total time spent on the task.
|
||||
* @apiSuccess {Object[]} data.users.tasks.intervals List of time intervals for the task.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.date Date of the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.time Time spent in the interval.
|
||||
* @apiSuccess {Object[]} data.users.tasks.intervals.items Detailed breakdown of intervals.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.items.start_at Start time of the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.activity_fill Activity fill percentage during the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.mouse_fill Mouse activity fill percentage.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.keyboard_fill Keyboard activity fill percentage.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.items.end_at End time of the interval.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.items.user_email User's email associated with the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.id Interval ID.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.project_id Project ID associated with the interval.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.items.project_name Project name associated with the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.task_id Task ID associated with the interval.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.items.task_name Task name associated with the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.user_id User ID associated with the interval.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.items.full_name User's full name associated with the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.hour Hour of the day for the interval.
|
||||
* @apiSuccess {String} data.users.tasks.intervals.items.day Day of the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.minute Minute of the hour for the interval.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.duration Duration of the interval.
|
||||
* @apiSuccess {Object} data.users.tasks.intervals.items.durationByDay Duration of the interval by day.
|
||||
* @apiSuccess {Integer} data.users.tasks.intervals.items.durationAtSelectedPeriod Duration at the selected period.
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": [
|
||||
* {
|
||||
* "id": 159,
|
||||
* "name": "Voluptas ab et ea.",
|
||||
* "time": 3851703975,
|
||||
* "users": [
|
||||
* {
|
||||
* "id": 7,
|
||||
* "full_name": "Dr. Adaline Toy",
|
||||
* "email": "projectManager1231@example.com",
|
||||
* "time": 3851703975,
|
||||
* "tasks": [
|
||||
* {
|
||||
* "id": 54,
|
||||
* "task_name": "Quo consequatur mollitia nam.",
|
||||
* "time": 550243425,
|
||||
* "intervals": [
|
||||
* {
|
||||
* "date": "2006-05-29",
|
||||
* "time": 43425,
|
||||
* "items": [
|
||||
* {
|
||||
* "start_at": "2006-05-29 00:43:45",
|
||||
* "activity_fill": 87,
|
||||
* "mouse_fill": 81,
|
||||
* "keyboard_fill": 6,
|
||||
* "end_at": "2006-06-01 12:03:45",
|
||||
* "user_email": "projectManager1231@example.com",
|
||||
* "id": 3372,
|
||||
* "project_id": 159,
|
||||
* "project_name": "Voluptas ab et ea.",
|
||||
* "task_id": 54,
|
||||
* "task_name": "Quo consequatur mollitia nam.",
|
||||
* "user_id": 7,
|
||||
* "full_name": "Dr. Adaline Toy",
|
||||
* "hour": 0,
|
||||
* "day": "2006-05-29",
|
||||
* "minute": 40,
|
||||
* "duration": 300000,
|
||||
* "durationByDay": {
|
||||
* "2006-05-29": 256575,
|
||||
* "2006-06-01": 43425
|
||||
* },
|
||||
* "durationAtSelectedPeriod": 43425
|
||||
* }
|
||||
* ]
|
||||
* },
|
||||
* // More intervals...
|
||||
* ]
|
||||
* }
|
||||
* // More tasks...
|
||||
* ]
|
||||
* }
|
||||
* // More users...
|
||||
* ]
|
||||
* }
|
||||
* // More projects...
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
public function __invoke(ProjectReportRequest $request): JsonResponse
|
||||
{
|
||||
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
|
||||
|
||||
return responder()->success(
|
||||
ProjectReportExport::init(
|
||||
$request->input('users', User::all()->pluck('id')->toArray()),
|
||||
$request->input('projects', Project::all()->pluck('id')->toArray()),
|
||||
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
|
||||
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
|
||||
$companyTimezone
|
||||
)->collection()->all(),
|
||||
)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /api/report/dashboard/download Download Dashboard Report
|
||||
* @apiDescription Downloads a dashboard report in the specified file format.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName DownloadDashboardReport
|
||||
* @apiGroup Reports
|
||||
* @apiUse AuthHeader
|
||||
* @apiHeader {String} Accept Accept mime type. Example: `text/csv`.
|
||||
*
|
||||
* @apiParam {String} start_at The start date and time for the report in ISO 8601 format.
|
||||
* @apiParam {String} end_at The end date and time for the report in ISO 8601 format.
|
||||
* @apiParam {String} user_timezone The timezone of the user. Example: `Asia/Omsk`.
|
||||
* @apiParam {Array} users List of user IDs to include in the report.
|
||||
* @apiParam {Array} projects List of project IDs to include in the report.
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "start_at": "2023-11-01T16:15:09Z",
|
||||
* "end_at": "2023-11-30T23:59:07Z",
|
||||
* "user_timezone": "Asia/Omsk",
|
||||
* "users": [7],
|
||||
* "projects": [159]
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} url The URL where the generated report can be downloaded.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "url": "/storage/reports/1b11d8f9-c5a3-4fe5-86bd-ae6a3031352c/Dashboard_Report.csv"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function download(ProjectReportRequest $request): JsonResponse
|
||||
{
|
||||
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
|
||||
|
||||
$job = new GenerateAndSendReport(
|
||||
ProjectReportExport::init(
|
||||
$request->input('users', User::all()->pluck('id')->toArray()),
|
||||
$request->input('projects', Project::all()->pluck('id')->toArray()),
|
||||
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
|
||||
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
|
||||
$companyTimezone
|
||||
),
|
||||
$request->user(),
|
||||
ReportHelper::getReportFormat($request),
|
||||
);
|
||||
|
||||
app(Dispatcher::class)->dispatchSync($job);
|
||||
|
||||
return responder()->success(['url' => $job->getPublicPath()])->respond();
|
||||
}
|
||||
|
||||
}
|
||||
98
app/Http/Controllers/Api/Reports/TimeUseReportController.php
Normal file
98
app/Http/Controllers/Api/Reports/TimeUseReportController.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Reports;
|
||||
|
||||
use App\Http\Requests\Reports\TimeUseReportRequest;
|
||||
use App\Models\User;
|
||||
use App\Reports\TimeUseReportExport;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Settings;
|
||||
|
||||
/**
|
||||
* Class TimeUseReportController
|
||||
*
|
||||
*/
|
||||
class TimeUseReportController
|
||||
{
|
||||
/**
|
||||
* @api {post} /api/report/time Get User Time Report
|
||||
* @apiDescription Retrieves the time report for specified users within a given time range.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetUserTimeReport
|
||||
* @apiGroup Reports
|
||||
*
|
||||
* @apiParam {String} start_at The start date and time for the report in ISO 8601 format.
|
||||
* @apiParam {String} end_at The end date and time for the report in ISO 8601 format.
|
||||
* @apiParam {String} user_timezone The timezone of the user. Example: `Asia/Omsk`.
|
||||
* @apiParam {Array} users List of user IDs to include in the report.
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "start_at": "2023-11-01T16:15:09Z",
|
||||
* "end_at": "2023-11-30T23:59:07Z",
|
||||
* "user_timezone": "Asia/Omsk",
|
||||
* "users": [7]
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object[]} data List of users and their respective time logs.
|
||||
* @apiSuccess {Number} data.time Total time logged by the user within the specified period (in seconds).
|
||||
* @apiSuccess {Object} data.user User information.
|
||||
* @apiSuccess {Number} data.user.id User ID.
|
||||
* @apiSuccess {String} data.user.email User's email address.
|
||||
* @apiSuccess {String} data.user.full_name User's full name.
|
||||
* @apiSuccess {Object[]} data.tasks List of tasks the user has logged time for.
|
||||
* @apiSuccess {Number} data.tasks.time Time logged for the task (in seconds).
|
||||
* @apiSuccess {Number} data.tasks.task_id Task ID.
|
||||
* @apiSuccess {String} data.tasks.task_name Task name.
|
||||
* @apiSuccess {Number} data.tasks.project_id Project ID associated with the task.
|
||||
* @apiSuccess {String} data.tasks.project_name Project name associated with the task.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": [
|
||||
* {
|
||||
* "time": 2151975,
|
||||
* "user": {
|
||||
* "id": 7,
|
||||
* "email": "projectManager1231@example.com",
|
||||
* "full_name": "Dr. Adaline Toy"
|
||||
* },
|
||||
* "tasks": [
|
||||
* {
|
||||
* "time": 307425,
|
||||
* "task_id": 56,
|
||||
* "task_name": "Similique enim aspernatur.",
|
||||
* "project_id": 159,
|
||||
* "project_name": "Voluptas ab et ea."
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
|
||||
public function __invoke(TimeUseReportRequest $request): JsonResponse
|
||||
{
|
||||
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
|
||||
|
||||
return responder()->success(
|
||||
TimeUseReportExport::init(
|
||||
$request->input('users') ?? User::all()->pluck('id')->toArray(),
|
||||
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
|
||||
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
|
||||
$companyTimezone
|
||||
)->collection()->all(),
|
||||
)->respond();
|
||||
}
|
||||
}
|
||||
201
app/Http/Controllers/Api/Reports/UniversalReportController.php
Normal file
201
app/Http/Controllers/Api/Reports/UniversalReportController.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Reports;
|
||||
|
||||
use App\Enums\UniversalReportType;
|
||||
use App\Enums\UniversalReportBase;
|
||||
use App\Exceptions\Entities\NotEnoughRightsException;
|
||||
use App\Helpers\ReportHelper;
|
||||
use App\Http\Requests\Reports\UniversalReport\UniversalReportEditRequest;
|
||||
use App\Http\Requests\Reports\UniversalReport\UniversalReportRequest;
|
||||
use App\Http\Requests\Reports\UniversalReport\UniversalReportShowRequest;
|
||||
use App\Http\Requests\Reports\UniversalReport\UniversalReportStoreRequest;
|
||||
use App\Http\Requests\Reports\UniversalReport\UniversalReportDestroyRequest;
|
||||
|
||||
use App\Jobs\GenerateAndSendReport;
|
||||
use App\Models\Project;
|
||||
use App\Models\UniversalReport;
|
||||
use App\Reports\PlannedTimeReportExport;
|
||||
use App\Reports\UniversalReportExport;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Settings;
|
||||
use Throwable;
|
||||
|
||||
class UniversalReportController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$items = [
|
||||
UniversalReportType::COMPANY->value => [],
|
||||
UniversalReportType::PERSONAL->value => [],
|
||||
];
|
||||
$user = request()->user();
|
||||
|
||||
if (request()->user()->isAdmin()) {
|
||||
UniversalReport::select('id', 'name', 'type')
|
||||
->where([
|
||||
['type', '=', UniversalReportType::COMPANY->value, 'or'],
|
||||
['user_id', '=', request()->user()->id, 'or']
|
||||
])
|
||||
->get()
|
||||
->each(function ($item) use (&$items) {
|
||||
$items[$item->type->value][] = $item->toArray();
|
||||
});
|
||||
|
||||
return responder()->success($items)->respond();
|
||||
}
|
||||
|
||||
UniversalReport::select('id', 'name', 'data_objects', 'base', 'type')->get()->each(function ($item) use (&$items) {
|
||||
if ($item->base->checkAccess($item->data_objects)) {
|
||||
unset($item->data_objects, $item->base);
|
||||
$items[$item->type->value][] = $item->toArray();
|
||||
}
|
||||
});
|
||||
|
||||
return responder()->success($items)->respond();
|
||||
}
|
||||
|
||||
public function getBases()
|
||||
{
|
||||
return responder()->success(UniversalReportBase::bases())->respond();
|
||||
}
|
||||
|
||||
public function getDataObjectsAndFields(Request $request)
|
||||
{
|
||||
$base = UniversalReportBase::tryFrom($request->input('base', null));
|
||||
// dd($base->dataObjects());
|
||||
return responder()->success([
|
||||
'fields' => $base->fields(),
|
||||
'dataObjects' => $base->dataObjects(),
|
||||
'charts' => $base->charts(),
|
||||
])->respond();
|
||||
}
|
||||
|
||||
public function store(UniversalReportStoreRequest $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($request->input('type') === UniversalReportType::COMPANY->value) {
|
||||
if ($request->user()->isAdmin()) {
|
||||
$report = $user->universalReports()->create([
|
||||
'name' => $request->name,
|
||||
'type' => $request->type,
|
||||
'base' => $request->base,
|
||||
'data_objects' => $request->dataObjects,
|
||||
'fields' => $request->fields,
|
||||
'charts' => $request->charts,
|
||||
]);
|
||||
return responder()->success(['message' => "The report was saved successfully", 'id' => $report->id])->respond(200);
|
||||
} else {
|
||||
return throw new NotEnoughRightsException('User rights do not allow saving the report for the company');
|
||||
}
|
||||
}
|
||||
|
||||
$report = $user->universalReports()->create([
|
||||
'name' => $request->name,
|
||||
'type' => $request->type,
|
||||
'base' => $request->base,
|
||||
'data_objects' => $request->dataObjects,
|
||||
'fields' => $request->fields,
|
||||
'charts' => $request->charts,
|
||||
]);
|
||||
|
||||
return responder()->success(['message' => "The report was saved successfully", 'id' => $report->id])->respond(200);
|
||||
}
|
||||
|
||||
public function show(UniversalReportShowRequest $request)
|
||||
{
|
||||
return responder()->success(UniversalReport::find($request->id))->respond();
|
||||
}
|
||||
|
||||
public function edit(UniversalReportEditRequest $request)
|
||||
{
|
||||
if ($request->input('type') === UniversalReportType::COMPANY->value) {
|
||||
if ($request->user()->isAdmin()) {
|
||||
UniversalReport::where('id', $request->id)->update([
|
||||
'name' => $request->name,
|
||||
'type' => $request->type,
|
||||
'base' => $request->base,
|
||||
'data_objects' => $request->dataObjects,
|
||||
'fields' => $request->fields,
|
||||
'charts' => $request->charts,
|
||||
]);
|
||||
} else {
|
||||
return throw new NotEnoughRightsException('User rights do not allow saving the report for the company');
|
||||
}
|
||||
}
|
||||
|
||||
UniversalReport::where('id', $request->id)->update([
|
||||
'name' => $request->name,
|
||||
'type' => $request->type,
|
||||
'base' => $request->base,
|
||||
'data_objects' => $request->dataObjects,
|
||||
'fields' => $request->fields,
|
||||
'charts' => $request->charts,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function __invoke(UniversalReportRequest $request): JsonResponse
|
||||
{
|
||||
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
|
||||
|
||||
return responder()->success(
|
||||
UniversalReportExport::init(
|
||||
$request->input('id'),
|
||||
Carbon::parse($request->input('start_at'))->setTimezone($companyTimezone),
|
||||
Carbon::parse($request->input('end_at'))->setTimezone($companyTimezone),
|
||||
Settings::scope('core')->get('timezone', 'UTC'),
|
||||
)->collection()->all(),
|
||||
)->respond();
|
||||
}
|
||||
|
||||
public function destroy(UniversalReportDestroyRequest $request)
|
||||
{
|
||||
UniversalReport::find($request->input('id', null))->delete();
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
|
||||
public function download(UniversalReportRequest $request): JsonResponse
|
||||
{
|
||||
$companyTimezone = Settings::scope('core')->get('timezone', 'UTC');
|
||||
$job = new GenerateAndSendReport(
|
||||
UniversalReportExport::init(
|
||||
$request->id,
|
||||
Carbon::parse($request->start_at) ?? Carbon::parse(),
|
||||
Carbon::parse($request->end_at) ?? Carbon::parse(),
|
||||
Settings::scope('core')->get('timezone', 'UTC'),
|
||||
$request->user()?->timezone ?? 'UTC',
|
||||
),
|
||||
|
||||
$request->user(),
|
||||
ReportHelper::getReportFormat($request),
|
||||
);
|
||||
|
||||
$job->handle();
|
||||
// app(Dispatcher::class)->dispatchSync($job);
|
||||
|
||||
return responder()->success(['url' => $job->getPublicPath()])->respond();
|
||||
}
|
||||
// /**
|
||||
// * @throws Throwable
|
||||
// */
|
||||
// public function download(UniversalReportRequest $request): JsonResponse
|
||||
// {
|
||||
// $job = new GenerateAndSendReport(
|
||||
// PlannedTimeReportExport::init(
|
||||
// $request->input('projects', Project::all()->pluck('id')->toArray()),
|
||||
// ),
|
||||
// $request->user(),
|
||||
// ReportHelper::getReportFormat($request),
|
||||
// );
|
||||
|
||||
// app(Dispatcher::class)->dispatchSync($job);
|
||||
|
||||
// return responder()->success(['url' => $job->getPublicPath()])->respond();
|
||||
// }
|
||||
}
|
||||
73
app/Http/Controllers/Api/RoleController.php
Normal file
73
app/Http/Controllers/Api/RoleController.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Enums\Role;
|
||||
use CatEvent;
|
||||
use Filter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class RoleController extends ItemController
|
||||
{
|
||||
/**
|
||||
* @api {post} /api/roles/list Get Roles List
|
||||
* @apiDescription Retrieves the list of roles available in the system.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetRolesList
|
||||
* @apiGroup Roles
|
||||
*
|
||||
* @apiSuccess {Number} status HTTP status code.
|
||||
* @apiSuccess {Boolean} success Request success status.
|
||||
* @apiSuccess {Object[]} data List of roles.
|
||||
* @apiSuccess {String} data.name Role name.
|
||||
* @apiSuccess {Number} data.id Role ID.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "data": [
|
||||
* {
|
||||
* "name": "ANY",
|
||||
* "id": -1
|
||||
* },
|
||||
* {
|
||||
* "name": "ADMIN",
|
||||
* "id": 0
|
||||
* },
|
||||
* {
|
||||
* "name": "MANAGER",
|
||||
* "id": 1
|
||||
* },
|
||||
* {
|
||||
* "name": "USER",
|
||||
* "id": 2
|
||||
* },
|
||||
* {
|
||||
* "name": "AUDITOR",
|
||||
* "id": 3
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName());
|
||||
|
||||
$items = Filter::process(
|
||||
Filter::getActionFilterName(),
|
||||
//For compatibility reasons generate serialized model-like array
|
||||
array_map(fn ($role) => ['name' => $role->name, 'id' => $role->value], Role::cases()),
|
||||
);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items]);
|
||||
|
||||
return responder()->success($items)->respond();
|
||||
}
|
||||
}
|
||||
253
app/Http/Controllers/Api/StatusController.php
Normal file
253
app/Http/Controllers/Api/StatusController.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
|
||||
|
||||
use App\Models\Status;
|
||||
use App\Http\Requests\Status\CreateStatusRequest;
|
||||
use App\Http\Requests\Status\DestroyStatusRequest;
|
||||
use App\Http\Requests\Status\ListStatusRequest;
|
||||
use App\Http\Requests\Status\ShowStatusRequestStatus;
|
||||
use App\Http\Requests\Status\UpdateStatusRequest;
|
||||
use CatEvent;
|
||||
use Exception;
|
||||
use Filter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Throwable;
|
||||
|
||||
class StatusController extends ItemController
|
||||
{
|
||||
protected const MODEL = Status::class;
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /statuses/show Show
|
||||
* @apiDescription Show status.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Show Status
|
||||
* @apiGroup Status
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id Status ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiUse StatusObject
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function show(ShowStatusRequestStatus $request): JsonResponse
|
||||
{
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {get} /statuses/list List
|
||||
* @apiDescription Get list of statuses.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Status List
|
||||
* @apiGroup Status
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {Object} res Status
|
||||
*
|
||||
* @apiUse StatusObject
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*
|
||||
*/
|
||||
public function index(ListStatusRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CreateStatusRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
* @api {post} /statuses/create Create
|
||||
* @apiDescription Creates status
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Create Status
|
||||
* @apiGroup Status
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {String} name Status name
|
||||
* @apiParam {String} active Status active
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "name": "Normal",
|
||||
* "active": false
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} res Status
|
||||
*
|
||||
* @apiSuccess {Number} id The ID of the status.
|
||||
* @apiSuccess {String} name The name of the status.
|
||||
* @apiSuccess {Boolean} active Indicates if the status is active.\
|
||||
* @apiSuccess {String} created_at The creation timestamp.
|
||||
* @apiSuccess {String} updated_at The last update timestamp.
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 10,
|
||||
* "name": "Normal",
|
||||
* "active": false,
|
||||
* "created_at": "2024-08-15T14:04:03.000000Z",
|
||||
* "updated_at": "2024-08-15T14:04:03.000000Z"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function create(CreateStatusRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(Filter::getRequestFilterName(), static function ($item) {
|
||||
$maxOrder = Status::max('order');
|
||||
$item['order'] = $maxOrder + 1;
|
||||
return $item;
|
||||
});
|
||||
|
||||
return $this->_create($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /statuses/edit Edit
|
||||
* @apiDescription Edit Status
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Edit
|
||||
* @apiGroup Status
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id ID
|
||||
* @apiParam {String} name Status name
|
||||
* @apiParam {String} active Status active
|
||||
*
|
||||
* @apiParamExample {json} Simple Request Example
|
||||
* {
|
||||
* "id": 1,
|
||||
* "name": "Normal",
|
||||
* "active": false
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} res Status
|
||||
*
|
||||
* @apiUse StatusObject
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
public function edit(UpdateStatusRequest $request): JsonResponse
|
||||
{
|
||||
CatEvent::listen(Filter::getBeforeActionEventName(), static function ($item, $requestData) {
|
||||
if (isset($requestData['order'])) {
|
||||
$newOrder = $requestData['order'];
|
||||
$oldOrder = $item->order;
|
||||
if ($newOrder < 1) {
|
||||
$newOrder = 1;
|
||||
}
|
||||
$maxOrder = Status::max('order');
|
||||
if ($newOrder > $maxOrder) {
|
||||
$newOrder = $maxOrder + 1;
|
||||
}
|
||||
$swapItem = Status::where('order', '=', $newOrder)->first();
|
||||
if (isset($swapItem)) {
|
||||
$swapItemOrder = $swapItem->order;
|
||||
|
||||
$item->order = 0;
|
||||
$item->save();
|
||||
|
||||
$swapItem->order = $oldOrder;
|
||||
$swapItem->save();
|
||||
|
||||
$item->order = $swapItemOrder;
|
||||
$item->save();
|
||||
|
||||
} else {
|
||||
$item->order = $newOrder;
|
||||
}
|
||||
}
|
||||
});
|
||||
return $this->_edit($request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /statuses/remove Destroy
|
||||
* @apiDescription Destroy User
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Destroy Status
|
||||
* @apiGroup Status
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiParam {Integer} id ID of the target status
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess (204) No Content Indicates that the status was successfully removed or deactivated.
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function destroy(DestroyStatusRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_destroy($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ListStatusRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
/**
|
||||
* @api {get} /invitations/count Count Invitations
|
||||
* @apiDescription Get the count of invitations
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName CountInvitations
|
||||
* @apiGroup Invitations
|
||||
*
|
||||
* @apiSuccess {Integer} total The total count of pending invitations.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* {
|
||||
* "total": 0
|
||||
* }
|
||||
*
|
||||
* @apiUse TotalSuccess
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function count(ListStatusRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_count($request);
|
||||
}
|
||||
}
|
||||
108
app/Http/Controllers/Api/TaskActivityController.php
Normal file
108
app/Http/Controllers/Api/TaskActivityController.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Enums\SortDirection;
|
||||
use App\Http\Requests\TaskActivity\ShowTaskActivityRequest;
|
||||
use App\Models\TaskComment;
|
||||
use App\Models\TaskHistory;
|
||||
use CatEvent;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Filter;
|
||||
use App\Helpers\QueryHelper;
|
||||
|
||||
class TaskActivityController extends ItemController
|
||||
{
|
||||
private function getQueryBuilder(array $filter, string $model): Builder
|
||||
{
|
||||
$model = new $model;
|
||||
|
||||
$query = new Builder($model::getQuery());
|
||||
$query->setModel($model);
|
||||
|
||||
$modelScopes = $model->getGlobalScopes();
|
||||
|
||||
foreach ($modelScopes as $key => $value) {
|
||||
$query->withGlobalScope($key, $value);
|
||||
}
|
||||
|
||||
foreach (Filter::process(Filter::getQueryAdditionalRelationsFilterName(), []) as $with) {
|
||||
$query->with($with);
|
||||
}
|
||||
|
||||
QueryHelper::apply($query, $model, $filter);
|
||||
$sortDirection = SortDirection::tryFrom($filter['orderBy'][1])?->value ?? 'asc';
|
||||
$query->orderBy('id', $sortDirection);
|
||||
|
||||
return Filter::process(
|
||||
Filter::getQueryFilterName(),
|
||||
$query
|
||||
);
|
||||
}
|
||||
|
||||
private function getCollectionFromModel(array $requestData, string $model): LengthAwarePaginator
|
||||
{
|
||||
if ($model === TaskComment::class) {
|
||||
$requestData['with'][] = 'attachmentsRelation';
|
||||
$requestData['with'][] = 'attachmentsRelation.user:id,full_name';
|
||||
}
|
||||
|
||||
$itemsQuery = $this->getQueryBuilder($requestData, $model);
|
||||
|
||||
$items = $itemsQuery->paginate(30);
|
||||
|
||||
Filter::process(
|
||||
Filter::getActionFilterName(),
|
||||
$items,
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ShowTaskActivityRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function index(ShowTaskActivityRequest $request): JsonResponse
|
||||
{
|
||||
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
|
||||
$requestedActivity = ActivityType::from($requestData['type']);
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestData);
|
||||
|
||||
$items = [];
|
||||
$total = 0;
|
||||
$perPage = 0;
|
||||
if ($requestedActivity === ActivityType::ALL) {
|
||||
$taskComments = $this->getCollectionFromModel($requestData, TaskComment::class);
|
||||
$taskHistory = $this->getCollectionFromModel($requestData, TaskHistory::class);
|
||||
$total = $taskComments->total() + $taskHistory->total();
|
||||
$perPage = $taskComments->perPage() + $taskHistory->perPage();
|
||||
|
||||
$sortDirection = SortDirection::tryFrom($requestData['orderBy'][1])?->value ?? 'asc';
|
||||
$items = collect(array_merge($taskComments->items(), $taskHistory->items()))->sortBy([
|
||||
fn ($a, $b) => $sortDirection === 'asc'
|
||||
? strtotime($a['created_at']) <=> strtotime($b['created_at'])
|
||||
: strtotime($b['created_at']) <=> strtotime($a['created_at']),
|
||||
], SORT_REGULAR, $sortDirection === 'desc');
|
||||
} elseif ($requestedActivity === ActivityType::HISTORY) {
|
||||
$taskHistory = $this->getCollectionFromModel($requestData, TaskHistory::class);
|
||||
$total = $taskHistory->total();
|
||||
$perPage = $taskHistory->perPage();
|
||||
$items = $taskHistory->items();
|
||||
} elseif ($requestedActivity === ActivityType::COMMENTS) {
|
||||
$taskComments = $this->getCollectionFromModel($requestData, TaskComment::class);
|
||||
$total = $taskComments->total();
|
||||
$perPage = $taskComments->perPage();
|
||||
$items = $taskComments->items();
|
||||
}
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$items, $requestData]);
|
||||
|
||||
return responder()->success(new LengthAwarePaginator($items, $total, $perPage))->respond();
|
||||
}
|
||||
}
|
||||
281
app/Http/Controllers/Api/TaskCommentController.php
Normal file
281
app/Http/Controllers/Api/TaskCommentController.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Http\Requests\Status\DestroyStatusRequest;
|
||||
use App\Http\Requests\TaskComment\CreateTaskCommentRequest;
|
||||
use App\Http\Requests\TaskComment\DestroyTaskCommentRequest;
|
||||
use App\Http\Requests\TaskComment\ListTaskCommentRequest;
|
||||
use App\Http\Requests\TaskComment\ShowTaskCommentRequestStatus;
|
||||
use App\Http\Requests\TaskComment\UpdateTaskCommentRequest;
|
||||
use Filter;
|
||||
use App\Models\TaskComment;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Throwable;
|
||||
|
||||
class TaskCommentController extends ItemController
|
||||
{
|
||||
protected const MODEL = TaskComment::class;
|
||||
|
||||
/**
|
||||
* @api {post} /task-comment/create Create Task Comment
|
||||
* @apiDescription Create a new task comment
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName CreateTaskComment
|
||||
* @apiGroup TaskComments
|
||||
*
|
||||
*
|
||||
* @apiPermission task_comment_create
|
||||
* @apiPermission task_comment_full_access
|
||||
* @apiParam {Integer} task_id ID of the task
|
||||
* @apiParam {String} comment The content of the comment
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "task_id": 1,
|
||||
* "comment": "This is a new comment"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Integer} id ID of the created comment
|
||||
* @apiSuccess {Integer} task_id ID of the task
|
||||
* @apiSuccess {Integer} user_id ID of the user who created the comment
|
||||
* @apiSuccess {String} comment The content of the comment
|
||||
* @apiSuccess {String} created_at Creation timestamp
|
||||
* @apiSuccess {String} updated_at Last update timestamp
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example:
|
||||
* HTTP/1.1 201 Created
|
||||
* {
|
||||
* "id": 1,
|
||||
* "task_id": 1,
|
||||
* "user_id": 1,
|
||||
* "comment": "This is a new comment",
|
||||
* "created_at": "2024-07-09T10:00:00.000000Z",
|
||||
* "updated_at": "2024-07-09T10:00:00.000000Z"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function create(CreateTaskCommentRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(
|
||||
Filter::getRequestFilterName(),
|
||||
static function (array $data) use ($request) {
|
||||
$data['user_id'] = $request->user()->id;
|
||||
|
||||
return $data;
|
||||
}
|
||||
);
|
||||
|
||||
return $this->_create($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /task-comment/edit Edit Task Comment
|
||||
* @apiDescription Edit an existing task comment
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName EditTaskComment
|
||||
* @apiGroup TaskComments
|
||||
*
|
||||
*
|
||||
* @apiParam {Integer} id ID of the comment to edit
|
||||
* @apiParam {String} comment The updated content of the comment
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "id": 1,
|
||||
* "comment": "This is the updated comment"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Integer} id ID of the edited comment
|
||||
* @apiSuccess {Integer} task_id ID of the task
|
||||
* @apiSuccess {Integer} user_id ID of the user who edited the comment
|
||||
* @apiSuccess {String} comment The updated content of the comment
|
||||
* @apiSuccess {String} created_at Creation timestamp
|
||||
* @apiSuccess {String} updated_at Last update timestamp
|
||||
* @apiSuccess {String} deleted_at Deletion timestamp (if applicable, otherwise null)
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "task_id": 1,
|
||||
* "user_id": 1,
|
||||
* "content": "2344",
|
||||
* "created_at": "2024-05-03T10:45:36.000000Z",
|
||||
* "updated_at": "2024-05-03T10:45:36.000000Z",
|
||||
* "deleted_at": null
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function edit(UpdateTaskCommentRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_edit($request);
|
||||
}
|
||||
/**
|
||||
* @api {any} /task-comment/list List Task Comments
|
||||
* @apiDescription Get list of Task Comments
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetTaskCommentList
|
||||
* @apiGroup Task Comments
|
||||
*
|
||||
* @apiPermission task_comment_list
|
||||
* @apiPermission task_comment_full_access
|
||||
*
|
||||
* @apiParam {Integer} [task_id] Optional task ID to filter comments
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "task_id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Integer} id ID of the comment
|
||||
* @apiSuccess {Integer} task_id ID of the task
|
||||
* @apiSuccess {Integer} user_id ID of the user who created the comment
|
||||
* @apiSuccess {String} content Content of the comment
|
||||
* @apiSuccess {String} created_at Creation timestamp
|
||||
* @apiSuccess {String} updated_at Last update timestamp
|
||||
* @apiSuccess {String} deleted_at Deletion timestamp (if applicable, otherwise null)
|
||||
* @apiSuccess {Object} user User who created the comment
|
||||
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "task_id": 1,
|
||||
* "user_id": 1,
|
||||
* "content": "2344",
|
||||
* "created_at": "2024-05-03T10:45:36.000000Z",
|
||||
* "updated_at": "2024-05-03T10:45:36.000000Z",
|
||||
* "deleted_at": null,
|
||||
* "user": {
|
||||
* "id": 1,
|
||||
* "full_name": "Admin",
|
||||
* "email": "admin@cattr.app",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_active": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-02-15T19:06:42.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 0,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2023-10-26 10:26:17",
|
||||
* "online": false,
|
||||
* "can_view_team_tab": true,
|
||||
* "can_create_task": true
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function index(ListTaskCommentRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(
|
||||
Filter::getQueryFilterName(),
|
||||
static function ($query) use ($request) {
|
||||
if (!$request->user()->can('edit', TaskComment::class)) {
|
||||
$query = $query->whereHas(
|
||||
'task',
|
||||
static fn (Builder $taskQuery) => $taskQuery->where(
|
||||
'user_id',
|
||||
'=',
|
||||
$request->user()->id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $query->with('user');
|
||||
}
|
||||
);
|
||||
|
||||
return $this->_index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 1.0.0
|
||||
* @throws Throwable
|
||||
* @api {post} /task-comment/show Show
|
||||
* @apiDescription Show Task Comment
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName ShowTaskComment
|
||||
* @apiGroup Task Comment
|
||||
*
|
||||
* @apiPermission task_comment_show
|
||||
* @apiPermission task_comment_full_access
|
||||
*/
|
||||
public function show(ShowTaskCommentRequestStatus $request): JsonResponse
|
||||
{
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiDeprecated since 4.0.0
|
||||
* @api {post} /task-comment/remove Destroy Task Comment
|
||||
* @apiDescription Destroy a Task Comment
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName DestroyTaskComment
|
||||
* @apiGroup Task Comment
|
||||
*
|
||||
* @apiPermission task_comment_remove
|
||||
* @apiPermission task_comment_full_access
|
||||
*
|
||||
* @apiParam {Integer} id ID of the task comment to destroy
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Integer} status Response status code
|
||||
* @apiSuccess {Boolean} success Response success status
|
||||
* @apiSuccess {String} message Success message
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function destroy(DestroyTaskCommentRequest $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
Filter::listen(
|
||||
Filter::getQueryFilterName(),
|
||||
static fn($query) => $user->hasRole([Role::ADMIN, Role::MANAGER]) ? $query :
|
||||
$query->where(['user_id' => $user->id])
|
||||
->whereHas('task', static fn($taskQuery) => $taskQuery->where(['user_id' => $user->id]))
|
||||
);
|
||||
|
||||
return $this->_destroy($request);
|
||||
}
|
||||
}
|
||||
1139
app/Http/Controllers/Api/TaskController.php
Normal file
1139
app/Http/Controllers/Api/TaskController.php
Normal file
File diff suppressed because it is too large
Load Diff
279
app/Http/Controllers/Api/TimeController.php
Normal file
279
app/Http/Controllers/Api/TimeController.php
Normal file
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Filter;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Validator;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class TimeController extends Controller
|
||||
{
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public static function getControllerRules(): array
|
||||
{
|
||||
return [
|
||||
'total' => 'time.total',
|
||||
'project' => 'time.project',
|
||||
'tasks' => 'time.tasks',
|
||||
'task' => 'time.task',
|
||||
'taskUser' => 'time.task-user',
|
||||
];
|
||||
}
|
||||
/**
|
||||
* @api {get,post} /time/total Total
|
||||
* @apiDescription Get total of Time
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Total
|
||||
* @apiGroup Time
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission time_total
|
||||
* @apiPermission time_full_access
|
||||
*
|
||||
* @apiParam {String} start_at Start DataTime
|
||||
* @apiParam {String} end_at End DataTime
|
||||
* @apiParam {Integer} user_id User ID
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "user_id": 1,
|
||||
* "start_at": "2005-01-01 00:00:00",
|
||||
* "end_at": "2019-01-01 00:00:00"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Integer} time Total time in seconds
|
||||
* @apiSuccess {String} start Datetime of first Time Interval start_at
|
||||
* @apiSuccess {String} end Datetime of last Time Interval end_at
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "time": 338230,
|
||||
* "start": "2020-01-23T19:42:27+00:00",
|
||||
* "end": "2020-04-30T21:58:31+00:00"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse ValidationError
|
||||
*/
|
||||
|
||||
/**
|
||||
* Display a total of time
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
* @deprecated
|
||||
*/
|
||||
public function total(Request $request): JsonResponse
|
||||
{
|
||||
$validationRules = [
|
||||
'start_at' => 'required|date',
|
||||
'end_at' => 'required|date',
|
||||
'user_id' => 'required|integer|exists:users,id'
|
||||
];
|
||||
|
||||
$validator = Validator::make($request->all(), $validationRules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return new JsonResponse(
|
||||
Filter::process($this->getEventUniqueName('answer.error.time.total'), [
|
||||
'error_type' => 'validation',
|
||||
'message' => 'Validation error',
|
||||
'info' => $validator->errors()
|
||||
]),
|
||||
400
|
||||
);
|
||||
}
|
||||
$filters = [
|
||||
'start_at' => ['>=', $request->get('start_at')],
|
||||
'end_at' => ['<=', $request->get('end_at')],
|
||||
'user_id' => ['=', $request->get('user_id')]
|
||||
];
|
||||
|
||||
/** @var Builder $itemsQuery */
|
||||
$itemsQuery = Filter::process(
|
||||
$this->getEventUniqueName('answer.success.item.query.prepare'),
|
||||
$this->applyQueryFilter($this->getQuery(), $filters)
|
||||
);
|
||||
|
||||
$timeIntervals = $itemsQuery->get();
|
||||
|
||||
$totalTime = $timeIntervals->sum(static fn($el) => Carbon::parse($el->end_at)->diffInSeconds($el->start_at));
|
||||
|
||||
return responder()->success([
|
||||
'time' => $totalTime,
|
||||
'start' => $timeIntervals->min('start_at'),
|
||||
'end' => $timeIntervals->max('end_at')
|
||||
])->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get,post} /time/tasks Tasks
|
||||
* @apiDescription Get tasks and its total time
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Tasks
|
||||
* @apiGroup Time
|
||||
*
|
||||
* @apiUse TimeIntervalParams
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "user_id": 1,
|
||||
* "task_id": 1,
|
||||
* "project_id": 2,
|
||||
* "start_at": "2005-01-01 00:00:00",
|
||||
* "end_at": "2019-01-01 00:00:00",
|
||||
* "activity_fill": 42,
|
||||
* "mouse_fill": 43,
|
||||
* "keyboard_fill": 43,
|
||||
* "id": [">", 1]
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} current_datetime Current datetime of server
|
||||
* @apiSuccess {Object[]} tasks Array of objects Task
|
||||
* @apiSuccess {Integer} tasks.id Tasks id
|
||||
* @apiSuccess {Integer} tasks.user_id Tasks User id
|
||||
* @apiSuccess {Integer} tasks.project_id Tasks Project id
|
||||
* @apiSuccess {Integer} tasks.time Tasks total time in seconds
|
||||
* @apiSuccess {String} tasks.start Datetime of first Tasks Time Interval start_at
|
||||
* @apiSuccess {String} tasks.end Datetime of last Tasks Time Interval end_at
|
||||
* @apiSuccess {Object[]} total Array of total tasks time
|
||||
* @apiSuccess {Integer} total.time Total time of tasks in seconds
|
||||
* @apiSuccess {String} total.start Datetime of first Time Interval start_at
|
||||
* @apiSuccess {String} total.end DateTime of last Time Interval end_at
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "current_datetime": "2020-01-28T10:57:40+00:00",
|
||||
* "tasks": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "user_id": 1,
|
||||
* "project_id": 1,
|
||||
* "time": 1490,
|
||||
* "start": "2020-01-23T19:42:27+00:00",
|
||||
* "end": "2020-01-23T20:07:21+00:00"
|
||||
* },
|
||||
* ],
|
||||
* "total": {
|
||||
* "time": 971480,
|
||||
* "start": "2020-01-23T19:42:27+00:00",
|
||||
* "end": "2020-11-01T08:28:06+00:00"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ValidationError
|
||||
*/
|
||||
/**
|
||||
* Display the Tasks and theirs total time
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
* @deprecated
|
||||
*/
|
||||
public function tasks(Request $request): JsonResponse
|
||||
{
|
||||
$validationRules = [
|
||||
'start_at' => 'date',
|
||||
'end_at' => 'date',
|
||||
'project_id' => 'exists:projects,id',
|
||||
'task_id' => 'exists:tasks,id'
|
||||
];
|
||||
|
||||
$validator = Validator::make($request->all(), $validationRules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return new JsonResponse(
|
||||
Filter::process($this->getEventUniqueName('answer.error.time.total'), [
|
||||
'error_type' => 'validation',
|
||||
'message' => 'Validation error',
|
||||
'info' => $validator->errors()
|
||||
]),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
$filters = $request->all();
|
||||
$request->get('start_at') ? $filters['start_at'] = ['>=', (string)$request->get('start_at')] : false;
|
||||
$request->get('end_at') ? $filters['end_at'] = ['<=', (string)$request->get('end_at')] : false;
|
||||
$request->get('project_id') ? $filters['task.project_id'] = $request->get('project_id') : false;
|
||||
$request->get('task_id') ? $filters['task_id'] = ['in', $request->get('task_id')] : false;
|
||||
|
||||
$baseQuery = $this->applyQueryFilter(
|
||||
$this->getQuery(),
|
||||
$filters ?: []
|
||||
);
|
||||
|
||||
$itemsQuery = Filter::process(
|
||||
$this->getEventUniqueName('answer.success.item.list.query.prepare'),
|
||||
$baseQuery
|
||||
);
|
||||
|
||||
$totalTime = 0;
|
||||
|
||||
$tasks = $itemsQuery
|
||||
->with('task')
|
||||
->get()
|
||||
->groupBy(['task_id', 'user_id'])
|
||||
->map(static function ($taskIntervals, $taskId) use (&$totalTime) {
|
||||
$task = [];
|
||||
|
||||
foreach ($taskIntervals as $userId => $userIntervals) {
|
||||
$taskTime = 0;
|
||||
|
||||
foreach ($userIntervals as $interval) {
|
||||
$taskTime += Carbon::parse($interval->end_at)->diffInSeconds($interval->start_at);
|
||||
}
|
||||
|
||||
$firstUserInterval = $userIntervals->first();
|
||||
$lastUserInterval = $userIntervals->last();
|
||||
|
||||
$task = [
|
||||
'id' => $taskId,
|
||||
'user_id' => $userId,
|
||||
'project_id' => $userIntervals[0]['task']['project_id'],
|
||||
'time' => $taskTime,
|
||||
'start' => Carbon::parse($firstUserInterval->start_at)->toISOString(),
|
||||
'end' => Carbon::parse($lastUserInterval->end_at)->toISOString()
|
||||
];
|
||||
|
||||
$totalTime += $taskTime;
|
||||
}
|
||||
|
||||
return $task;
|
||||
})
|
||||
->values();
|
||||
|
||||
$first = $itemsQuery->get()->first();
|
||||
$last = $itemsQuery->get()->last();
|
||||
|
||||
return responder()->success([
|
||||
'tasks' => $tasks,
|
||||
'total' => [
|
||||
'time' => $totalTime,
|
||||
'start' => $first ? Carbon::parse($first->start_at)->toISOString() : null,
|
||||
'end' => $last ? Carbon::parse($last->end_at)->toISOString() : null,
|
||||
]
|
||||
])->respond();
|
||||
}
|
||||
}
|
||||
570
app/Http/Controllers/Api/UserController.php
Normal file
570
app/Http/Controllers/Api/UserController.php
Normal file
@@ -0,0 +1,570 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App;
|
||||
use App\Enums\Role;
|
||||
use App\Enums\ScreenshotsState;
|
||||
use App\Http\Requests\User\ListUsersRequest;
|
||||
use App\Scopes\UserAccessScope;
|
||||
use Settings;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Filter;
|
||||
use App\Mail\UserCreated;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use CatEvent;
|
||||
use Mail;
|
||||
use App\Http\Requests\User\CreateUserRequest;
|
||||
use App\Http\Requests\User\EditUserRequest;
|
||||
use App\Http\Requests\User\SendInviteUserRequest;
|
||||
use App\Http\Requests\User\ShowUserRequest;
|
||||
use App\Http\Requests\User\DestroyUserRequest;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
class UserController extends ItemController
|
||||
{
|
||||
protected const MODEL = User::class;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @api {get, post} /users/list List
|
||||
* @apiDescription Get list of Users with any params
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName GetUserList
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission users_list
|
||||
* @apiPermission users_full_access
|
||||
*
|
||||
* @apiSuccess {Object[]} users List of users.
|
||||
* @apiSuccess {Integer} users.id The unique ID of the user.
|
||||
* @apiSuccess {String} users.full_name Full name of the user.
|
||||
* @apiSuccess {String} users.email Email address of the user.
|
||||
* @apiSuccess {String} users.url URL associated with the user.
|
||||
* @apiSuccess {Integer} users.company_id ID of the company the user belongs to.
|
||||
* @apiSuccess {String} users.avatar URL of the user's avatar image.
|
||||
* @apiSuccess {Integer} users.screenshots_state The current state of screenshot monitoring.
|
||||
* @apiSuccess {Boolean} users.manual_time Indicates if manual time tracking is allowed.
|
||||
* @apiSuccess {Integer} users.computer_time_popup Time in seconds before showing a time popup.
|
||||
* @apiSuccess {Boolean} users.blur_screenshots Indicates if screenshots are blurred.
|
||||
* @apiSuccess {Boolean} users.web_and_app_monitoring Indicates if web and app monitoring is enabled.
|
||||
* @apiSuccess {Integer} users.screenshots_interval Interval in minutes for taking screenshots.
|
||||
* @apiSuccess {Boolean} users.active Indicates if the user is active.
|
||||
* @apiSuccess {String} users.deleted_at Deletion timestamp, or `null` if the user is not deleted.
|
||||
* @apiSuccess {String} users.created_at Creation timestamp of the user.
|
||||
* @apiSuccess {String} users.updated_at Last update timestamp of the user.
|
||||
* @apiSuccess {String} users.timezone The timezone of the user, or `null`.
|
||||
* @apiSuccess {Boolean} users.important Indicates if the user is marked as important.
|
||||
* @apiSuccess {Boolean} users.change_password Indicates if the user must change their password.
|
||||
* @apiSuccess {Integer} users.role_id ID of the user's role.
|
||||
* @apiSuccess {String} users.user_language Language preference of the user.
|
||||
* @apiSuccess {String} users.type The user type, e.g., "employee".
|
||||
* @apiSuccess {Boolean} users.invitation_sent Indicates if an invitation has been sent.
|
||||
* @apiSuccess {Integer} users.nonce Nonce value for secure actions.
|
||||
* @apiSuccess {Boolean} users.client_installed Indicates if the client software is installed.
|
||||
* @apiSuccess {Boolean} users.permanent_screenshots Indicates if permanent screenshots are enabled.
|
||||
* @apiSuccess {String} users.last_activity The last recorded activity timestamp.
|
||||
* @apiSuccess {Boolean} users.screenshots_state_locked Indicates if screenshot state is locked.
|
||||
* @apiSuccess {Boolean} users.online Indicates if the user is currently online.
|
||||
* @apiSuccess {Boolean} users.can_view_team_tab Indicates if the user can view the team tab.
|
||||
* @apiSuccess {Boolean} users.can_create_task Indicates if the user can create tasks.
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "full_name": "Admin",
|
||||
* "email": "admin@cattr.app",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_state": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-08-19T10:42:18.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 0,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2024-08-19 10:42:18",
|
||||
* "screenshots_state_locked": false,
|
||||
* "online": false,
|
||||
* "can_view_team_tab": true,
|
||||
* "can_create_task": true
|
||||
* },
|
||||
* {
|
||||
* "id": 2,
|
||||
* "full_name": "Fabiola Mertz",
|
||||
* "email": "projectManager@example.com",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_state": 2,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 2,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2023-10-26 09:44:17",
|
||||
* "screenshots_state_locked": false,
|
||||
* "online": false,
|
||||
* "can_view_team_tab": false,
|
||||
* "can_create_task": false
|
||||
* },...
|
||||
* ]
|
||||
* }
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
public function index(ListUsersRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /users/create Create
|
||||
* @apiDescription Create User Entity
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName CreateUser
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission users_create
|
||||
* @apiPermission users_full_access
|
||||
*
|
||||
* @apiParam {String} user_language The language of the new user (e.g., "en")
|
||||
* @apiParam {String} timezone The timezone of the new user (e.g., "Europe/Moscow")
|
||||
* @apiParam {Integer} role_id ID of the role of the new user
|
||||
* @apiParam {Integer} active Will new user be active or not `(1 - active, 0 - not)`
|
||||
* @apiParam {Integer} screenshots_state State of screenshots monitoring (e.g., 1 for enabled)
|
||||
* @apiParam {Boolean} send_invite Whether to send an invitation to the new user (true - send, false - do not send)
|
||||
* @apiParam {Boolean} manual_time Whether manual time tracking is enabled for the new user
|
||||
* @apiParam {Integer} screenshots_interval Interval in minutes for taking screenshots
|
||||
* @apiParam {Integer} computer_time_popup Time in minutes before showing a time popup
|
||||
* @apiParam {String} type The type of user (e.g., "employee")
|
||||
* @apiParam {Boolean} web_and_app_monitoring Whether web and app monitoring is enabled
|
||||
* @apiParam {String} email New user email
|
||||
* @apiParam {String} password New user password
|
||||
* @apiParam {String} full_name New user name
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "user_language" : "en",
|
||||
* "timezone" : "Europe/Moscow",
|
||||
* "role_id" : 2,
|
||||
* "active" : true,
|
||||
* "screenshots_state" : 1,
|
||||
* "send_invite" : 1,
|
||||
* "manual_time" : 1,
|
||||
* "screenshots_interval" : 10,
|
||||
* "computer_time_popup" : 3,
|
||||
* "type" : "employee",
|
||||
* "web_and_app_monitoring" : 1,
|
||||
* "email" : "123@cattr.app",
|
||||
* "password" : "password",
|
||||
* "full_name" : "name"
|
||||
* }
|
||||
* @apiSuccess {String} full_name Full name of the user.
|
||||
* @apiSuccess {String} email Email address of the user.
|
||||
* @apiSuccess {String} user_language Language of the user.
|
||||
* @apiSuccess {Boolean} active Whether the user is active.
|
||||
* @apiSuccess {Integer} screenshots_state State of screenshots monitoring.
|
||||
* @apiSuccess {Boolean} manual_time Whether manual time tracking is enabled.
|
||||
* @apiSuccess {Integer} screenshots_interval Interval in minutes for taking screenshots.
|
||||
* @apiSuccess {Integer} computer_time_popup Time in minutes before showing a time popup.
|
||||
* @apiSuccess {String} timezone Timezone of the user.
|
||||
* @apiSuccess {Integer} role_id ID of the role assigned to the user.
|
||||
* @apiSuccess {String} type Type of the user (e.g., "employee").
|
||||
* @apiSuccess {Boolean} web_and_app_monitoring Whether web and app monitoring is enabled.
|
||||
* @apiSuccess {Boolean} screenshots_state_locked Whether the screenshot state is locked.
|
||||
* @apiSuccess {Boolean} invitation_sent Whether an invitation has been sent.
|
||||
* @apiSuccess {String} updated_at Timestamp of the last update.
|
||||
* @apiSuccess {String} created_at Timestamp of when the user was created.
|
||||
* @apiSuccess {Integer} id ID of the created user.
|
||||
* @apiSuccess {Boolean} online Whether the user is currently online.
|
||||
* @apiSuccess {Boolean} can_view_team_tab Whether the user can view the team tab.
|
||||
* @apiSuccess {Boolean} can_create_task Whether the user can create tasks.
|
||||
*
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "full_name": "name",
|
||||
* "email": "123@cattr.app",
|
||||
* "user_language": "en",
|
||||
* "active": 1,
|
||||
* "screenshots_state": 1,
|
||||
* "manual_time": 1,
|
||||
* "screenshots_interval": 10,
|
||||
* "computer_time_popup": 3,
|
||||
* "timezone": "Europe/Moscow",
|
||||
* "role_id": 2,
|
||||
* "type": "employee",
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_state_locked": true,
|
||||
* "invitation_sent": true,
|
||||
* "updated_at": "2024-08-21T14:29:06.000000Z",
|
||||
* "created_at": "2024-08-21T14:29:06.000000Z",
|
||||
* "id": 10,
|
||||
* "online": false,
|
||||
* "can_view_team_tab": false,
|
||||
* "can_create_task": false
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ForbiddenError
|
||||
*/
|
||||
/**
|
||||
* @param CreateUserRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function create(CreateUserRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(Filter::getRequestFilterName(), static function ($requestData) use ($request) {
|
||||
$requestData['screenshots_state_locked'] = $request->user()->isAdmin() && ScreenshotsState::tryFrom($requestData['screenshots_state'])->mustBeInherited();
|
||||
|
||||
return $requestData;
|
||||
});
|
||||
|
||||
return $this->_create($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /users/edit Edit
|
||||
* @apiDescription Edit User
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName EditUser
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission users_edit
|
||||
* @apiPermission users_full_access
|
||||
* @apiParam {String} user_language The language of the new user (e.g., "en")
|
||||
* @apiParam {String} timezone The timezone of the new user (e.g., "Europe/Moscow")
|
||||
* @apiParam {Integer} role_id ID of the role of the new user
|
||||
* @apiParam {Integer} id The ID of the user being edited.
|
||||
* @apiParam {String} full_name New user name
|
||||
* @apiParam {String} email New user email
|
||||
* @apiParam {String} url URL associated with the user
|
||||
* @apiParam {Integer} company_id The ID of the company to which the user belongs
|
||||
* @apiParam {String} avatar The URL of the user’s avatar
|
||||
* @apiParam {Integer} screenshots_state State of screenshots monitoring (e.g., 1 for enabled)
|
||||
* @apiParam {Boolean} manual_time Whether manual time tracking is enabled for the new user
|
||||
* @apiParam {Integer} computer_time_popup Time in minutes before showing a time popup
|
||||
* @apiParam {Boolean} blur_screenshots Indicates if screenshots are blurred
|
||||
* @apiParam {Boolean} web_and_app_monitoring Whether web and app monitoring is enabled
|
||||
* @apiParam {Integer} screenshots_interval Interval in minutes for taking screenshots
|
||||
* @apiParam {Integer} active Will new user be active or not `(1 - active, 0 - not)`
|
||||
* @apiParam {String} deleted_at Deletion timestamp, or `null` if the user is not deleted.
|
||||
* @apiParam {Boolean} send_invite Whether to send an invitation to the new user (true - send, false - do not send)
|
||||
*
|
||||
*
|
||||
*
|
||||
* @apiParam {String} type The type of user (e.g., "employee")
|
||||
*
|
||||
* @apiParam {String} password New user password
|
||||
*
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "user_language" : "en",
|
||||
* "timezone" : "Europe/Moscow",
|
||||
* "role_id" : 2,
|
||||
* "id" : 3,
|
||||
* "full_name" : "Rachael Reichert",
|
||||
* "email": "projectAuditor@example.com",
|
||||
* "url" : null,
|
||||
* "company_id" : 1,
|
||||
* "avatar" : null,
|
||||
* "screenshots_state" : 1,
|
||||
* "manual_time" : 0,
|
||||
* "computer_time_popup" : 300,
|
||||
* "blur_screenshots" : false,
|
||||
* "web_and_app_monitoring" : true,
|
||||
* "screenshots_interval" : 5,
|
||||
* "active" : true,
|
||||
* "deleted_at" : null,
|
||||
* "created_at" : "2023-10-26T10:26:42.000000Z",
|
||||
* "updated_at" : "2023-10-26T10:26:42.000000Z",
|
||||
* "important" : 0,
|
||||
* "change_password" : 0,
|
||||
* "type" : "employee",
|
||||
* "invitation_sent" : false,
|
||||
* "nonce" : 0,
|
||||
* "client_installed" : 0,
|
||||
* "permanent_screenshots" : 0,
|
||||
* "last_activity" : "2023-10-26 10:05:42",
|
||||
* "screenshots_state_locked" : false,
|
||||
* "online" : false,
|
||||
* "can_view_team_tab" : false,
|
||||
* "can_create_task" : false
|
||||
* }
|
||||
* @apiUse UserObject
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
*/
|
||||
/**
|
||||
* @param EditUserRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function edit(EditUserRequest $request): JsonResponse
|
||||
{
|
||||
Filter::listen(Filter::getActionFilterName(), static function (User $user) use ($request) {
|
||||
if ($user->screenshots_state_locked && !$request->user()->isAdmin()) {
|
||||
$user->screenshots_state = $user->getOriginal('screenshots_state');
|
||||
return $user;
|
||||
}
|
||||
|
||||
$user->screenshots_state_locked = $request->user()->isAdmin() && ScreenshotsState::tryFrom($user->screenshots_state)->mustBeInherited();
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
return $this->_edit($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get, post} /users/show Show User
|
||||
* @apiDescription Retrieves detailed information about a specific user.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName ShowUser
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission users_show
|
||||
* @apiPermission users_full_access
|
||||
*
|
||||
* @apiParam {Integer} id User id
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
* @apiUse UserObject
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse ItemNotFoundError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse ValidationError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param ShowUserRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function show(ShowUserRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_show($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @api {post} /users/remove Destroy
|
||||
* @apiDescription Destroy User
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName DestroyUser
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission users_remove
|
||||
* @apiPermission users_full_access
|
||||
*
|
||||
* @apiParam {Integer} id ID of the target user
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message Destroy status
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 No Content
|
||||
* {
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ValidationError
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function destroy(DestroyUserRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_destroy($request);
|
||||
}
|
||||
/**
|
||||
* @throws Exception
|
||||
* @api {get,post} /users/count Count
|
||||
* @apiDescription Count Users
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Count
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission users_count
|
||||
* @apiPermission users_full_access
|
||||
*
|
||||
* @apiSuccess {String} total Amount of users that we have
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "total": 2
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function count(ListUsersRequest $request): JsonResponse
|
||||
{
|
||||
return $this->_count($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SendInviteUserRequest $request
|
||||
* @return JsonResponse
|
||||
* @throws Throwable
|
||||
*/
|
||||
/**
|
||||
* @api {post} /api/users/send-invite Send User Invitation
|
||||
* @apiDescription Sends an invitation to a user by generating a password, marking the invitation as sent, and dispatching relevant events.
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName SendUserInvite
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiPermission users_invite
|
||||
*
|
||||
* @apiParam {Integer} id The ID of the user to whom the invitation will be sent.
|
||||
*
|
||||
* @apiParamExample {json} Request Example:
|
||||
* {
|
||||
* "id": 1
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message A confirmation that the invite was sent successfully.
|
||||
*
|
||||
* @apiSuccessExample {json} Success Response:
|
||||
* HTTP/1.1 204 No Content
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ForbiddenError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function sendInvite(SendInviteUserRequest $request): JsonResponse
|
||||
{
|
||||
$requestId = Filter::process(Filter::getRequestFilterName(), $request->validated('id'));
|
||||
|
||||
$itemsQuery = $this->getQuery(['id' => $requestId]);
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $requestId);
|
||||
|
||||
$item = Filter::process(Filter::getActionFilterName(), $itemsQuery->first());
|
||||
|
||||
$password = Str::random();
|
||||
$item->password = $password;
|
||||
$item->invitation_sent = true;
|
||||
$item->save();
|
||||
|
||||
throw_unless($item, new NotFoundHttpException);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), [$requestId, $item]);
|
||||
|
||||
$language = Settings::scope('core')->get('language', 'en');
|
||||
|
||||
Mail::to($item->email)->locale($language)->send(new UserCreated($item->email, $password));
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {patch} /users/activity Activity
|
||||
* @apiDescription Updates the time of the user's last activity
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Activity
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 No Content
|
||||
* {
|
||||
* }
|
||||
*
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function updateActivity(): JsonResponse
|
||||
{
|
||||
$user = request()->user();
|
||||
|
||||
CatEvent::dispatch(Filter::getBeforeActionEventName(), $user);
|
||||
|
||||
Filter::process(Filter::getActionFilterName(), $user)->update(['last_activity' => Carbon::now()]);
|
||||
|
||||
CatEvent::dispatch(Filter::getAfterActionEventName(), $user);
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
}
|
||||
334
app/Http/Controllers/AuthController.php
Normal file
334
app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\Entities\AuthorizationException;
|
||||
use App\Exceptions\Entities\DeprecatedApiException;
|
||||
use App\Helpers\Recaptcha;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Http\Transformers\AuthTokenTransformer;
|
||||
use Cache;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
/**
|
||||
* @apiDefine AuthHeader
|
||||
* @apiHeader {String} Authorization Token for user auth
|
||||
* @apiHeaderExample {json} Authorization Header Example
|
||||
* {
|
||||
* "Authorization": "bearer 16184cf3b2510464a53c0e573c75740540fe..."
|
||||
* }
|
||||
*/
|
||||
|
||||
public function __construct(protected Recaptcha $recaptcha)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /auth/login Login
|
||||
* @apiDescription Get user Token
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Login
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiParam {String} email User email
|
||||
* @apiParam {String} password User password
|
||||
* @apiParam {String} [recaptcha] Recaptcha token
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "email": "johndoe@example.com",
|
||||
* "password": "amazingpassword",
|
||||
* "recaptcha": "03AOLTBLR5UtIoenazYWjaZ4AFZiv1OWegWV..."
|
||||
* }
|
||||
* @apiUse User
|
||||
* @apiUse 400Error
|
||||
* @apiUse ParamsValidationError
|
||||
* @apiUse UnauthorizedError
|
||||
* @apiUse UserDeactivatedError
|
||||
* @apiUse CaptchaError
|
||||
* @apiUse LimiterError
|
||||
*/
|
||||
public function login(LoginRequest $request): JsonResponse
|
||||
{
|
||||
$credentials = $request->only(['email', 'password', 'recaptcha']);
|
||||
|
||||
$this->recaptcha->check($credentials);
|
||||
|
||||
if (!auth()->attempt([
|
||||
'email' => $credentials['email'],
|
||||
'password' => $credentials['password'],
|
||||
])) {
|
||||
$this->recaptcha->incrementCaptchaAmounts();
|
||||
$this->recaptcha->check($credentials);
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
if (!$user || !$user->active) {
|
||||
$this->recaptcha->incrementCaptchaAmounts();
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_DISABLED);
|
||||
}
|
||||
|
||||
if ($user->invitation_sent) {
|
||||
$user->invitation_sent = false;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$this->recaptcha->clearCaptchaAmounts();
|
||||
|
||||
if (preg_match('/' . config('auth.cattr-client-agent') . '/', $request->header('User_agent'))) {
|
||||
$user->client_installed = 1;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return responder()->success([
|
||||
'token' => $user->createToken(Str::uuid())->plainTextToken,
|
||||
], new AuthTokenTransformer)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /auth/logout Logout
|
||||
* @apiDescription Invalidate current token
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Logout
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 OK
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /auth/logout-from-all Logout from all
|
||||
* @apiDescription Invalidate all user tokens
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Logout all
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 204 OK
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function logoutFromAll(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->tokens()->delete();
|
||||
|
||||
return responder()->success()->respond(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /auth/me Me
|
||||
* @apiDescription Get authenticated User Entity
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Me
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {Integer} id ID of the user
|
||||
* @apiSuccess {String} full_name Full name of the user
|
||||
* @apiSuccess {String} email Email of the user
|
||||
* @apiSuccess {String} [url] URL of the user (optional)
|
||||
* @apiSuccess {Integer} company_id Company ID of the user
|
||||
* @apiSuccess {String} [avatar] Avatar URL of the user (optional)
|
||||
* @apiSuccess {Boolean} screenshots_active Indicates if screenshots are active
|
||||
* @apiSuccess {Boolean} manual_time Indicates if manual time tracking is allowed
|
||||
* @apiSuccess {Integer} computer_time_popup Time interval for computer time popup
|
||||
* @apiSuccess {Boolean} blur_screenshots Indicates if screenshots are blurred
|
||||
* @apiSuccess {Boolean} web_and_app_monitoring Indicates if web and app monitoring is enabled
|
||||
* @apiSuccess {Integer} screenshots_interval Interval for taking screenshots
|
||||
* @apiSuccess {Boolean} active Indicates if the user is active
|
||||
* @apiSuccess {String} [deleted_at] Deletion timestamp (if applicable, otherwise null)
|
||||
* @apiSuccess {String} created_at Creation timestamp
|
||||
* @apiSuccess {String} updated_at Last update timestamp
|
||||
* @apiSuccess {String} [timezone] Timezone of the user (optional)
|
||||
* @apiSuccess {Boolean} important Indicates if the user is marked as important
|
||||
* @apiSuccess {Boolean} change_password Indicates if the user needs to change password
|
||||
* @apiSuccess {Integer} role_id Role ID of the user
|
||||
* @apiSuccess {String} user_language Language of the user
|
||||
* @apiSuccess {String} type Type of the user (e.g., "employee")
|
||||
* @apiSuccess {Boolean} invitation_sent Indicates if invitation is sent to the user
|
||||
* @apiSuccess {Integer} nonce Nonce value of the user
|
||||
* @apiSuccess {Boolean} client_installed Indicates if client is installed
|
||||
* @apiSuccess {Boolean} permanent_screenshots Indicates if screenshots are permanent
|
||||
* @apiSuccess {String} last_activity Last activity timestamp of the user
|
||||
* @apiSuccess {Boolean} online Indicates if the user is online
|
||||
* @apiSuccess {Boolean} can_view_team_tab Indicates if the user can view team tab
|
||||
* @apiSuccess {Boolean} can_create_task Indicates if the user can create tasks
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "id": 1,
|
||||
* "full_name": "Admin",
|
||||
* "email": "admin@cattr.app",
|
||||
* "url": "",
|
||||
* "company_id": 1,
|
||||
* "avatar": "",
|
||||
* "screenshots_active": 1,
|
||||
* "manual_time": 0,
|
||||
* "computer_time_popup": 300,
|
||||
* "blur_screenshots": false,
|
||||
* "web_and_app_monitoring": true,
|
||||
* "screenshots_interval": 5,
|
||||
* "active": 1,
|
||||
* "deleted_at": null,
|
||||
* "created_at": "2023-10-26T10:26:17.000000Z",
|
||||
* "updated_at": "2024-02-15T19:06:42.000000Z",
|
||||
* "timezone": null,
|
||||
* "important": 0,
|
||||
* "change_password": 0,
|
||||
* "role_id": 0,
|
||||
* "user_language": "en",
|
||||
* "type": "employee",
|
||||
* "invitation_sent": false,
|
||||
* "nonce": 0,
|
||||
* "client_installed": 0,
|
||||
* "permanent_screenshots": 0,
|
||||
* "last_activity": "2023-10-26 10:26:17",
|
||||
* "online": false,
|
||||
* "can_view_team_tab": true,
|
||||
* "can_create_task": true
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
return responder()->success($request->user())->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /auth/desktop-key Issue key
|
||||
* @apiDescription Issues key for desktop auth
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Issue key
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
* @apiUse User
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function issueDesktopKey(Request $request): JsonResponse
|
||||
{
|
||||
$token = Str::random(40);
|
||||
|
||||
$lifetime = now()->addMinutes(config('auth.lifetime_minutes.desktop_token'));
|
||||
|
||||
Cache::store('octane')->put(
|
||||
sha1($request->ip()) . ":$token",
|
||||
$request->user()->id,
|
||||
$lifetime,
|
||||
);
|
||||
return responder()->success([
|
||||
'token' => $token,
|
||||
'type' => 'desktop',
|
||||
'expires' => now()->addMinutes(config('auth.lifetime_minutes.desktop_token'))->toIso8601String(),
|
||||
], new AuthTokenTransformer)->meta(['frontend_uri' => config('app.frontend_url')])->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {put} /auth/desktop-key Key auth
|
||||
* @apiDescription Exchange desktop key to JWT
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Key auth
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
* @apiUse User
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function authDesktopKey(Request $request): JsonResponse
|
||||
{
|
||||
$token = $request->header('Authorization');
|
||||
|
||||
if (!$token) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$token = explode(' ', $token);
|
||||
|
||||
if (count($token) !== 2 || $token[0] !== 'desktop' || !Cache::store('octane')->has(sha1($request->ip()) . ":$token[1]")) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$user = auth()->loginUsingId(Cache::store('octane')->get(sha1($request->ip()) . ":$token[1]"));
|
||||
|
||||
if (!optional($user)->active) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_DISABLED);
|
||||
}
|
||||
|
||||
return responder()->success([
|
||||
'token' => $user->createToken(Str::uuid())->plainTextToken,
|
||||
], new AuthTokenTransformer)->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiDeprecated Exists only for compatibility with old Cattr client
|
||||
* @api {post} /auth/refresh Refresh
|
||||
* @apiDescription Refreshes JWT
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Refresh
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiUse AuthHeader
|
||||
*
|
||||
* @apiSuccess {String} access_token Token
|
||||
* @apiSuccess {String} token_type Token Type
|
||||
* @apiSuccess {String} expires_in Token TTL 8601String Date
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
|
||||
/**
|
||||
* @return JsonResponse
|
||||
* @deprecated Exists only for compatibility with old Cattr client
|
||||
*/
|
||||
public function refresh(): JsonResponse
|
||||
{
|
||||
throw new DeprecatedApiException();
|
||||
}
|
||||
}
|
||||
71
app/Http/Controllers/Controller.php
Normal file
71
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Routing\Route as RouteModel;
|
||||
use Illuminate\Routing\RouteCollection;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Collection;
|
||||
use Route;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use DispatchesJobs;
|
||||
use ValidatesRequests;
|
||||
|
||||
public static function getControllerRules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function frontendRoute(Request $request) {
|
||||
return view('app');
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel router pass to fallback not non-exist urls only but wrong-method requests too.
|
||||
* So required to check if route have alternative request methods
|
||||
* throw not-found or wrong-method exceptions manually
|
||||
* @param Request $request
|
||||
*/
|
||||
public function universalRoute(Request $request): void
|
||||
{
|
||||
/** @var Router $router */
|
||||
$router = app('router');
|
||||
/** @var RouteCollection $routes */
|
||||
$routeCollection = $router->getRoutes();
|
||||
/** @var string[] $methods */
|
||||
$methods = array_diff(Router::$verbs, [$request->getMethod(), 'OPTIONS']);
|
||||
|
||||
foreach ($methods as $method) {
|
||||
// Get all routes for method without fallback routes
|
||||
/** @var Route[]|Collection $routes */
|
||||
$routes = collect($routeCollection->get($method))->filter(static function ($route) {
|
||||
/** @var RouteModel $route */
|
||||
return !$route->isFallback && $route->uri !== '{fallbackPlaceholder}';
|
||||
});
|
||||
|
||||
// Look if any route have match with current request
|
||||
$mismatch = $routes->first(static function ($value) use ($request) {
|
||||
/** @var RouteModel $value */
|
||||
return $value->matches($request, false);
|
||||
});
|
||||
|
||||
// Throw wrong-method exception if matches found
|
||||
if ($mismatch !== null) {
|
||||
throw new MethodNotAllowedHttpException([]);
|
||||
}
|
||||
}
|
||||
|
||||
// No matches, throw not-found exception
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
}
|
||||
237
app/Http/Controllers/PasswordResetController.php
Normal file
237
app/Http/Controllers/PasswordResetController.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\Entities\AuthorizationException;
|
||||
use App\Helpers\Recaptcha;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset as PasswordResetEvent;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use DB;
|
||||
use Mail;
|
||||
use Password;
|
||||
use Validator;
|
||||
|
||||
class PasswordResetController extends BaseController
|
||||
{
|
||||
public function __construct(private Recaptcha $recaptcha)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /v1/auth/password/reset/validate Validate
|
||||
* @apiDescription Validates password reset token
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Validate token
|
||||
* @apiGroup Password Reset
|
||||
*
|
||||
* @apiParam {String} email User email
|
||||
* @apiParam {String} token Password reset token
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "email": "johndoe@example.com",
|
||||
* "token": "03AOLTBLR5UtIoenazYWjaZ4AFZiv1OWegWV..."
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message Message from server
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "message": "Password reset data is valid"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ParamsValidationError
|
||||
* @apiUse InvalidPasswordResetDataError
|
||||
*/
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function validate(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'token' => 'required|string'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_VALIDATION_FAILED);
|
||||
}
|
||||
|
||||
$user = Password::broker()->getUser($request->all());
|
||||
if (!$user) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_INVALID_PASSWORD_RESET_DATA);
|
||||
}
|
||||
|
||||
$isValidToken = Password::broker()->getRepository()->exists($user, $request->input('token'));
|
||||
if (!$isValidToken) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_INVALID_PASSWORD_RESET_DATA);
|
||||
}
|
||||
|
||||
return new JsonResponse(['message' => 'Password reset data is valid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /v1/auth/password/reset/request Request
|
||||
* @apiDescription Sends email to user with reset link
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Request
|
||||
* @apiGroup Password Reset
|
||||
*
|
||||
* @apiParam {String} login User login
|
||||
* @apiParam {String} [recaptcha] Recaptcha token
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "email": "johndoe@example.com",
|
||||
* "recaptcha": "03AOLTBLR5UtIoenazYWjaZ4AFZiv1OWegWV..."
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} message Message from server
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "message": "Link for restore password has been sent to specified email"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ParamsValidationError
|
||||
* @apiUse NoSuchUserError
|
||||
* @apiUse CaptchaError
|
||||
* @apiUse LimiterError
|
||||
*/
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function request(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), ['email' => 'required|email']);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_VALIDATION_FAILED);
|
||||
}
|
||||
|
||||
$credentials = $request->only(['email', 'recaptcha']);
|
||||
$this->recaptcha->check($credentials);
|
||||
$user = User::where('email', $credentials['email'])->first();
|
||||
|
||||
if (!$user) {
|
||||
$this->recaptcha->incrementCaptchaAmounts();
|
||||
$this->recaptcha->check($credentials);
|
||||
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$this->recaptcha->clearCaptchaAmounts();
|
||||
|
||||
Password::broker()->sendResetLink(['email' => $credentials['email']]);
|
||||
|
||||
return new JsonResponse([
|
||||
'message' => 'Link for restore password has been sent to specified email',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @api {post} /v1/auth/password/reset/process Process
|
||||
* @apiDescription Resets user password
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Process
|
||||
* @apiGroup Password Reset
|
||||
*
|
||||
* @apiParam {String} email User email
|
||||
* @apiParam {String} token Password reset token
|
||||
* @apiParam {String} password New password
|
||||
* @apiParam {String} password_confirmation Password confirmation
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "email": "johndoe@example.com",
|
||||
* "token": "16184cf3b2510464a53c0e573c75740540fe...",
|
||||
* "password_confirmation": "amazingpassword",
|
||||
* "password": "amazingpassword"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {String} access_token Token
|
||||
* @apiSuccess {String} token_type Token Type
|
||||
* @apiSuccess {String} expires_in Token TTL in seconds
|
||||
* @apiSuccess {Object} user User Entity
|
||||
*
|
||||
* @apiUse UserObject
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "access_token": "16184cf3b2510464a53c0e573c75740540fe...",
|
||||
* "token_type": "bearer",
|
||||
* "password": "amazingpassword",
|
||||
* "expires_in": "3600",
|
||||
* "user": {}
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
* @apiUse ParamsValidationError
|
||||
* @apiUse InvalidPasswordResetDataError
|
||||
* @apiUse UnauthorizedError
|
||||
*/
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function process(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'token' => 'required|string',
|
||||
'password' => 'required',
|
||||
'password_confirmation' => 'required'
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_VALIDATION_FAILED);
|
||||
}
|
||||
|
||||
$resetRequest = DB::table('password_resets')
|
||||
->where('email', $request->input('email'))
|
||||
->first();
|
||||
|
||||
if (!$resetRequest) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_INVALID_PASSWORD_RESET_DATA);
|
||||
}
|
||||
|
||||
$response = Password::broker()->reset(
|
||||
$request->all(),
|
||||
static function (User $user, string $password) {
|
||||
$user->password = $password;
|
||||
$user->save();
|
||||
event(new PasswordResetEvent($user));
|
||||
auth()->login($user);
|
||||
}
|
||||
);
|
||||
|
||||
if ($response !== Password::PASSWORD_RESET) {
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$token = auth()->setTTL(config('auth.lifetime_minutes.jwt'))->refresh();
|
||||
|
||||
return new JsonResponse([
|
||||
'access_token' => $token,
|
||||
'token_type' => 'bearer',
|
||||
'expires_in' => now()->addMinutes(config('auth.lifetime_minutes.jwt'))->toIso8601String(),
|
||||
'user' => auth()->user(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
150
app/Http/Controllers/RegistrationController.php
Normal file
150
app/Http/Controllers/RegistrationController.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ScreenshotsState;
|
||||
use App\Models\Invitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Settings;
|
||||
|
||||
/**
|
||||
* Class RegistrationController
|
||||
* @codeCoverageIgnore until it is implemented on frontend
|
||||
*/
|
||||
class RegistrationController extends Controller
|
||||
{
|
||||
/**
|
||||
* @param $key
|
||||
* @return JsonResponse
|
||||
* @api {get} /auth/register/{key} Get Form
|
||||
* @apiDescription Returns invitation form data by a invitation token
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName GetRegistration
|
||||
* @apiGroup Invitation
|
||||
*
|
||||
* @apiParam (Parameters from url) {String} key User invitation key
|
||||
*
|
||||
* @apiSuccess {String} email UserInvited email
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "email": "test@example.com"
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Email not found
|
||||
* HTTP/1.1 404 Not found
|
||||
* {
|
||||
* "error": "Not found"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
*
|
||||
*/
|
||||
public function getForm($key): JsonResponse
|
||||
{
|
||||
$invitation = Invitation::where('key', $key)
|
||||
->where('expires_at', '>=', time())
|
||||
->first();
|
||||
|
||||
if (!isset($invitation)) {
|
||||
return new JsonResponse([
|
||||
'message' => __('The specified key has expired or does not exist')
|
||||
], 404);
|
||||
}
|
||||
|
||||
return responder()->success(['email' => $invitation->email])->respond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $key
|
||||
* @return JsonResponse
|
||||
* @api {post} /auth/register/{key} Post Form
|
||||
* @apiDescription Registers user by key
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName PostRegistration
|
||||
* @apiGroup Invitation
|
||||
*
|
||||
* @apiParam (Parameters from url) {String} key User invitation key
|
||||
*
|
||||
* @apiParam {String} email New user email
|
||||
* @apiParam {String} password New user password
|
||||
* @apiParam {String} fullName New user name
|
||||
*
|
||||
* @apiParamExample {json} Request Example
|
||||
* {
|
||||
* "email": "johndoe@example.com",
|
||||
* "password": "amazingpassword",
|
||||
* "fullName": "John Doe"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Number} user_id New user ID
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user_id": 2
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Email not found
|
||||
* HTTP/1.1 404 Not found
|
||||
* {
|
||||
* "message": "The specified key has expired or does not exist"
|
||||
* }
|
||||
*
|
||||
* @apiErrorExample {json} Email mismatch
|
||||
* HTTP/1.1 400 Bad request
|
||||
* {
|
||||
* "message": "The email address does not match the key"
|
||||
* }
|
||||
*
|
||||
* @apiUse 400Error
|
||||
*
|
||||
*/
|
||||
public function postForm(Request $request, string $key): JsonResponse
|
||||
{
|
||||
$invitation = Invitation::where('key', $key)
|
||||
->where('expires_at', '>=', time())
|
||||
->first();
|
||||
|
||||
if (!isset($invitation)) {
|
||||
return new JsonResponse([
|
||||
'message' => __('The specified key has expired or does not exist'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
if ($request->input('email') !== $invitation->email) {
|
||||
return new JsonResponse([
|
||||
'message' => __('The email address does not match the key'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$language = Settings::scope('core')->get('language', 'en');
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::create([
|
||||
'full_name' => $request->input('full_name'),
|
||||
'email' => $request->input('email'),
|
||||
'password' => $request->input('password'),
|
||||
'active' => true,
|
||||
'manual_time' => false,
|
||||
'screenshots_state' => ScreenshotsState::REQUIRED,
|
||||
'screenshots_state_locked' => true,
|
||||
'computer_time_popup' => 3,
|
||||
'screenshots_interval' => 10,
|
||||
'role_id' => $invitation->role_id,
|
||||
'user_language' => $language,
|
||||
]);
|
||||
|
||||
$invitation->delete();
|
||||
|
||||
return responder()->success(['user_id' => $user->id])->respond();
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/StatusController.php
Normal file
44
app/Http/Controllers/StatusController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\CatHelper;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class StatusController extends Controller
|
||||
{
|
||||
/**
|
||||
* @api {get} /status Status
|
||||
* @apiDescription Check API status
|
||||
*
|
||||
* @apiVersion 4.0.0
|
||||
* @apiName Status
|
||||
* @apiGroup Status
|
||||
*
|
||||
* @apiSuccess {String} cat A cat for you
|
||||
* @apiSuccess {Array} modules Information about installed modules
|
||||
* @apiSuccess {Array} version Information about version modules
|
||||
*
|
||||
* @apiSuccessExample {json} Response Example
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "cattr": true,
|
||||
* "cat": "(=ㅇ༝ㅇ=)"
|
||||
* "version": "dev"
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
return responder()->success([
|
||||
'cattr' => true,
|
||||
'cat' => CatHelper::getCat(),
|
||||
'version' => config('app.version'),
|
||||
])->respond();
|
||||
}
|
||||
}
|
||||
85
app/Http/Kernel.php
Normal file
85
app/Http/Kernel.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Http\Middleware\Authenticate;
|
||||
use App\Http\Middleware\RegisterModulesEvents;
|
||||
use App\Http\Middleware\SentryContext;
|
||||
use App\Http\Middleware\TrimStrings;
|
||||
use Illuminate\Http\Middleware\HandleCors;
|
||||
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
||||
use Illuminate\Auth\Middleware\Authorize;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
||||
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||
use Illuminate\Http\Middleware\TrustProxies;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Routing\Middleware\ValidateSignature;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
|
||||
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
|
||||
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [
|
||||
CheckForMaintenanceMode::class,
|
||||
ValidatePostSize::class,
|
||||
TrimStrings::class,
|
||||
ConvertEmptyStringsToNull::class,
|
||||
HandleCors::class,
|
||||
SentryContext::class,
|
||||
TrustProxies::class,
|
||||
RegisterModulesEvents::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
SubstituteBindings::class,
|
||||
EnsureFrontendRequestsAreStateful::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*
|
||||
* These middleware may be assigned to groups or used individually.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => Authenticate::class,
|
||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||
'bindings' => SubstituteBindings::class,
|
||||
'can' => Authorize::class,
|
||||
'throttle' => ThrottleRequests::class,
|
||||
'abilities' => CheckAbilities::class,
|
||||
'ability' => CheckForAnyAbility::class,
|
||||
'signed' => ValidateSignature::class,
|
||||
];
|
||||
}
|
||||
26
app/Http/Middleware/Authenticate.php
Normal file
26
app/Http/Middleware/Authenticate.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Exceptions\Entities\AuthorizationException;
|
||||
use Closure;
|
||||
use Illuminate\Auth\Middleware\Authenticate as BaseAuthenticate;
|
||||
use Lang;
|
||||
|
||||
class Authenticate extends BaseAuthenticate
|
||||
{
|
||||
public const DEFAULT_USER_LANGUAGE = 'en';
|
||||
|
||||
public function handle($request, Closure $next, ...$guards): mixed
|
||||
{
|
||||
$this->authenticate($request, $guards);
|
||||
|
||||
if (!$request->user()->active) {
|
||||
$request->user()->tokens()->whereId($request->user()->currentAccessToken()->id)->delete();
|
||||
throw new AuthorizationException(AuthorizationException::ERROR_TYPE_USER_DISABLED);
|
||||
}
|
||||
|
||||
Lang::setLocale($request->user()->user_language ?: self::DEFAULT_USER_LANGUAGE);
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/EncryptCookies.php
Normal file
17
app/Http/Middleware/EncryptCookies.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
99
app/Http/Middleware/RegisterModulesEvents.php
Normal file
99
app/Http/Middleware/RegisterModulesEvents.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App;
|
||||
use App\Events\ChangeEvent;
|
||||
use App\Filters\AttachmentFilter;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectGroup;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskHistory;
|
||||
use App\Models\TimeInterval;
|
||||
use App\Observers\AttachmentObserver;
|
||||
use CatEvent;
|
||||
use Filter;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RegisterModulesEvents
|
||||
{
|
||||
/**
|
||||
* @param Task|Project|TimeInterval|TaskHistory $model
|
||||
*/
|
||||
public static function broadcastEvent(string $entityType, string $action, $model): void
|
||||
{
|
||||
foreach (ChangeEvent::getRelatedUserIds($model) as $userId) {
|
||||
broadcast(new ChangeEvent($entityType, $action, $model, $userId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subscribers from modules for Event and Filter
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// TODO:
|
||||
// [ ] move to Observers folder
|
||||
// [ ] rewrite with laravel Event so updates that come from modules will trigger update
|
||||
CatEvent::listen('event.after.action.*', static function (string $eventName, array $data) {
|
||||
$eventNameParts = explode('.', $eventName);
|
||||
[$entityType, $action] = array_slice($eventNameParts, 3, 2); // Strip "event.after.action" and get the next two parts
|
||||
if (!in_array($entityType, ['tasks', 'projects', 'projects_members', 'intervals', 'task_comments', 'project_groups'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($action, ['create', 'edit', 'destroy'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entityType === 'projects_members') {
|
||||
$entityType = 'projects';
|
||||
$projectId = $data[0];
|
||||
$model = Project::query()->find($projectId);
|
||||
} elseif ($entityType === 'task_comments') {
|
||||
$entityType = 'tasks_activities';
|
||||
$model = $data[0];
|
||||
} elseif ($entityType === 'project_groups') {
|
||||
$entityType = 'projects';
|
||||
$action = 'edit';
|
||||
/** @var ProjectGroup $group */
|
||||
$group = $data[0];
|
||||
$model = $group->projects()->get();
|
||||
} else {
|
||||
$model = $data[0];
|
||||
}
|
||||
|
||||
App::terminating(static function () use ($entityType, $action, $model) {
|
||||
$items = is_array($model) || $model instanceof Collection ? $model : [$model];
|
||||
foreach ($items as $item) {
|
||||
static::broadcastEvent($entityType, $action, $item);
|
||||
|
||||
if (in_array($entityType, ['tasks', 'projects'])) {
|
||||
$project = match (true) {
|
||||
$item instanceof Task => $item->project,
|
||||
$item instanceof Project => $item,
|
||||
};
|
||||
|
||||
static::broadcastEvent('gantt', 'updateAll', $project);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// CatEvent and Filter are scoped to request, subscribe on every request for it to work with laravel octane
|
||||
Filter::subscribe(AttachmentFilter::class);
|
||||
CatEvent::subscribe(AttachmentObserver::class);
|
||||
|
||||
collect(Module::allEnabled())->each(static function (\Nwidart\Modules\Module $module) {
|
||||
App::call([preg_grep("/ModuleServiceProvider$/i", $module->get('providers'))[0], 'registerEvents']);
|
||||
});
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
52
app/Http/Middleware/SentryContext.php
Normal file
52
app/Http/Middleware/SentryContext.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\Role;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Route;
|
||||
use Sentry\State\Scope;
|
||||
use Throwable;
|
||||
use function Sentry\configureScope;
|
||||
|
||||
class SentryContext
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if (env('IMAGE_VERSION')) {
|
||||
configureScope(static function (Scope $scope): void {
|
||||
$scope->setTag('docker', env('IMAGE_VERSION'));
|
||||
});
|
||||
}
|
||||
|
||||
if ($user = $request->user()) {
|
||||
configureScope(static function (Scope $scope) use ($user): void {
|
||||
$scope->setUser([
|
||||
'id' => $user->id,
|
||||
'email' => config('sentry.send_default_pii') ? $user->email : sha1($user->email),
|
||||
'role' => is_int($user->role_id) ? Role::tryFrom($user->role_id)?->name :$user->role_id->name,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
configureScope(static function (Scope $scope) use ($request): void {
|
||||
$scope->setTag('request.host', $request->host());
|
||||
$scope->setTag('request.method', $request->method());
|
||||
try {
|
||||
$scope->setTag('request.route', Route::getRoutes()->match($request)->getName());
|
||||
} catch (Throwable) {
|
||||
}
|
||||
});
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
18
app/Http/Middleware/TrimStrings.php
Normal file
18
app/Http/Middleware/TrimStrings.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as BaseTrimmer;
|
||||
|
||||
class TrimStrings extends BaseTrimmer
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
}
|
||||
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
|
||||
|
||||
class VerifyCsrfToken extends BaseVerifier
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'/api/*'
|
||||
];
|
||||
}
|
||||
25
app/Http/Requests/Attachment/CreateAttachmentRequest.php
Normal file
25
app/Http/Requests/Attachment/CreateAttachmentRequest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Attachment;
|
||||
|
||||
use App\Helpers\AttachmentHelper;
|
||||
use App\Http\Requests\AuthorizesAfterValidation;
|
||||
use App\Http\Requests\CattrFormRequest;
|
||||
|
||||
class CreateAttachmentRequest extends CattrFormRequest
|
||||
{
|
||||
use AuthorizesAfterValidation;
|
||||
|
||||
public function authorizeValidated(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function _rules(): array
|
||||
{
|
||||
$maxFileSize = AttachmentHelper::getMaxAllowedFileSize();
|
||||
return [
|
||||
'attachment' => "file|required|max:$maxFileSize",
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/Attachment/DownloadAttachmentRequest.php
Normal file
23
app/Http/Requests/Attachment/DownloadAttachmentRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Attachment;
|
||||
|
||||
use App\Http\Requests\AuthorizesAfterValidation;
|
||||
use App\Http\Requests\CattrFormRequest;
|
||||
|
||||
class DownloadAttachmentRequest extends CattrFormRequest
|
||||
{
|
||||
use AuthorizesAfterValidation;
|
||||
|
||||
public function authorizeValidated(): bool
|
||||
{
|
||||
return $this->user()->can('view', request('attachment')->project);
|
||||
}
|
||||
|
||||
public function _rules(): array
|
||||
{
|
||||
return [
|
||||
'seconds' => 'sometimes|int'
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/Auth/LoginRequest.php
Normal file
22
app/Http/Requests/Auth/LoginRequest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use App\Http\Requests\CattrFormRequest;
|
||||
|
||||
class LoginRequest extends CattrFormRequest
|
||||
{
|
||||
public function _authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function _rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required',
|
||||
'password' => 'required',
|
||||
'recaptcha' => 'sometimes|nullable|string'
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/AuthorizesAfterValidation.php
Normal file
34
app/Http/Requests/AuthorizesAfterValidation.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Helpers\FilterDispatcher;
|
||||
use Filter;
|
||||
|
||||
trait AuthorizesAfterValidation
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function _authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $validator
|
||||
*/
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
if (! $validator->failed() && ! Filter::process(Filter::getAuthValidationFilterName(), $this->authorizeValidated())) {
|
||||
$this->failedAuthorization();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function authorizeValidated(): mixed;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user