invite team member
CI/CD / test_and_check_app (push) Failing after 57s
CI/CD / Publish immutable image (push) Skipped

This commit is contained in:
2026-09-19 04:56:36 +02:00
parent f9958fb7cf
commit 5113ea56fc
12 changed files with 442 additions and 52 deletions
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Actions\Teams;
use App\Models\TeamInvite;
use App\Models\User;
use Illuminate\Support\Facades\DB;
readonly class AcceptTeamInvite
{
public function __invoke(TeamInvite $invite): void
{
abort_unless(auth()->user()->email === $invite->invited_email, 403);
$team = $invite->team;
$newUser = User::whereEmail($invite->invited_email)->firstOrFail();
try {
DB::transaction(function () use ($team, $newUser, $invite) {
$team->members()->attach($newUser);
$invite->delete();
});
} catch (\Exception $exception) {
DB::rollBack();
}
}
}
+13
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Enums\TeamMemberRole;
use App\Http\Requests\AddTeamMemberRequest;
use App\Http\Requests\StoreTeamRequest;
use App\Http\Requests\UpdateTeamRequest;
use App\Http\Resources\TeamResource;
@@ -74,6 +75,18 @@ class TeamController extends Controller
]);
}
/**
* Show the form for editing the specified resource.
*/
public function addMember(AddTeamMemberRequest $request, Team $team)
{
$validated = $request->validated();
$email = Arr::get($validated, 'email');
return response()->json(['message' => __('')]);
}
/**
* Update the specified resource in storage.
*/
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Teams\AcceptTeamInvite;
use App\Http\Requests\AcceptTeamInviteRequest;
use App\Models\TeamInvite;
use Illuminate\Http\Request;
use Inertia\Inertia;
class TeamInviteAcceptController extends Controller
{
public function create(Request $request, AcceptTeamInvite $accept)
{
$inviteToken = $request->input('inviteToken');
if ($inviteToken) {
$invite = TeamInvite::findByPlainToken($inviteToken)->first();
if ($invite) {
$accept($invite);
return to_route('teams.index');
}
}
return Inertia::render('team/invite/Create', [
'inviteToken' => $inviteToken,
]);
}
public function store(AcceptTeamInviteRequest $request, AcceptTeamInvite $accept)
{
$request->validated();
$accept($request->teamInvite());
return to_route('teams.index');
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use App\Models\TeamInvite;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class AcceptTeamInviteRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'inviteToken' => 'required|string',
];
}
public function after(): array
{
return [
function (Validator $validator) {
if (($this->teamInvite() == null) || ($this->teamInvite()->invited_email !== auth()->user()->email)) {
$validator->errors()->add('inviteToken', 'InviteToken is invalid or expired.');
}
},
];
}
public function teamInvite(): ?TeamInvite
{
return TeamInvite::findByPlainToken($this->inviteToken)->first();
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class AddTeamMemberRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => 'required|string|email|max:255',
];
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Models;
use App\Notifications\InviteToTeam;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Notifications\Notifiable;
use Illuminate\Notifications\Notification;
use Str;
#[Fillable(['team_id', 'invited_by_user_id', 'invited_email'])]
class TeamInvite extends Model
{
use Notifiable;
protected ?string $plainInviteToken = null;
protected static function booted(): void
{
self::creating(function (self $model) {
$model->plainInviteToken = Str::random(128);
$hashedToken = hash('sha256', $model->plainInviteToken);
$model->invite_token = $hashedToken;
});
self::created(function (self $model) {
$model->notify(new InviteToTeam);
});
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
public function invitedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'invited_by_user_id');
}
public function plainInviteToken(): ?string
{
return $this->plainInviteToken;
}
/**
* Route notifications for the mail channel.
*
* @return array<string, string>|string
*/
public function routeNotificationForMail(Notification $notification): array|string
{
return $this->invited_email;
}
#[Scope]
protected function findByPlainToken(Builder $query, string $token): Builder
{
return $query->where('invite_token', hash('sha256', $token));
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Notifications;
use App\Models\TeamInvite;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class InviteToTeam extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(TeamInvite $invite): MailMessage
{
$user = User::whereEmail($invite->invited_email)->first();
return (new MailMessage)
->subject('Team Invitation')
->greeting(
$user !== null
? sprintf('Hello %s,', $user->name)
: 'Hello',
)
->line(sprintf(
'%s invited you to the %s team.',
$invite->invitedBy->name,
$invite->team->name,
))
->linesIf($user === null, [
'It seems like you don\'t have an account yet.',
'Create one now for free to join the team.',
])
->action($user === null
? 'Register and join this team'
: 'Join this team',
route($user === null
? 'register'
: 'teams.invite.create',
['inviteToken' => $invite->plainInviteToken()]
));
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}