Skip to content

Commit ae5ce9e

Browse files
authored
Merge pull request #95 from codebar-ag/main
main/production
2 parents 83ca106 + b8be38b commit ae5ce9e

File tree

89 files changed

+5735
-3077
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+5735
-3077
lines changed

app/Console/Kernel.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,27 @@ protected function schedule(Schedule $schedule): void
2222
->when(fn (): bool => config('scheduling.tasks.auth_send_mails_expiring_api_tokens'))
2323
->everyTenMinutes();
2424

25-
$schedule->command('self-host:check-for-update')
26-
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
27-
->twiceDaily();
28-
29-
$schedule->command('self-host:telemetry')
30-
->when(fn (): bool => config('scheduling.tasks.self_hosting_telemetry'))
31-
->twiceDaily();
25+
if (config('app.key') && (config('scheduling.tasks.self_hosting_check_for_update') || config('scheduling.tasks.self_hosting_telemetry'))) {
26+
// Convert string to a stable integer for seeding
27+
/** @var int $seed Take the first 8 hex chars → 32-bit int */
28+
$seed = hexdec(substr(hash('md5', config('app.key')), 0, 8));
29+
$seed = abs($seed); // Ensure it's positive
30+
mt_srand($seed);
31+
$firstHour = mt_rand(0, 23);
32+
$secondHour = ($firstHour + 12) % 24;
33+
$minuteOffset = mt_rand(0, 59);
34+
mt_srand(null); // Reset the random number generator
35+
36+
if (config('scheduling.tasks.self_hosting_check_for_update')) {
37+
$schedule->command('self-host:check-for-update')
38+
->twiceDailyAt($firstHour, $secondHour, $minuteOffset);
39+
}
40+
41+
if (config('scheduling.tasks.self_hosting_telemetry')) {
42+
$schedule->command('self-host:telemetry')
43+
->twiceDailyAt($firstHour, $secondHour, $minuteOffset);
44+
}
45+
}
3246

3347
$schedule->command('self-host:database-consistency')
3448
->when(fn (): bool => config('scheduling.tasks.self_hosting_database_consistency'))

