Skip to content

Commit

Permalink
Add roles for project members; Restrict access to sensitive project v…
Browse files Browse the repository at this point in the history
…alues
  • Loading branch information
korridor committed Sep 30, 2024
1 parent 32c7e55 commit a6e5d37
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 13 deletions.
11 changes: 11 additions & 0 deletions app/Enums/ProjectMemberRole.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum ProjectMemberRole: string
{
case Manager = 'manager';
case Normal = 'normal';
}
41 changes: 41 additions & 0 deletions app/Http/Controllers/Api/V1/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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;
Expand All @@ -15,6 +16,8 @@
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;
Expand Down Expand Up @@ -50,6 +53,12 @@ public function index(Organization $organization, ProjectIndexRequest $request):

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') {
Expand All @@ -60,6 +69,14 @@ public function index(Organization $organization, ProjectIndexRequest $request):

$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);
}

Expand All @@ -73,6 +90,26 @@ public function index(Organization $organization, ProjectIndexRequest $request):
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');

Expand Down Expand Up @@ -101,6 +138,8 @@ public function store(Organization $organization, ProjectStoreRequest $request):
$project->organization()->associate($organization);
$project->save();

$project->setAttribute('limited_visibility', false);

return new ProjectResource($project);
}

Expand Down Expand Up @@ -132,6 +171,8 @@ public function update(Organization $organization, Project $project, ProjectUpda
$billableRateService->updateTimeEntriesBillableRateForProject($project);
}

$project->setAttribute('limited_visibility', false);

return new ProjectResource($project);
}

Expand Down
13 changes: 10 additions & 3 deletions app/Http/Controllers/Api/V1/ProjectMemberController.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public function store(Organization $organization, Project $project, ProjectMembe
}

$projectMember = new ProjectMember;
$projectMember->role = $request->getRole();
$projectMember->billable_rate = $request->getBillableRate();
$projectMember->member()->associate($member);
$projectMember->user()->associate($member->user);
Expand All @@ -95,11 +96,17 @@ public function store(Organization $organization, Project $project, ProjectMembe
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);
}

Expand Down
14 changes: 13 additions & 1 deletion app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

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;

/**
Expand All @@ -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
{
Expand All @@ -37,6 +39,11 @@ public function rules(): array
'integer',
'min:0',
],
'role' => [
'required',
'string',
Rule::enum(ProjectMemberRole::class),
],
];
}

Expand All @@ -46,4 +53,9 @@ public function getBillableRate(): ?int

return $input !== null && $input !== 0 ? (int) $this->input('billable_rate') : null;
}

public function getRole(): ProjectMemberRole
{
return ProjectMemberRole::from($this->validated('role'));
}
}
15 changes: 13 additions & 2 deletions app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

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
Expand All @@ -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
{
Expand All @@ -26,13 +28,22 @@ public function rules(): array
'integer',
'min:0',
],
'role' => [
'string',
Rule::enum(ProjectMemberRole::class),
],
];
}

public function getBillableRate(): ?int
{
$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;
}
}
10 changes: 7 additions & 3 deletions app/Http/Resources/V1/Project/ProjectResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,13 +34,15 @@ public function toArray(Request $request): array
/** @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,
];
}
}
3 changes: 3 additions & 0 deletions app/Models/ProjectMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Models;

use App\Enums\ProjectMemberRole;
use App\Models\Concerns\CustomAuditable;
use App\Models\Concerns\HasUuids;
use Database\Factories\ProjectMemberFactory;
Expand All @@ -22,6 +23,7 @@
* @property string $user_id User ID (legacy)
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property ProjectMemberRole $role
* @property-read Project $project
* @property-read Member $member
* @property-read User $user
Expand All @@ -45,6 +47,7 @@ class ProjectMember extends Model implements AuditableContract
*/
protected $casts = [
'billable_rate' => 'int',
'role' => ProjectMemberRole::class,
];

/**
Expand Down
11 changes: 11 additions & 0 deletions database/factories/ProjectMemberFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Database\Factories;

use App\Enums\ProjectMemberRole;
use App\Models\Member;
use App\Models\Project;
use App\Models\ProjectMember;
Expand All @@ -24,12 +25,22 @@ public function definition(): array
{
return [
'billable_rate' => $this->faker->numberBetween(10, 10000) * 100,
'role' => ProjectMemberRole::Normal,
'project_id' => Project::factory(),
'user_id' => User::factory(),
'member_id' => Member::factory(),
];
}

public function role(ProjectMemberRole $role): self
{
return $this->state(function (array $attributes) use ($role) {
return [
'role' => $role,
];
});
}

/**
* @deprecated Use forMember instead
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('project_members', function (Blueprint $table): void {
$table->string('role')->default('normal');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('project_members', function (Blueprint $table): void {
$table->dropColumn('role');
});
}
};
Loading

0 comments on commit a6e5d37

Please sign in to comment.