mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
Added time entry api endpoints; Increased phpstan level; renamed to solidtime
This commit is contained in:
@@ -11,11 +11,22 @@ use App\Http\Resources\V1\Project\ProjectResource;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
protected function checkPermission(Organization $organization, string $permission, ?Project $project = null): void
|
||||
{
|
||||
parent::checkPermission($organization, $permission);
|
||||
if ($project !== null && $project->organization_id !== $organization->id) {
|
||||
throw new AuthorizationException('Project does not belong to organization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get projects
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function index(Organization $organization): JsonResource
|
||||
@@ -29,17 +40,22 @@ class ProjectController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function show(Organization $organization, Project $project): JsonResource
|
||||
{
|
||||
$this->checkPermission($organization, 'projects:view');
|
||||
$this->checkPermission($organization, 'projects:view', $project);
|
||||
|
||||
$project->load('organization');
|
||||
|
||||
return new ProjectResource($project);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create project
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function store(Organization $organization, ProjectStoreRequest $request): JsonResource
|
||||
@@ -55,11 +71,13 @@ class ProjectController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Update project
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function update(Organization $organization, Project $project, ProjectUpdateRequest $request): JsonResource
|
||||
{
|
||||
$this->checkPermission($organization, 'projects:update');
|
||||
$this->checkPermission($organization, 'projects:update', $project);
|
||||
$project->name = $request->input('name');
|
||||
$project->color = $request->input('color');
|
||||
$project->save();
|
||||
@@ -68,13 +86,17 @@ class ProjectController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete project
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function destroy(Organization $organization, Project $project): JsonResource
|
||||
public function destroy(Organization $organization, Project $project): JsonResponse
|
||||
{
|
||||
$this->checkPermission($organization, 'projects:delete');
|
||||
$this->checkPermission($organization, 'projects:delete', $project);
|
||||
|
||||
$project->delete();
|
||||
|
||||
return new ProjectResource($project);
|
||||
return response()
|
||||
->json(null, 204);
|
||||
}
|
||||
}
|
||||
|
||||
126
app/Http/Controllers/Api/V1/TimeEntryController.php
Normal file
126
app/Http/Controllers/Api/V1/TimeEntryController.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Exceptions\TimeEntryStillRunning;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateRequest;
|
||||
use App\Http\Resources\V1\TimeEntry\TimeEntryCollection;
|
||||
use App\Http\Resources\V1\TimeEntry\TimeEntryResource;
|
||||
use App\Models\Organization;
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TimeEntryController extends Controller
|
||||
{
|
||||
protected function checkPermission(Organization $organization, string $permission, ?TimeEntry $timeEntry = null): void
|
||||
{
|
||||
parent::checkPermission($organization, $permission);
|
||||
if ($timeEntry !== null && $timeEntry->organization_id !== $organization->getKey()) {
|
||||
throw new AuthorizationException('Time entry does not belong to organization');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time entries
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
|
||||
{
|
||||
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
}
|
||||
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization');
|
||||
|
||||
if ($request->has('before')) {
|
||||
|
||||
}
|
||||
|
||||
if ($request->has('after')) {
|
||||
|
||||
}
|
||||
|
||||
if ($request->has('user_id')) {
|
||||
$timeEntriesQuery->where('user_id', $request->input('user_id'));
|
||||
}
|
||||
|
||||
$timeEntries = $timeEntriesQuery->get();
|
||||
|
||||
return new TimeEntryCollection($timeEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create time entry
|
||||
*
|
||||
* @throws AuthorizationException|TimeEntryStillRunning
|
||||
*/
|
||||
public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource
|
||||
{
|
||||
if ($request->get('user_id') === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:create:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:create:all');
|
||||
}
|
||||
|
||||
if ($request->get('end') === null && TimeEntry::where('user_id', $request->get('user_id'))->where('end', null)->exists()) {
|
||||
// TODO: documentation
|
||||
throw new TimeEntryStillRunning('User already has an active time entry');
|
||||
}
|
||||
|
||||
$timeEntry = new TimeEntry();
|
||||
$timeEntry->fill($request->validated());
|
||||
$timeEntry->organization()->associate($organization);
|
||||
$timeEntry->save();
|
||||
|
||||
return new TimeEntryResource($timeEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update time entry
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function update(Organization $organization, TimeEntry $timeEntry, TimeEntryUpdateRequest $request): JsonResource
|
||||
{
|
||||
if ($timeEntry->user_id === Auth::id() && $request->get('user_id') === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:update:own', $timeEntry);
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
|
||||
}
|
||||
|
||||
$timeEntry->fill($request->validated());
|
||||
$timeEntry->save();
|
||||
|
||||
return new TimeEntryResource($timeEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete time entry
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
|
||||
{
|
||||
if ($timeEntry->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);
|
||||
}
|
||||
|
||||
$timeEntry->delete();
|
||||
|
||||
return response()
|
||||
->json(null, 204);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ class HandleInertiaRequests extends Middleware
|
||||
* Defines the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
|
||||
@@ -26,5 +26,6 @@ class TrustProxies extends Middleware
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB |
|
||||
Request::HEADER_X_FORWARDED_TRAEFIK;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class ProjectStoreRequest extends FormRequest
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ class ProjectUpdateRequest extends FormRequest
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
51
app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php
Normal file
51
app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
|
||||
/**
|
||||
* @property Organization $organization
|
||||
*/
|
||||
class TimeEntryIndexRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(User::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<User> $builder */
|
||||
return $builder->whereHas('organizations', function (Builder $builder) {
|
||||
/** @var Builder<Organization> $builder */
|
||||
return $builder->whereKey($this->organization->getKey());
|
||||
});
|
||||
}),
|
||||
],
|
||||
'before' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'date_format:Y-m-d',
|
||||
'before:after',
|
||||
],
|
||||
'after' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'date_format:Y-m-d',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
83
app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php
Normal file
83
app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
|
||||
/**
|
||||
* @property Organization $organization Organization from model binding
|
||||
*/
|
||||
class TimeEntryStoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => [
|
||||
'required',
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(User::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<User> $builder */
|
||||
return $builder->whereHas('organizations', function (Builder $builder) {
|
||||
/** @var Builder<Organization> $builder */
|
||||
return $builder->whereKey($this->organization->getKey());
|
||||
});
|
||||
}),
|
||||
],
|
||||
'task_id' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Task> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Start of time entry (ISO 8601 format, UTC timezone)
|
||||
'start' => [
|
||||
'required',
|
||||
'date', // TODO
|
||||
],
|
||||
// End of time entry (ISO 8601 format, UTC timezone)
|
||||
'end' => [
|
||||
'required',
|
||||
'nullable',
|
||||
'date', // TODO
|
||||
'after:start',
|
||||
],
|
||||
// Description of time entry
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
// List of tag IDs
|
||||
'tags' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'tags.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Tag> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
70
app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php
Normal file
70
app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Models\Organization;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
|
||||
/**
|
||||
* @property Organization $organization Organization from model binding
|
||||
*/
|
||||
class TimeEntryUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'task_id' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Task> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Start of time entry (ISO 8601 format, UTC timezone)
|
||||
'start' => [
|
||||
'required',
|
||||
'date', // TODO
|
||||
],
|
||||
// End of time entry (ISO 8601 format, UTC timezone)
|
||||
'end' => [
|
||||
'required',
|
||||
'nullable',
|
||||
'date', // TODO
|
||||
'after:start',
|
||||
],
|
||||
// Description of time entry
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
// List of tag IDs
|
||||
'tags' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'tags.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Tag> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
17
app/Http/Resources/V1/TimeEntry/TimeEntryCollection.php
Normal file
17
app/Http/Resources/V1/TimeEntry/TimeEntryCollection.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\TimeEntry;
|
||||
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class TimeEntryCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* The resource that this resource collects.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $collects = TimeEntryResource::class;
|
||||
}
|
||||
28
app/Http/Resources/V1/TimeEntry/TimeEntryResource.php
Normal file
28
app/Http/Resources/V1/TimeEntry/TimeEntryResource.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Resources\V1\TimeEntry;
|
||||
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @property TimeEntry $resource
|
||||
*/
|
||||
class TimeEntryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, string|boolean|integer>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
/** @var string $id ID of time entry */
|
||||
'id' => $this->resource->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user