Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add roles for project members; Restrict access to sensitive project v… #195

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe "member" instead of "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,
];
}
}
2 changes: 2 additions & 0 deletions app/Http/Resources/V1/ProjectMember/ProjectMemberResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public function toArray(Request $request): array
'member_id' => $this->resource->member_id,
/** @var string $project_id ID of the project */
'project_id' => $this->resource->project_id,
/** @var string $role Role of the project member */
'role' => $this->resource->role->value,
];
}
}
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
Loading