-
-
Notifications
You must be signed in to change notification settings - Fork 143
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
30 changed files
with
1,921 additions
and
150 deletions.
There are no files selected for viewing
61 changes: 61 additions & 0 deletions
61
app/Console/Commands/Report/ReportSetExpiredToPrivateCommand.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Console\Commands\Report; | ||
|
||
use App\Models\Report; | ||
use Illuminate\Console\Command; | ||
use Illuminate\Database\Eloquent\Collection; | ||
use Illuminate\Support\Carbon; | ||
|
||
class ReportSetExpiredToPrivateCommand extends Command | ||
{ | ||
/** | ||
* The name and signature of the console command. | ||
* | ||
* @var string | ||
*/ | ||
protected $signature = 'report:set-expired-to-private '. | ||
' { --dry-run : Do not actually save anything to the database, just output what would happen }'; | ||
|
||
/** | ||
* The console command description. | ||
* | ||
* @var string | ||
*/ | ||
protected $description = 'Makes public reports private if the public_until date has passed.'; | ||
|
||
/** | ||
* Execute the console command. | ||
*/ | ||
public function handle(): int | ||
{ | ||
$this->comment('Makes public reports private if the public_until date has passed...'); | ||
$dryRun = (bool) $this->option('dry-run'); | ||
if ($dryRun) { | ||
$this->comment('Running in dry-run mode. Nothing will be saved to the database.'); | ||
} | ||
|
||
$resetReports = 0; | ||
Report::query() | ||
->where('public_until', '<', Carbon::now()) | ||
->orderBy('created_at', 'asc') | ||
->chunk(500, function (Collection $reports) use ($dryRun, &$resetReports): void { | ||
/** @var Collection<int, Report> $reports */ | ||
foreach ($reports as $report) { | ||
$this->info('Make report "'.$report->name.'" ('.$report->getKey().') private, expired: '.$report->public_until->toIso8601ZuluString().' ('.$report->public_until->diffForHumans().')'); | ||
$resetReports++; | ||
if (! $dryRun) { | ||
$report->is_public = false; | ||
$report->share_secret = null; | ||
$report->save(); | ||
} | ||
} | ||
}); | ||
|
||
$this->comment('Finished setting '.$resetReports.' expired reports to private...'); | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Http\Controllers\Api\V1\Public; | ||
|
||
use App\Http\Controllers\Api\V1\Controller; | ||
use App\Http\Resources\V1\Report\DetailedReportResource; | ||
use App\Models\Report; | ||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Database\Eloquent\ModelNotFoundException; | ||
use Illuminate\Http\Request; | ||
|
||
class ReportController extends Controller | ||
{ | ||
/** | ||
* Get report by a share secret | ||
* | ||
* This endpoint is public and does not require authentication. The report must be public and not expired. | ||
* The report is considered expired if the `public_until` field is set and the date is in the past. | ||
* The report is considered public if the `is_public` field is set to `true`. | ||
* | ||
* @operationId getPublicReport | ||
*/ | ||
public function show(Request $request): DetailedReportResource | ||
{ | ||
$shareSecret = $request->header('X-Api-Key'); | ||
if (! is_string($shareSecret)) { | ||
throw new ModelNotFoundException; | ||
} | ||
|
||
$report = Report::query() | ||
->where('share_secret', '=', $shareSecret) | ||
->where('is_public', '=', true) | ||
->where(function (Builder $builder): void { | ||
/** @var Builder<Report> $builder */ | ||
$builder->whereNull('public_until') | ||
->orWhere('public_until', '>', now()); | ||
}) | ||
->firstOrFail(); | ||
|
||
return new DetailedReportResource($report); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Http\Controllers\Api\V1; | ||
|
||
use App\Enums\TimeEntryAggregationType; | ||
use App\Http\Requests\V1\Report\ReportStoreRequest; | ||
use App\Http\Requests\V1\Report\ReportUpdateRequest; | ||
use App\Http\Resources\V1\Report\DetailedReportResource; | ||
use App\Http\Resources\V1\Report\ReportCollection; | ||
use App\Models\Organization; | ||
use App\Models\Report; | ||
use App\Service\Dto\ReportPropertiesDto; | ||
use App\Service\ReportService; | ||
use Illuminate\Auth\Access\AuthorizationException; | ||
use Illuminate\Http\JsonResponse; | ||
|
||
class ReportController extends Controller | ||
{ | ||
/** | ||
* @throws AuthorizationException | ||
*/ | ||
protected function checkPermission(Organization $organization, string $permission, ?Report $report = null): void | ||
{ | ||
parent::checkPermission($organization, $permission); | ||
if ($report !== null && $report->organization_id !== $organization->id) { | ||
throw new AuthorizationException('Report does not belong to organization'); | ||
} | ||
} | ||
|
||
/** | ||
* Get reports | ||
* | ||
* @throws AuthorizationException | ||
* | ||
* @operationId getReports | ||
*/ | ||
public function index(Organization $organization): ReportCollection | ||
{ | ||
$this->checkPermission($organization, 'reports:view'); | ||
|
||
$reports = Report::query() | ||
->orderBy('created_at', 'desc') | ||
->whereBelongsTo($organization, 'organization') | ||
->paginate(config('app.pagination_per_page_default')); | ||
|
||
return new ReportCollection($reports); | ||
} | ||
|
||
/** | ||
* Get report | ||
* | ||
* @throws AuthorizationException | ||
* | ||
* @operationId getReport | ||
*/ | ||
public function show(Organization $organization, Report $report): DetailedReportResource | ||
{ | ||
$this->checkPermission($organization, 'reports:view', $report); | ||
|
||
return new DetailedReportResource($report); | ||
} | ||
|
||
/** | ||
* Create report | ||
* | ||
* @throws AuthorizationException | ||
* | ||
* @operationId createReport | ||
*/ | ||
public function store(Organization $organization, ReportStoreRequest $request): DetailedReportResource | ||
{ | ||
$this->checkPermission($organization, 'reports:create'); | ||
|
||
$report = new Report; | ||
$report->name = $request->getName(); | ||
$report->description = $request->getDescription(); | ||
$isPublic = $request->getIsPublic(); | ||
$report->is_public = $isPublic; | ||
$properties = new ReportPropertiesDto; | ||
$properties->group = TimeEntryAggregationType::from($request->input('properties.group')); | ||
$properties->subGroup = TimeEntryAggregationType::from($request->input('properties.sub_group')); | ||
$report->properties = $properties; | ||
if ($isPublic) { | ||
$report->share_secret = app(ReportService::class)->generateSecret(); | ||
$report->public_until = $request->getPublicUntil(); | ||
} else { | ||
$report->share_secret = null; | ||
$report->public_until = null; | ||
} | ||
$report->organization()->associate($organization); | ||
$report->save(); | ||
|
||
return new DetailedReportResource($report); | ||
} | ||
|
||
/** | ||
* Update report | ||
* | ||
* @throws AuthorizationException | ||
* | ||
* @operationId updateReport | ||
*/ | ||
public function update(Organization $organization, Report $report, ReportUpdateRequest $request): DetailedReportResource | ||
{ | ||
$this->checkPermission($organization, 'reports:update', $report); | ||
|
||
if ($request->has('name')) { | ||
$report->name = $request->getName(); | ||
} | ||
if ($request->has('description')) { | ||
$report->description = $request->getDescription(); | ||
} | ||
if ($request->has('is_public') && $request->getIsPublic() !== $report->is_public) { | ||
$isPublic = $request->getIsPublic(); | ||
$report->is_public = $isPublic; | ||
if ($isPublic) { | ||
$report->share_secret = app(ReportService::class)->generateSecret(); | ||
$report->public_until = $request->getPublicUntil(); | ||
} else { | ||
$report->share_secret = null; | ||
$report->public_until = null; | ||
} | ||
} | ||
$report->save(); | ||
|
||
return new DetailedReportResource($report); | ||
} | ||
|
||
/** | ||
* Delete report | ||
* | ||
* @throws AuthorizationException | ||
* | ||
* @operationId deleteReport | ||
*/ | ||
public function destroy(Organization $organization, Report $report): JsonResponse | ||
{ | ||
$this->checkPermission($organization, 'reports:delete', $report); | ||
|
||
$report->delete(); | ||
|
||
return response()->json(null, 204); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Http\Requests\V1\Report; | ||
|
||
use App\Enums\TimeEntryAggregationType; | ||
use App\Models\Organization; | ||
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule; | ||
use Illuminate\Contracts\Validation\ValidationRule; | ||
use Illuminate\Foundation\Http\FormRequest; | ||
use Illuminate\Support\Carbon; | ||
use Illuminate\Validation\Rule; | ||
|
||
/** | ||
* @property Organization $organization Organization from model binding | ||
*/ | ||
class ReportStoreRequest extends FormRequest | ||
{ | ||
/** | ||
* Get the validation rules that apply to the request. | ||
* | ||
* @return array<string, array<string|ValidationRule|LegacyValidationRule>> | ||
*/ | ||
public function rules(): array | ||
{ | ||
return [ | ||
'name' => [ | ||
'required', | ||
'string', | ||
'max:255', | ||
], | ||
'description' => [ | ||
'nullable', | ||
'string', | ||
], | ||
'is_public' => [ | ||
'required', | ||
'boolean', | ||
], | ||
// After this date the report will be automatically set to private (is_public=false) (ISO 8601 format, UTC timezone) | ||
'public_until' => [ | ||
'nullable', | ||
'date_format:Y-m-d\TH:i:s\Z', | ||
'after:now', | ||
], | ||
'properties' => [ | ||
'required', | ||
'array', | ||
], | ||
'properties.start' => [ | ||
'nullable', | ||
'date_format:Y-m-d\TH:i:s\Z', | ||
], | ||
'properties.end' => [ | ||
'nullable', | ||
'date_format:Y-m-d\TH:i:s\Z', | ||
], | ||
'properties.active' => [ | ||
'nullable', | ||
'boolean', | ||
], | ||
'properties.member_ids' => [ | ||
'nullable', | ||
'array', | ||
], | ||
'properties.member_ids.*' => [ | ||
'string', | ||
'uuid', | ||
], | ||
'properties.billable' => [ | ||
'nullable', | ||
'boolean', | ||
], | ||
'properties.client_ids' => [ | ||
'nullable', | ||
'array', | ||
], | ||
'properties.client_ids.*' => [ | ||
'string', | ||
'uuid', | ||
], | ||
'properties.project_ids' => [ | ||
'nullable', | ||
'array', | ||
], | ||
'properties.project_ids.*' => [ | ||
'string', | ||
'uuid', | ||
], | ||
'properties.tag_ids' => [ | ||
'nullable', | ||
'array', | ||
], | ||
'properties.tag_ids.*' => [ | ||
'string', | ||
'uuid', | ||
], | ||
'properties.task_ids' => [ | ||
'nullable', | ||
'array', | ||
], | ||
'properties.task_ids.*' => [ | ||
'string', | ||
'uuid', | ||
], | ||
'properties.group' => [ | ||
'required', | ||
Rule::enum(TimeEntryAggregationType::class), | ||
], | ||
|
||
'properties.sub_group' => [ | ||
'required', | ||
Rule::enum(TimeEntryAggregationType::class), | ||
], | ||
]; | ||
} | ||
|
||
public function getName(): string | ||
{ | ||
return (string) $this->input('name'); | ||
} | ||
|
||
public function getDescription(): ?string | ||
{ | ||
return $this->input('description'); | ||
} | ||
|
||
public function getIsPublic(): bool | ||
{ | ||
return (bool) $this->input('is_public'); | ||
} | ||
|
||
public function getPublicUntil(): ?Carbon | ||
{ | ||
$publicUntil = $this->input('public_until'); | ||
|
||
return $publicUntil === null ? null : Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $publicUntil); | ||
} | ||
} |
Oops, something went wrong.