From 53ba60bbe5a4504a43fe74ac670a118a61410b6c Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Sun, 21 Apr 2024 20:25:33 +0200 Subject: [PATCH 1/5] Added resend invitation endpoint --- .../Api/V1/InvitationController.php | 18 ++++++ app/Providers/JetstreamServiceProvider.php | 9 +++ routes/api.php | 1 + tests/TestCase.php | 2 + .../Api/V1/InvitationEndpointTest.php | 61 +++++++++++++++++++ 5 files changed, 91 insertions(+) diff --git a/app/Http/Controllers/Api/V1/InvitationController.php b/app/Http/Controllers/Api/V1/InvitationController.php index 397decf0..94875cbc 100644 --- a/app/Http/Controllers/Api/V1/InvitationController.php +++ b/app/Http/Controllers/Api/V1/InvitationController.php @@ -12,7 +12,9 @@ use App\Models\Organization; use App\Models\OrganizationInvitation; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Mail; use Laravel\Jetstream\Contracts\InvitesTeamMembers; +use Laravel\Jetstream\Mail\TeamInvitation; class InvitationController extends Controller { @@ -64,6 +66,22 @@ class InvitationController extends Controller return response()->json(null, 204); } + /** + * Resend email for a pending invitation + * + * @throws AuthorizationException + * + * @operationId resendInvitationEmail + */ + public function resend(Organization $organization, OrganizationInvitation $invitation): JsonResponse + { + $this->checkPermission($organization, 'invitations:resend', $invitation); + + Mail::to($invitation->email)->send(new TeamInvitation($invitation)); + + return response()->json(null, 204); + } + /** * Remove a pending invitation * diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index bc787977..f975e58f 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -71,6 +71,7 @@ class JetstreamServiceProvider extends ServiceProvider 'project-members:update', 'project-members:delete', 'tasks:view', + 'tasks:view:all', 'tasks:create', 'tasks:update', 'tasks:delete', @@ -95,6 +96,7 @@ class JetstreamServiceProvider extends ServiceProvider 'import', 'invitations:view', 'invitations:create', + 'invitations:resend', 'invitations:remove', 'members:view', 'members:invite-placeholder', @@ -114,6 +116,7 @@ class JetstreamServiceProvider extends ServiceProvider 'project-members:update', 'project-members:delete', 'tasks:view', + 'tasks:view:all', 'tasks:create', 'tasks:update', 'tasks:delete', @@ -136,6 +139,10 @@ class JetstreamServiceProvider extends ServiceProvider 'organizations:view', 'organizations:update', 'import', + 'invitations:view', + 'invitations:create', + 'invitations:resend', + 'invitations:remove', 'members:view', 'members:invite-placeholder', ])->description('Administrator users can perform any action.'); @@ -151,6 +158,7 @@ class JetstreamServiceProvider extends ServiceProvider 'project-members:update', 'project-members:delete', 'tasks:view', + 'tasks:view:all', 'tasks:create', 'tasks:update', 'tasks:delete', @@ -171,6 +179,7 @@ class JetstreamServiceProvider extends ServiceProvider 'clients:update', 'clients:delete', 'organizations:view', + 'invitations:view', 'members:view', ])->description('Managers have the ability to read, create, and update their own time entries as well as those of their team.'); diff --git a/routes/api.php b/routes/api.php index 1db37f7a..0c9494ba 100644 --- a/routes/api.php +++ b/routes/api.php @@ -49,6 +49,7 @@ Route::middleware([ Route::name('invitations.')->group(static function () { Route::get('/organizations/{organization}/invitations', [InvitationController::class, 'index'])->name('index'); Route::post('/organizations/{organization}/invitations', [InvitationController::class, 'store'])->name('store'); + Route::post('/organizations/{organization}/invitations/{invitation}/resend', [InvitationController::class, 'resend'])->name('resend'); Route::delete('/organizations/{organization}/invitations/{invitation}', [InvitationController::class, 'destroy'])->name('destroy'); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index e47e0a72..0aeb8c23 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,6 +6,7 @@ namespace Tests; use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; +use Illuminate\Support\Facades\Mail; use TiMacDonald\Log\LogFake; abstract class TestCase extends BaseTestCase @@ -15,6 +16,7 @@ abstract class TestCase extends BaseTestCase protected function setUp(): void { parent::setUp(); + Mail::fake(); LogFake::bind(); } diff --git a/tests/Unit/Endpoint/Api/V1/InvitationEndpointTest.php b/tests/Unit/Endpoint/Api/V1/InvitationEndpointTest.php index bd8fe78f..1afb963b 100644 --- a/tests/Unit/Endpoint/Api/V1/InvitationEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/InvitationEndpointTest.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Tests\Unit\Endpoint\Api\V1; use App\Models\OrganizationInvitation; +use Illuminate\Support\Facades\Mail; +use Laravel\Jetstream\Mail\TeamInvitation; use Laravel\Passport\Passport; class InvitationEndpointTest extends ApiEndpointTestAbstract @@ -77,6 +79,65 @@ class InvitationEndpointTest extends ApiEndpointTestAbstract $this->assertEquals('employee', $invitation->role); } + public function test_resend_fails_if_user_has_no_permission_to_resend_the_invitation(): void + { + // Arrange + $data = $this->createUserWithPermission([ + ]); + Passport::actingAs($data->user); + $invitation = OrganizationInvitation::factory()->forOrganization($data->organization)->create(); + + // Act + $response = $this->postJson(route('api.v1.invitations.resend', [ + $data->organization->getKey(), + $invitation->getKey(), + ])); + + // Assert + Mail::assertNothingSent(); + Mail::assertNothingQueued(); + $response->assertStatus(403); + } + + public function test_resend_fails_if_invitation_belongs_to_different_organization(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'invitations:resend', + ]); + Passport::actingAs($data->user); + $invitation = OrganizationInvitation::factory()->create(); + + // Act + $response = $this->postJson(route('api.v1.invitations.resend', [$data->organization->getKey(), $invitation->getKey()])); + + // Assert + Mail::assertNothingSent(); + Mail::assertNothingQueued(); + $response->assertStatus(403); + } + + public function test_resend_resends_invitation_email(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'invitations:resend', + ]); + Passport::actingAs($data->user); + $invitation = OrganizationInvitation::factory()->forOrganization($data->organization)->create(); + + // Act + $response = $this->postJson(route('api.v1.invitations.resend', [ + $data->organization->getKey(), + $invitation->getKey(), + ])); + + // Assert + Mail::assertSent(fn (TeamInvitation $mail): bool => $mail->invitation->is($invitation)); + Mail::assertNothingQueued(); + $response->assertStatus(204); + } + public function test_delete_fails_if_user_has_no_permission_to_remove_invitations(): void { // Arrange From 68e5f891871d1b90f371fd26a1a1a09e9c51c631 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Sun, 21 Apr 2024 20:25:49 +0200 Subject: [PATCH 2/5] Updated favicons --- public/favicons/browserconfig.xml | 2 +- public/favicons/site.webmanifest | 4 ++-- resources/views/app.blade.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/favicons/browserconfig.xml b/public/favicons/browserconfig.xml index d469e489..9824d87b 100644 --- a/public/favicons/browserconfig.xml +++ b/public/favicons/browserconfig.xml @@ -3,7 +3,7 @@ - #da532c + #000000 diff --git a/public/favicons/site.webmanifest b/public/favicons/site.webmanifest index 3035a89b..c620e597 100644 --- a/public/favicons/site.webmanifest +++ b/public/favicons/site.webmanifest @@ -13,7 +13,7 @@ "type": "image/png" } ], - "theme_color": "#ffffff", - "background_color": "#ffffff", + "theme_color": "#000000", + "background_color": "#000000", "display": "standalone" } diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 6e46e928..f4cb779c 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -17,9 +17,9 @@ - + - + @routes From 2acef6a997d5fb7380651bbb6bb05ff504264ad1 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Sun, 21 Apr 2024 20:25:58 +0200 Subject: [PATCH 3/5] Fixed typos --- resources/js/Components/OrganizationSwitcher.vue | 8 +++----- resources/js/Pages/Teams/Partials/CreateTeamForm.vue | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/resources/js/Components/OrganizationSwitcher.vue b/resources/js/Components/OrganizationSwitcher.vue index db43be99..05e916da 100644 --- a/resources/js/Components/OrganizationSwitcher.vue +++ b/resources/js/Components/OrganizationSwitcher.vue @@ -90,16 +90,14 @@ const switchToTeam = (team: Organization) => { + href="/billing"> Billing - Create New Team + Create new organization @@ -107,7 +105,7 @@ const switchToTeam = (team: Organization) => {
- Switch Teams + Switch Organizations