Added me endpoints

This commit is contained in:
Constantin Graf
2024-07-18 10:00:20 +02:00
committed by Gregor Vostrak
parent 0e7dec2f40
commit 21207a4058
11 changed files with 261 additions and 48 deletions

View File

@@ -14,7 +14,6 @@ use App\Exceptions\Api\UserNotPlaceholderApiException;
use App\Http\Requests\V1\Member\MemberIndexRequest;
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\Member;
use App\Models\Organization;
@@ -40,7 +39,7 @@ class MemberController extends Controller
/**
* List all members of an organization
*
* @return MemberCollection<MemberPivotResource>>
* @return MemberCollection<MemberResource>
*
* @throws AuthorizationException
*
@@ -50,7 +49,9 @@ class MemberController extends Controller
{
$this->checkPermission($organization, 'members:view');
$members = $organization->users()
$members = Member::query()
->whereBelongsTo($organization, 'organization')
->with(['user'])
->paginate(config('app.pagination_per_page_default'));
return MemberCollection::make($members);

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Resources\V1\User\UserResource;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Resources\Json\JsonResource;
class UserController extends Controller
{
/**
* Get the current user
*
* This endpoint is independent of organization.
*
* @operationId getMe
*
* @throws AuthorizationException
*/
public function me(): JsonResource
{
$user = $this->user();
return new UserResource($user);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Resources\V1\Member\PersonalMemberCollection;
use App\Http\Resources\V1\Member\PersonalMemberResource;
use App\Models\Member;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Resources\Json\JsonResource;
class UserMemberController extends Controller
{
/**
* Get the memberships of the current user
*
* This endpoint is independent of organization.
*
* @operationId getMyMemberships
*
* @return PersonalMemberCollection<PersonalMemberResource>
*
* @throws AuthorizationException
*/
public function myMembers(): JsonResource
{
$user = $this->user();
$members = Member::query()
->whereBelongsTo($user, 'user')
->with(['organization'])
->get();
return new PersonalMemberCollection($members);
}
}

View File

@@ -14,5 +14,5 @@ class MemberCollection extends ResourceCollection implements PaginatedResourceCo
*
* @var string
*/
public $collects = MemberPivotResource::class;
public $collects = MemberResource::class;
}

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\Member;
use App\Http\Resources\V1\BaseResource;
use App\Models\Member;
use App\Models\User;
use Illuminate\Http\Request;
/**
* @property User $resource
*/
class MemberPivotResource extends BaseResource
{
/**
* Transform the resource into an array.
*
* @return array<string, string|bool|int|null|array<string>>
*/
public function toArray(Request $request): array
{
/** @var Member $member */
$member = $this->resource->getRelationValue('membership');
return [
/** @var string $id ID of membership */
'id' => $member->id,
/** @var string $id ID of user */
'user_id' => $this->resource->id,
/** @var string $name Name */
'name' => $this->resource->name,
/** @var string $email Email */
'email' => $this->resource->email,
/** @var string $role Role */
'role' => $member->role,
/** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */
'is_placeholder' => $this->resource->is_placeholder,
/** @var int|null $billable_rate Billable rate in cents per hour */
'billable_rate' => $member->billable_rate,
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\Member;
use App\Http\Resources\PaginatedResourceCollection;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PersonalMemberCollection extends ResourceCollection implements PaginatedResourceCollection
{
/**
* The resource that this resource collects.
*
* @var string
*/
public $collects = PersonalMemberResource::class;
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\Member;
use App\Http\Resources\V1\BaseResource;
use App\Models\Member;
use Illuminate\Http\Request;
/**
* @property Member $resource
*/
class PersonalMemberResource extends BaseResource
{
/**
* Transform the resource into an array.
*
* @return array<string, string|bool|int|null|array<string>>
*/
public function toArray(Request $request): array
{
return [
/** @var string $id ID of membership */
'id' => $this->resource->id,
'organization' => [
/** @var int $id ID of organization */
'id' => $this->resource->organization->id,
/** @var string $name Name of organization */
'name' => $this->resource->organization->name,
],
/** @var string $role Role */
'role' => $this->resource->role,
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\V1\User;
use App\Enums\Weekday;
use App\Http\Resources\V1\BaseResource;
use App\Models\User;
use Illuminate\Http\Request;
/**
* @property User $resource
*/
class UserResource extends BaseResource
{
/**
* Transform the resource into an array.
*
* @return array<string, string|bool|int|null|array<string>>
*/
public function toArray(Request $request): array
{
return [
/** @var string $id ID of user */
'id' => $this->resource->id,
/** @var string $name Name of user */
'name' => $this->resource->name,
/** @var string $email Email of user */
'email' => $this->resource->email,
/** @var string $profile_photo_url Profile photo URL */
'profile_photo_url' => $this->resource->profile_photo_url,
/** @var string $timezone Timezone (f.e. Europe/Berlin or America/New_York) */
'timezone' => $this->resource->timezone,
/** @var Weekday $week_start Starting day of the week */
'week_start' => $this->resource->week_start->value,
];
}
}

View File

@@ -12,6 +12,8 @@ use App\Http\Controllers\Api\V1\ProjectMemberController;
use App\Http\Controllers\Api\V1\TagController;
use App\Http\Controllers\Api\V1\TaskController;
use App\Http\Controllers\Api\V1\TimeEntryController;
use App\Http\Controllers\Api\V1\UserController;
use App\Http\Controllers\Api\V1\UserMemberController;
use App\Http\Controllers\Api\V1\UserTimeEntryController;
use Illuminate\Support\Facades\Route;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -45,6 +47,16 @@ Route::middleware([
Route::post('/organizations/{organization}/members/{member}/invite-placeholder', [MemberController::class, 'invitePlaceholder'])->name('invite-placeholder');
});
// User routes
Route::name('users.')->group(static function () {
Route::get('/users/me', [UserController::class, 'me'])->name('me');
});
// User Member routes
Route::name('users.members.')->group(static function () {
Route::get('/users/me/members', [UserMemberController::class, 'myMembers'])->name('my-members');
});
// Invitation routes
Route::name('invitations.')->group(static function () {
Route::get('/organizations/{organization}/invitations', [InvitationController::class, 'index'])->name('index');

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1;
use Laravel\Passport\Passport;
class UserEndpointTest extends ApiEndpointTestAbstract
{
public function test_me_fails_when_not_authenticated(): void
{
// Act
$response = $this->getJson(route('api.v1.users.me'));
// Assert
$response->assertUnauthorized();
$response->assertJson(['message' => 'Unauthenticated.']);
}
public function test_me_returns_information_about_the_current_user(): void
{
// Arrange
$data = $this->createUserWithPermission();
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.users.me'));
// Assert
$response->assertSuccessful();
$response->assertJson([
'data' => [
'id' => $data->user->getKey(),
'name' => $data->user->name,
'email' => $data->user->email,
'profile_photo_url' => $data->user->profile_photo_url,
'timezone' => $data->user->timezone,
'week_start' => $data->user->week_start->value,
],
]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Endpoint\Api\V1;
use App\Models\Member;
use App\Models\Organization;
use Laravel\Passport\Passport;
class UserMemberEndpointTest extends ApiEndpointTestAbstract
{
public function test_my_members_fails_when_not_authenticated(): void
{
// Act
$response = $this->getJson(route('api.v1.users.members.my-members'));
// Assert
$response->assertUnauthorized();
$response->assertJson(['message' => 'Unauthenticated.']);
}
public function test_my_members_returns_information_about_the_organization_membership_of_the_current_user(): void
{
// Arrange
$data = $this->createUserWithPermission();
$otherOrganization = Organization::factory()->create();
$otherMember = Member::factory()->forOrganization($otherOrganization)->forUser($data->user)->create();
Passport::actingAs($data->user);
// Act
$response = $this->getJson(route('api.v1.users.members.my-members'));
// Assert
$response->assertSuccessful();
$response->assertJsonCount(2, 'data');
$otherMemberResponse = collect($response->json('data'))->where('id', '=', $otherMember->getKey())->first();
$this->assertNotNull($otherMemberResponse);
$this->assertSame($otherMember->organization->getKey(), $otherMemberResponse['organization']['id']);
$memberResponse = collect($response->json('data'))->where('id', '=', $data->member->getKey())->first();
$this->assertNotNull($memberResponse);
}
}