app/Http/Controllers/Api/V1/ClientController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,17 @@ protected function checkPermission(Organization $organization, string $permissio
3838
public function index(Organization $organization, ClientIndexRequest $request): ClientCollection
3939
{
4040
$this->checkPermission($organization, 'clients:view');
41+
$canViewAllClients = $this->hasPermission($organization, 'clients:view:all');
42+
$user = $this->user();
4143

4244
$clientsQuery = Client::query()
4345
->whereBelongsTo($organization, 'organization')
4446
->orderBy('name');
4547

48+
if (! $canViewAllClients) {
49+
$clientsQuery->visibleByEmployee($user);
50+
}
51+
4652
$filterArchived = $request->getFilterArchived();
4753
if ($filterArchived === 'true') {
4854
$clientsQuery->whereNotNull('archived_at');

app/Http/Controllers/Api/V1/OrganizationController.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public function update(Organization $organization, OrganizationUpdateRequest $re
4646
if ($request->getEmployeesCanSeeBillableRates() !== null) {
4747
$organization->employees_can_see_billable_rates = $request->getEmployeesCanSeeBillableRates();
4848
}
49+
if ($request->getEmployeesCanManageTasks() !== null) {
50+
$organization->employees_can_manage_tasks = $request->getEmployeesCanManageTasks();
51+
}
4952
if ($request->getNumberFormat() !== null) {
5053
$organization->number_format = $request->getNumberFormat();
5154
}

app/Http/Controllers/Api/V1/TaskController.php

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use App\Http\Resources\V1\Task\TaskCollection;
1212
use App\Http\Resources\V1\Task\TaskResource;
1313
use App\Models\Organization;
14+
use App\Models\Project;
1415
use App\Models\Task;
1516
use Illuminate\Auth\Access\AuthorizationException;
1617
use Illuminate\Http\JsonResponse;
@@ -27,6 +28,26 @@ protected function checkPermission(Organization $organization, string $permissio
2728
}
2829
}
2930

31+
/**
32+
* Check scoped permission and verify user has access to the project
33+
*
34+
* @throws AuthorizationException
35+
*/
36+
private function checkScopedPermissionForProject(Organization $organization, Project $project, string $permission): void
37+
{
38+
$this->checkPermission($organization, $permission);
39+
40+
$user = $this->user();
41+
$hasAccess = Project::query()
42+
->where('id', $project->id)
43+
->visibleByEmployee($user)
44+
->exists();
45+
46+
if (! $hasAccess) {
47+
throw new AuthorizationException('You do not have permission to '.$permission.' in this project.');
48+
}
49+
}
50+
3051
/**
3152
* Get tasks
3253
*
@@ -75,7 +96,15 @@ public function index(Organization $organization, TaskIndexRequest $request): Ta
7596
*/
7697
public function store(Organization $organization, TaskStoreRequest $request): JsonResource
7798
{
78-
$this->checkPermission($organization, 'tasks:create');
99+
/** @var Project $project */
100+
$project = Project::query()->findOrFail($request->input('project_id'));
101+
102+
if ($this->hasPermission($organization, 'tasks:create:all')) {
103+
$this->checkPermission($organization, 'tasks:create:all');
104+
} else {
105+
$this->checkScopedPermissionForProject($organization, $project, 'tasks:create');
106+
}
107+
79108
$task = new Task;
80109
$task->name = $request->input('name');
81110
$task->project_id = $request->input('project_id');
@@ -97,7 +126,17 @@ public function store(Organization $organization, TaskStoreRequest $request): Js
97126
*/
98127
public function update(Organization $organization, Task $task, TaskUpdateRequest $request): JsonResource
99128
{
100-
$this->checkPermission($organization, 'tasks:update', $task);
129+
// Check task belongs to organization
130+
if ($task->organization_id !== $organization->id) {
131+
throw new AuthorizationException('Task does not belong to organization');
132+
}
133+
134+
if ($this->hasPermission($organization, 'tasks:update:all')) {
135+
$this->checkPermission($organization, 'tasks:update:all');
136+
} else {
137+
$this->checkScopedPermissionForProject($organization, $task->project, 'tasks:update');
138+
}
139+
101140
$task->name = $request->input('name');
102141
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
103142
$task->estimated_time = $request->getEstimatedTime();
@@ -119,7 +158,16 @@ public function update(Organization $organization, Task $task, TaskUpdateRequest
119158
*/
120159
public function destroy(Organization $organization, Task $task): JsonResponse
121160
{
122-
$this->checkPermission($organization, 'tasks:delete', $task);
161+
// Check task belongs to organization
162+
if ($task->organization_id !== $organization->id) {
163+
throw new AuthorizationException('Task does not belong to organization');
164+
}
165+
166+
if ($this->hasPermission($organization, 'tasks:delete:all')) {
167+
$this->checkPermission($organization, 'tasks:delete:all');
168+
} else {
169+
$this->checkScopedPermissionForProject($organization, $task->project, 'tasks:delete');
170+
}
123171

124172
if ($task->timeEntries()->exists()) {
125173
throw new EntityStillInUseApiException('task', 'time_entry');

app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public function rules(): array
3939
'employees_can_see_billable_rates' => [
4040
'boolean',
4141
],
42+
'employees_can_manage_tasks' => [
43+
'boolean',
44+
],
4245
'prevent_overlapping_time_entries' => [
4346
'boolean',
4447
],
@@ -102,6 +105,11 @@ public function getEmployeesCanSeeBillableRates(): ?bool
102105
return $this->has('employees_can_see_billable_rates') ? $this->boolean('employees_can_see_billable_rates') : null;
103106
}
104107

108+
public function getEmployeesCanManageTasks(): ?bool
109+
{
110+
return $this->has('employees_can_manage_tasks') ? $this->boolean('employees_can_manage_tasks') : null;
111+
}
112+
105113
public function getPreventOverlappingTimeEntries(): ?bool
106114
{
107115
return $this->has('prevent_overlapping_time_entries') ? $this->boolean('prevent_overlapping_time_entries') : null;

app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function rules(): array
7979
'description' => [
8080
'nullable',
8181
'string',
82-
'max:500',
82+
'max:5000',
8383
],
8484
// List of tag IDs
8585
'tags' => [

app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function rules(): array
7979
'changes.description' => [
8080
'nullable',
8181
'string',
82-
'max:500',
82+
'max:5000',
8383
],
8484
// List of tag IDs
8585
'changes.tags' => [

app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function rules(): array
7777
'description' => [
7878
'nullable',
7979
'string',
80-
'max:500',
80+
'max:5000',
8181
],
8282
// List of tag IDs
8383
'tags' => [

app/Http/Resources/V1/Organization/OrganizationResource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public function toArray(Request $request): array
5353
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
5454
/** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */
5555
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
56+
/** @var bool $employees_can_manage_tasks Can members of the organization with role "employee" manage tasks in public projects and projects they are assigned to */
57+
'employees_can_manage_tasks' => $this->resource->employees_can_manage_tasks,
5658
/** @var bool $prevent_overlapping_time_entries Prevent creating overlapping time entries (only new entries) */
5759
'prevent_overlapping_time_entries' => $this->resource->prevent_overlapping_time_entries,
5860
/** @var string $currency Currency code (ISO 4217) */

app/Models/Client.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Models\Concerns\CustomAuditable;
88
use App\Models\Concerns\HasUuids;
99
use Database\Factories\ClientFactory;
10+
use Illuminate\Database\Eloquent\Builder;
1011
use Illuminate\Database\Eloquent\Casts\Attribute;
1112
use Illuminate\Database\Eloquent\Factories\HasFactory;
1213
use Illuminate\Database\Eloquent\Model;
@@ -62,6 +63,18 @@ public function projects(): HasMany
6263
return $this->hasMany(Project::class, 'client_id');
6364
}
6465

66+
/**
67+
* @param Builder<Client> $builder
68+
* @return Builder<Client>
69+
*/
70+
public function scopeVisibleByEmployee(Builder $builder, User $user): Builder
71+
{
72+
return $builder->whereHas('projects', function (Builder $builder) use ($user): Builder {
73+
/** @var Builder<Project> $builder */
74+
return $builder->visibleByEmployee($user);
75+
});
76+
}
77+
6578
/**
6679
* @return Attribute<bool, never>
6780
*/

0 commit comments

Comments
 (0)