Skip to content

Commit

Permalink
Moved invitation from jetstream to API; Deactived moved jetstream fea…
Browse files Browse the repository at this point in the history
…tures
  • Loading branch information
korridor committed Jul 15, 2024
1 parent 555417d commit fd8d596
Show file tree
Hide file tree
Showing 25 changed files with 294 additions and 348 deletions.
6 changes: 0 additions & 6 deletions app/Actions/Jetstream/AddOrganizationMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use App\Enums\Role;
use App\Models\Organization;
use App\Models\User;
use App\Service\UserService;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
Expand Down Expand Up @@ -43,10 +42,6 @@ public function add(User $owner, Organization $organization, string $email, ?str
$organization->users()->attach(
$newOrganizationMember, ['role' => $role]
);

if ($role === Role::Owner->value) {
app(UserService::class)->changeOwnership($organization, $newOrganizationMember);
}
});

TeamMemberAdded::dispatch($organization, $newOrganizationMember);
Expand Down Expand Up @@ -84,7 +79,6 @@ protected function rules(): array
'required',
'string',
Rule::in([
Role::Owner->value,
Role::Admin->value,
Role::Manager->value,
Role::Employee->value,
Expand Down
1 change: 1 addition & 0 deletions app/Actions/Jetstream/DeleteOrganization.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class DeleteOrganization implements DeletesTeams
*/
public function delete(Organization $organization): void
{
/** @see ValidateOrganizationDeletion */
app(DeletionService::class)->deleteOrganization($organization);
}
}
2 changes: 2 additions & 0 deletions app/Actions/Jetstream/DeleteUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class DeleteUser implements DeletesUsers
{
/**
* Delete the given user.
*
* @throws ValidationException
*/
public function delete(User $user): void
{
Expand Down
90 changes: 4 additions & 86 deletions app/Actions/Jetstream/InviteOrganizationMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,103 +4,21 @@

namespace App\Actions\Jetstream;

use App\Enums\Role;
use App\Exceptions\MovedToApiException;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Models\User;
use App\Service\PermissionStore;
use Closure;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In;
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
use Exception;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
use Laravel\Jetstream\Events\InvitingTeamMember;
use Laravel\Jetstream\Mail\TeamInvitation;

class InviteOrganizationMember implements InvitesTeamMembers
{
/**
* Invite a new team member to the given team.
*
* @throws AuthorizationException
* @throws Exception
*/
public function invite(User $user, Organization $organization, string $email, ?string $role = null): void
{
if (! app(PermissionStore::class)->has($organization, 'invitations:create')) {
throw new AuthorizationException();
}

$this->validate($organization, $email, $role);

InvitingTeamMember::dispatch($organization, $email, $role);

/** @var OrganizationInvitation $invitation */
$invitation = $organization->teamInvitations()->create([
'email' => $email,
'role' => $role,
]);

Mail::to($email)->send(new TeamInvitation($invitation));
}

/**
* Validate the invite member operation.
*/
protected function validate(Organization $organization, string $email, ?string $role): void
{
Validator::make([
'email' => $email,
'role' => $role,
], $this->rules($organization))->after(
$this->ensureUserIsNotAlreadyOnTeam($organization, $email)
)->validateWithBag('addTeamMember');
}

/**
* Get the validation rules for inviting a team member.
*
* @return array<string, array<ValidationRule|Rule|string|In>>
*/
protected function rules(Organization $organization): array
{
return array_filter([
'email' => [
'required',
'email',
(new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder) use ($organization) {
/** @var Builder<OrganizationInvitation> $builder */
return $builder->whereBelongsTo($organization, 'organization');
}))->withMessage(__('This user has already been invited to the team.')),
],
'role' => [
'required',
'string',
Rule::in([
Role::Owner->value,
Role::Admin->value,
Role::Manager->value,
Role::Employee->value,
]),
],
]);
}

/**
* Ensure that the user is not already on the team.
*/
protected function ensureUserIsNotAlreadyOnTeam(Organization $organization, string $email): Closure
{
return function ($validator) use ($organization, $email) {
$validator->errors()->addIf(
$organization->hasRealUserWithEmail($email),
'email',
__('This user already belongs to the team.')
);
};
throw new MovedToApiException();
}
}
39 changes: 5 additions & 34 deletions app/Actions/Jetstream/RemoveOrganizationMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,21 @@

namespace App\Actions\Jetstream;

use App\Exceptions\MovedToApiException;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
use Exception;
use Laravel\Jetstream\Contracts\RemovesTeamMembers;
use Laravel\Jetstream\Events\TeamMemberRemoved;

class RemoveOrganizationMember implements RemovesTeamMembers
{
/**
* Remove the team member from the given team.
*
* @throws Exception
*/
public function remove(User $user, Organization $organization, User $teamMember): void
{
$this->authorize($user, $organization, $teamMember);

$this->ensureUserDoesNotOwnTeam($teamMember, $organization);

$organization->removeUser($teamMember);

TeamMemberRemoved::dispatch($organization, $teamMember);
}

/**
* Authorize that the user can remove the team member.
*/
protected function authorize(User $user, Organization $organization, User $teamMember): void
{
if (! Gate::forUser($user)->check('removeTeamMember', $organization) &&
$user->id !== $teamMember->id) {
throw new AuthorizationException;
}
}

/**
* Ensure that the currently authenticated user does not own the team.
*/
protected function ensureUserDoesNotOwnTeam(User $teamMember, Organization $organization): void
{
if ($teamMember->id === $organization->owner->id) {
throw ValidationException::withMessages([
'team' => [__('You may not leave a team that you created.')],
])->errorBag('removeTeamMember');
}
throw new MovedToApiException();
}
}
50 changes: 4 additions & 46 deletions app/Actions/Jetstream/UpdateMemberRole.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,21 @@
namespace App\Actions\Jetstream;

use App\Enums\Role;
use App\Exceptions\MovedToApiException;
use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use App\Service\PermissionStore;
use App\Service\UserService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Events\TeamMemberUpdated;
use Exception;

class UpdateMemberRole
{
/**
* Update the role for the given team member.
*
* @throws AuthorizationException
* @throws ValidationException
* @throws Exception
*/
public function update(User $actingUser, Organization $organization, string $userId, string $role): void
{
if (! app(PermissionStore::class)->has($organization, 'members:change-ownership')) {
throw new AuthorizationException();
}

$user = User::where('id', '=', $userId)->firstOrFail();
$member = Member::whereBelongsTo($user)->whereBelongsTo($organization)->firstOrFail();
if ($member->role === Role::Placeholder->value) {
abort(403, 'Cannot update the role of a placeholder member.');
}

Validator::make([
'role' => $role,
], [
'role' => [
'required',
'string',
Rule::in([
Role::Owner->value,
Role::Admin->value,
Role::Manager->value,
Role::Employee->value,
]),
],
])->validate();

DB::transaction(function () use ($organization, $userId, $role, $user) {
$organization->users()->updateExistingPivot($userId, [
'role' => $role,
]);

if ($role === Role::Owner->value) {
app(UserService::class)->changeOwnership($organization, $user);
}
});

TeamMemberUpdated::dispatch($organization->fresh(), User::findOrFail($userId));
throw new MovedToApiException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace App\Exceptions\Api;

class UserIsAlreadyMemberOfOrganizationApiException extends ApiException
{
public const string KEY = 'user_is_already_member_of_organization';
}
15 changes: 15 additions & 0 deletions app/Exceptions/MovedToApiException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Exceptions;

use Symfony\Component\HttpKernel\Exception\HttpException;

class MovedToApiException extends HttpException
{
public function __construct()
{
parent::__construct(403, 'Moved to API');
}
}
21 changes: 11 additions & 10 deletions app/Http/Controllers/Api/V1/InvitationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@

namespace App\Http\Controllers\Api\V1;

use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
use App\Http\Resources\V1\Invitation\InvitationCollection;
use App\Http\Resources\V1\Invitation\InvitationResource;
use App\Mail\OrganizationInvitationMail;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Service\InvitationService;
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
{
Expand Down Expand Up @@ -49,19 +50,18 @@ public function index(Organization $organization, InvitationIndexRequest $reques
* Invite a user to the organization
*
* @throws AuthorizationException
* @throws UserIsAlreadyMemberOfOrganizationApiException
*
* @operationId invite
*/
public function store(Organization $organization, InvitationStoreRequest $request): JsonResponse
public function store(Organization $organization, InvitationStoreRequest $request, InvitationService $invitationService): JsonResponse
{
$this->checkPermission($organization, 'invitations:create');

app(InvitesTeamMembers::class)->invite(
$this->user(),
$organization,
$request->input('email'),
$request->getRole()->value
);
$email = $request->getEmail();
$role = $request->getRole();

$invitationService->inviteUser($organization, $email, $role);

return response()->json(null, 204);
}
Expand All @@ -77,7 +77,8 @@ public function resend(Organization $organization, OrganizationInvitation $invit
{
$this->checkPermission($organization, 'invitations:resend', $invitation);

Mail::to($invitation->email)->send(new TeamInvitation($invitation));
Mail::to($invitation->email)
->queue(new OrganizationInvitationMail($invitation));

return response()->json(null, 204);
}
Expand Down
12 changes: 3 additions & 9 deletions app/Http/Controllers/Api/V1/MemberController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@
use App\Models\ProjectMember;
use App\Models\TimeEntry;
use App\Service\BillableRateService;
use App\Service\InvitationService;
use App\Service\MemberService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;

class MemberController extends Controller
{
Expand Down Expand Up @@ -134,7 +133,7 @@ public function destroy(Organization $organization, Member $member): JsonRespons
*
* @operationId invitePlaceholder
*/
public function invitePlaceholder(Organization $organization, Member $member, Request $request): JsonResponse
public function invitePlaceholder(Organization $organization, Member $member, InvitationService $invitationService): JsonResponse
{
$this->checkPermission($organization, 'members:invite-placeholder', $member);
$user = $member->user;
Expand All @@ -143,12 +142,7 @@ public function invitePlaceholder(Organization $organization, Member $member, Re
throw new UserNotPlaceholderApiException();
}

app(InvitesTeamMembers::class)->invite(
$this->user(),
$organization,
$user->email,
Role::Employee->value,
);
$invitationService->inviteUser($organization, $user->email, Role::Employee);

return response()->json(null, 204);
}
Expand Down
Loading

0 comments on commit fd8d596

Please sign in to comment.