Migrated endpoints from user to member; Renamed membership to member

This commit is contained in:
Constantin Graf
2024-05-13 19:50:50 +02:00
parent ba335b4f05
commit b2365e2778
64 changed files with 853 additions and 456 deletions

View File

@@ -5,8 +5,11 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Models\Organization;
use App\Models\User;
use App\Service\PermissionStore;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class Controller extends \App\Http\Controllers\Controller
{
@@ -29,4 +32,19 @@ class Controller extends \App\Http\Controllers\Controller
{
return $this->permissionStore->has($organization, $permission);
}
/**
* @throws AuthorizationException
*/
protected function user(): User
{
/** @var User|null $user */
$user = Auth::user();
if ($user === null) {
Log::error('This function should only be called in authenticated context');
throw new AuthorizationException();
}
return $user;
}
}

View File

@@ -57,7 +57,7 @@ class InvitationController extends Controller
$this->checkPermission($organization, 'invitations:create');
app(InvitesTeamMembers::class)->invite(
$request->user(),
$this->user(),
$organization,
$request->input('email'),
$request->input('role')

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Enums\Role;
use App\Exceptions\Api\EntityStillInUseApiException;
use App\Exceptions\Api\UserNotPlaceholderApiException;
use App\Http\Requests\V1\Member\MemberIndexRequest;
@@ -11,7 +12,7 @@ use App\Http\Requests\V1\Member\MemberUpdateRequest;
use App\Http\Resources\V1\Member\MemberCollection;
use App\Http\Resources\V1\Member\MemberPivotResource;
use App\Http\Resources\V1\Member\MemberResource;
use App\Models\Membership;
use App\Models\Member;
use App\Models\Organization;
use App\Models\ProjectMember;
use App\Models\TimeEntry;
@@ -23,10 +24,10 @@ use Laravel\Jetstream\Contracts\InvitesTeamMembers;
class MemberController extends Controller
{
protected function checkPermission(Organization $organization, string $permission, ?Membership $membership = null): void
protected function checkPermission(Organization $organization, string $permission, ?Member $member = null): void
{
parent::checkPermission($organization, $permission);
if ($membership !== null && $membership->organization_id !== $organization->id) {
if ($member !== null && $member->organization_id !== $organization->id) {
throw new AuthorizationException('Member does not belong to organization');
}
}
@@ -57,15 +58,15 @@ class MemberController extends Controller
*
* @operationId updateMember
*/
public function update(Organization $organization, Membership $membership, MemberUpdateRequest $request): JsonResource
public function update(Organization $organization, Member $member, MemberUpdateRequest $request): JsonResource
{
$this->checkPermission($organization, 'members:update', $membership);
$this->checkPermission($organization, 'members:update', $member);
$membership->billable_rate = $request->input('billable_rate');
$membership->role = $request->input('role');
$membership->save();
$member->billable_rate = $request->input('billable_rate');
$member->role = $request->input('role');
$member->save();
return new MemberResource($membership);
return new MemberResource($member);
}
/**
@@ -75,18 +76,18 @@ class MemberController extends Controller
*
* @operationId removeMember
*/
public function destroy(Organization $organization, Membership $membership): JsonResponse
public function destroy(Organization $organization, Member $member): JsonResponse
{
$this->checkPermission($organization, 'members:delete', $membership);
$this->checkPermission($organization, 'members:delete', $member);
if (TimeEntry::query()->where('user_id', $membership->user_id)->whereBelongsTo($organization, 'organization')->exists()) {
if (TimeEntry::query()->where('user_id', $member->user_id)->whereBelongsTo($organization, 'organization')->exists()) {
throw new EntityStillInUseApiException('member', 'time_entry');
}
if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $membership->user_id)->exists()) {
if (ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $member->user_id)->exists()) {
throw new EntityStillInUseApiException('member', 'project_member');
}
$membership->delete();
$member->delete();
return response()
->json(null, 204);
@@ -99,20 +100,20 @@ class MemberController extends Controller
*
* @operationId invitePlaceholder
*/
public function invitePlaceholder(Organization $organization, Membership $membership, Request $request): JsonResponse
public function invitePlaceholder(Organization $organization, Member $member, Request $request): JsonResponse
{
$this->checkPermission($organization, 'members:invite-placeholder', $membership);
$user = $membership->user;
$this->checkPermission($organization, 'members:invite-placeholder', $member);
$user = $member->user;
if (! $user->is_placeholder) {
throw new UserNotPlaceholderApiException();
}
app(InvitesTeamMembers::class)->invite(
$request->user(),
$this->user(),
$organization,
$user->email,
'employee'
Role::Employee->value,
);
return response()->json(null, 204);

View File

@@ -17,7 +17,6 @@ use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class ProjectController extends Controller
@@ -43,8 +42,7 @@ class ProjectController extends Controller
{
$this->checkPermission($organization, 'projects:view');
$canViewAllProjects = $this->hasPermission($organization, 'projects:view:all');
/** @var User $user */
$user = Auth::user();
$user = $this->user();
$projectsQuery = Project::query()
->whereBelongsTo($organization, 'organization');

View File

@@ -10,10 +10,10 @@ use App\Http\Requests\V1\ProjectMember\ProjectMemberStoreRequest;
use App\Http\Requests\V1\ProjectMember\ProjectMemberUpdateRequest;
use App\Http\Resources\V1\ProjectMember\ProjectMemberCollection;
use App\Http\Resources\V1\ProjectMember\ProjectMemberResource;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -62,17 +62,18 @@ class ProjectMemberController extends Controller
{
$this->checkPermission($organization, 'project-members:create', $project);
$user = User::findOrFail((string) $request->input('user_id'));
if ($user->is_placeholder) {
$member = Member::findOrFail((string) $request->input('member_id'));
if ($member->user->is_placeholder) {
throw new InactiveUserCanNotBeUsedApiException();
}
if (ProjectMember::whereBelongsTo($project, 'project')->whereBelongsTo($user, 'user')->exists()) {
if (ProjectMember::whereBelongsTo($project, 'project')->whereBelongsTo($member, 'member')->exists()) {
throw new UserIsAlreadyMemberOfProjectApiException();
}
$projectMember = new ProjectMember();
$projectMember->billable_rate = $request->input('billable_rate');
$projectMember->user()->associate($user);
$projectMember->member()->associate($member);
$projectMember->user()->associate($member->user);
$projectMember->project()->associate($project);
$projectMember->save();

View File

@@ -12,11 +12,9 @@ use App\Http\Resources\V1\Task\TaskCollection;
use App\Http\Resources\V1\Task\TaskResource;
use App\Models\Organization;
use App\Models\Task;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
class TaskController extends Controller
{
@@ -41,8 +39,7 @@ class TaskController extends Controller
{
$this->checkPermission($organization, 'tasks:view');
$canViewAllTasks = $this->hasPermission($organization, 'tasks:view:all');
/** @var User $user */
$user = Auth::user();
$user = $this->user();
$projectId = $request->input('project_id');

View File

@@ -13,6 +13,7 @@ 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\Member;
use App\Models\Organization;
use App\Models\TimeEntry;
use App\Service\TimeEntryFilter;
@@ -22,9 +23,9 @@ use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Ramsey\Uuid\Type\Time;
class TimeEntryController extends Controller
{
@@ -48,7 +49,9 @@ class TimeEntryController extends Controller
*/
public function index(Organization $organization, TimeEntryIndexRequest $request): JsonResource
{
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) {
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:view:own');
} else {
$this->checkPermission($organization, 'time-entries:view:all');
@@ -62,7 +65,8 @@ class TimeEntryController extends Controller
$filter->addBeforeFilter($request->input('before'));
$filter->addAfterFilter($request->input('after'));
$filter->addActiveFilter($request->input('active'));
$filter->addUserIdFilter($request->input('user_id'));
$filter->addMemberIdFilter($member);
$filter->addMemberIdsFilter($request->input('member_ids'));
$filter->addProjectIdsFilter($request->input('project_ids'));
$filter->addTagIdsFilter($request->input('tag_ids'));
$filter->addTaskIdsFilter($request->input('task_ids'));
@@ -78,7 +82,7 @@ class TimeEntryController extends Controller
$timeEntries = $timeEntriesQuery->get();
if ($timeEntries->count() === $limit && $request->has('only_full_dates') && (bool) $request->get('only_full_dates') === true) {
$user = Auth::user();
$user = $this->user();
$timezone = app(TimezoneService::class)->getTimezoneFromUser($user);
$lastDate = null;
/** @var TimeEntry $timeEntry */
@@ -113,11 +117,38 @@ class TimeEntryController extends Controller
/**
* Get aggregated time entries in organization
*
* This endpoint allows you to filter time entries and aggregate them by different criteria.
* The parameters `group` and `sub_group` allow you to group the time entries by different criteria.
* If the group parameters are all set to `null` or are all missing, the endpoint will aggregate all filtered time entries.
*
* @operationId getAggregatedTimeEntries
*
* @return array{
* data: array{
* grouped_data: null|array<array{
* type: string,
* value: string|null,
* aggregate: int,
* cost: int,
* grouped_data: null|array<array{
* type: string,
* value: string|null,
* aggregate: int,
* cost: int
* }>
* }>,
* aggregate: int,
* cost: int
* }
* }
*
* @throws AuthorizationException
*/
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request): array
{
if ($request->has('user_id') && $request->get('user_id') === Auth::id()) {
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
if ($member !== null && $member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:view:own');
} else {
$this->checkPermission($organization, 'time-entries:view:all');
@@ -130,7 +161,8 @@ class TimeEntryController extends Controller
$filter->addBeforeFilter($request->input('before'));
$filter->addAfterFilter($request->input('after'));
$filter->addActiveFilter($request->input('active'));
$filter->addUserIdFilter($request->input('user_id'));
$filter->addMemberIdFilter($member);
$filter->addMemberIdsFilter($request->input('member_ids'));
$filter->addProjectIdsFilter($request->input('project_ids'));
$filter->addTagIdsFilter($request->input('tag_ids'));
$filter->addTaskIdsFilter($request->input('task_ids'));
@@ -138,10 +170,12 @@ class TimeEntryController extends Controller
$filter->addBillableFilter($request->input('billable'));
$timeEntriesQuery = $filter->get();
$user = Auth::user();
$user = $this->user();
$group1Type = $request->get('group_1');
$group2Type = $request->get('group_2');
/** @var string|null $group1Type */
$group1Type = $request->get('group');
/** @var string|null $group2Type */
$group2Type = $request->get('sub_group');
$group1Select = null;
$group2Select = null;
@@ -178,11 +212,15 @@ class TimeEntryController extends Controller
$group1ResponseSum = 0;
$group1ResponseCost = 0;
foreach ($groupedAggregates as $group1 => $group1Aggregates) {
/** @var string $group1 */
$group2Response = [];
if ($group2Select !== null) {
$group2ResponseSum = 0;
$group2ResponseCost = 0;
foreach ($group1Aggregates as $group2 => $aggregate) {
/** @var string $group2 */
/** @var Collection<int, object{aggregate: int, cost: int}> $aggregate */
/** @var string $group2Type */
$group2Response[] = [
'type' => $group2Type,
'value' => $group2 === '' ? null : $group2,
@@ -193,15 +231,18 @@ class TimeEntryController extends Controller
$group2ResponseCost += (int) $aggregate->get(0)->cost;
}
} else {
/** @var Collection<int, object{aggregate: int, cost: int}> $group1Aggregates */
$group2ResponseSum = (int) $group1Aggregates->get(0)->aggregate;
$group2ResponseCost = (int) $group1Aggregates->get(0)->cost;
$group2Response = null;
}
/** @var string $group1Type */
$group1Response[] = [
'type' => $group1Type,
'value' => $group1 === '' ? null : $group1,
'aggregate' => $group2ResponseSum,
'cost' => $group2ResponseCost,
'grouped_data' => $group2Response,
];
$group1ResponseSum += $group2ResponseSum;
@@ -209,6 +250,7 @@ class TimeEntryController extends Controller
}
} else {
$group1Response = null;
/** @var Collection<int, object{aggregate: int, cost: int}> $timeEntriesAggregates */
$group1ResponseSum = (int) $timeEntriesAggregates->get(0)->aggregate;
$group1ResponseCost = (int) $timeEntriesAggregates->get(0)->cost;
}
@@ -266,18 +308,21 @@ class TimeEntryController extends Controller
*/
public function store(Organization $organization, TimeEntryStoreRequest $request): JsonResource
{
if ($request->get('user_id') === Auth::id()) {
/** @var Member $member */
$member = Member::query()->findOrFail($request->get('member_id'));
if ($member->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::query()->where('user_id', $request->get('user_id'))->where('end', null)->exists()) {
if ($request->get('end') === null && TimeEntry::query()->whereBelongsTo($member, 'member')->where('end', null)->exists()) {
throw new TimeEntryStillRunningApiException();
}
$timeEntry = new TimeEntry();
$timeEntry->fill($request->validated());
$timeEntry->user_id = $member->user_id;
$timeEntry->description = $request->get('description') ?? '';
$timeEntry->organization()->associate($organization);
$timeEntry->setComputedAttributeValue('billable_rate');
@@ -295,10 +340,12 @@ class TimeEntryController extends Controller
*/
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);
/** @var Member|null $member */
$member = $request->has('member_id') ? Member::query()->findOrFail($request->get('member_id')) : null;
if ($timeEntry->member->user_id === Auth::id() && $member?->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:update:own');
} else {
$this->checkPermission($organization, 'time-entries:update:all', $timeEntry);
$this->checkPermission($organization, 'time-entries:update:all');
}
if ($timeEntry->end !== null && $request->has('end') && $request->get('end') === null) {
@@ -321,7 +368,7 @@ class TimeEntryController extends Controller
*/
public function destroy(Organization $organization, TimeEntry $timeEntry): JsonResponse
{
if ($timeEntry->user_id === Auth::id()) {
if ($timeEntry->member->user_id === Auth::id()) {
$this->checkPermission($organization, 'time-entries:delete:own', $timeEntry);
} else {
$this->checkPermission($organization, 'time-entries:delete:all', $timeEntry);

View File

@@ -10,7 +10,6 @@ use App\Models\TimeEntry;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class UserTimeEntryController extends Controller
@@ -24,8 +23,7 @@ class UserTimeEntryController extends Controller
*/
public function myActive(): JsonResource
{
/** @var User $user */
$user = Auth::user();
$user = $this->user();
$activeTimeEntriesOfUser = TimeEntry::query()
->whereBelongsTo($user, 'user')