Add roles for project members; Restrict access to sensitive project values

This commit is contained in:
Constantin Graf
2024-09-30 19:03:14 +02:00
parent 32c7e55a15
commit a6e5d375a1
13 changed files with 334 additions and 15 deletions

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\ProjectMemberRole;
use App\Exceptions\Api\EntityStillInUseApiException;
use App\Http\Requests\V1\Project\ProjectIndexRequest;
use App\Http\Requests\V1\Project\ProjectStoreRequest;
@@ -15,6 +16,8 @@ use App\Models\Project;
use App\Models\ProjectMember;
use App\Service\BillableRateService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Carbon;
@@ -50,6 +53,12 @@ class ProjectController extends Controller
if (! $canViewAllProjects) {
$projectsQuery->visibleByEmployee($user);
$projectsQuery->with([
'members' => function (HasMany $query): void {
/** @var Builder<ProjectMember> $query */
$query->whereBelongsTo($this->user(), 'user');
},
]);
}
$filterArchived = $request->getFilterArchived();
if ($filterArchived === 'true') {
@@ -60,6 +69,14 @@ class ProjectController extends Controller
$projects = $projectsQuery->paginate(config('app.pagination_per_page_default'));
foreach ($projects->items() as $project) {
if ($canViewAllProjects) {
$project->setAttribute('limited_visibility', false);
} else {
$project->setAttribute('limited_visibility', $project->members->firstWhere('user_id', $this->user()->id)?->role !== ProjectMemberRole::Manager);
}
}
return new ProjectCollection($projects);
}
@@ -73,6 +90,26 @@ class ProjectController extends Controller
public function show(Organization $organization, Project $project): JsonResource
{
$this->checkPermission($organization, 'projects:view', $project);
$canViewAllProjects = $this->hasPermission($organization, 'projects:view:all');
$project->load([
'members' => function (HasMany $query): void {
/** @var Builder<ProjectMember> $query */
$query->whereBelongsTo($this->user(), 'user');
},
]);
if (! $canViewAllProjects) {
if (! $project->is_public && $project->members->firstWhere('user_id', '=', $this->user()->id) === null) {
throw new AuthorizationException('No access to project');
}
}
if ($canViewAllProjects) {
$project->setAttribute('limited_visibility', false);
} else {
$project->setAttribute('limited_visibility', $project->members->firstWhere('user_id', $this->user()->id)?->role !== ProjectMemberRole::Manager);
}
$project->load('organization');
@@ -101,6 +138,8 @@ class ProjectController extends Controller
$project->organization()->associate($organization);
$project->save();
$project->setAttribute('limited_visibility', false);
return new ProjectResource($project);
}
@@ -132,6 +171,8 @@ class ProjectController extends Controller
$billableRateService->updateTimeEntriesBillableRateForProject($project);
}
$project->setAttribute('limited_visibility', false);
return new ProjectResource($project);
}

View File

@@ -72,6 +72,7 @@ class ProjectMemberController extends Controller
}
$projectMember = new ProjectMember;
$projectMember->role = $request->getRole();
$projectMember->billable_rate = $request->getBillableRate();
$projectMember->member()->associate($member);
$projectMember->user()->associate($member->user);
@@ -95,11 +96,17 @@ class ProjectMemberController extends Controller
public function update(Organization $organization, ProjectMember $projectMember, ProjectMemberUpdateRequest $request, BillableRateService $billableRateService): JsonResource
{
$this->checkPermission($organization, 'project-members:update', projectMember: $projectMember);
$oldBillableRate = $projectMember->billable_rate;
$projectMember->billable_rate = $request->getBillableRate();
$hasBillableRate = $request->has('billable_rate');
if ($hasBillableRate) {
$oldBillableRate = $projectMember->billable_rate;
$projectMember->billable_rate = $request->getBillableRate();
}
if ($request->getRole() !== null) {
$projectMember->role = $request->getRole();
}
$projectMember->save();
if ($oldBillableRate !== $request->getBillableRate()) {
if ($hasBillableRate && $oldBillableRate !== $request->getBillableRate()) {
$billableRateService->updateTimeEntriesBillableRateForProjectMember($projectMember);
}

View File

@@ -4,11 +4,13 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\ProjectMember;
use App\Enums\ProjectMemberRole;
use App\Models\Member;
use App\Models\Organization;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
/**
@@ -19,7 +21,7 @@ class ProjectMemberStoreRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule>>
*/
public function rules(): array
{
@@ -37,6 +39,11 @@ class ProjectMemberStoreRequest extends FormRequest
'integer',
'min:0',
],
'role' => [
'required',
'string',
Rule::enum(ProjectMemberRole::class),
],
];
}
@@ -46,4 +53,9 @@ class ProjectMemberStoreRequest extends FormRequest
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
}
public function getRole(): ProjectMemberRole
{
return ProjectMemberRole::from($this->validated('role'));
}
}

View File

@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Http\Requests\V1\ProjectMember;
use App\Enums\ProjectMemberRole;
use App\Models\Organization;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* @property Organization $organization Organization from model binding
@@ -16,7 +18,7 @@ class ProjectMemberUpdateRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, array<string|ValidationRule>>
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule>>
*/
public function rules(): array
{
@@ -26,6 +28,10 @@ class ProjectMemberUpdateRequest extends FormRequest
'integer',
'min:0',
],
'role' => [
'string',
Rule::enum(ProjectMemberRole::class),
],
];
}
@@ -33,6 +39,11 @@ class ProjectMemberUpdateRequest extends FormRequest
{
$input = $this->input('billable_rate');
return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
return $input !== null && ((int) $input) !== 0 ? (int) $this->validated('billable_rate') : null;
}
public function getRole(): ?ProjectMemberRole
{
return $this->has('role') ? ProjectMemberRole::from($this->validated('role')) : null;
}
}

View File

@@ -20,6 +20,8 @@ class ProjectResource extends BaseResource
*/
public function toArray(Request $request): array
{
$limitedVisibility = is_bool($this->resource->getAttributeValue('limited_visibility')) ? $this->resource->getAttributeValue('limited_visibility') : true;
return [
/** @var string $id ID of project */
'id' => $this->resource->id,
@@ -32,13 +34,15 @@ class ProjectResource extends BaseResource
/** @var bool $is_archived Whether the client is archived */
'is_archived' => $this->resource->is_archived,
/** @var int|null $billable_rate Billable rate in cents per hour */
'billable_rate' => $this->resource->billable_rate,
'billable_rate' => $limitedVisibility ? null : $this->resource->billable_rate,
/** @var bool $is_billable Project time entries billable default */
'is_billable' => $this->resource->is_billable,
/** @var int|null $estimated_time Estimated time in seconds */
'estimated_time' => $this->resource->estimated_time,
'estimated_time' => $limitedVisibility ? null : $this->resource->estimated_time,
/** @var int $spent_time Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
'spent_time' => $this->resource->spent_time,
'spent_time' => $limitedVisibility ? null : $this->resource->spent_time,
/** @var bool $limited_visibility */
'limited_visibility' => $limitedVisibility,
];
}
}