diff --git a/app/Actions/Teams/AcceptTeamInvite.php b/app/Actions/Teams/AcceptTeamInvite.php
new file mode 100644
index 0000000..b5b280e
--- /dev/null
+++ b/app/Actions/Teams/AcceptTeamInvite.php
@@ -0,0 +1,27 @@
+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();
+ }
+ }
+}
diff --git a/app/Http/Controllers/TeamController.php b/app/Http/Controllers/TeamController.php
index 778326d..17c2e6c 100644
--- a/app/Http/Controllers/TeamController.php
+++ b/app/Http/Controllers/TeamController.php
@@ -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.
*/
diff --git a/app/Http/Controllers/TeamInviteAcceptController.php b/app/Http/Controllers/TeamInviteAcceptController.php
new file mode 100644
index 0000000..2764adb
--- /dev/null
+++ b/app/Http/Controllers/TeamInviteAcceptController.php
@@ -0,0 +1,39 @@
+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');
+ }
+}
diff --git a/app/Http/Requests/AcceptTeamInviteRequest.php b/app/Http/Requests/AcceptTeamInviteRequest.php
new file mode 100644
index 0000000..6ca6e61
--- /dev/null
+++ b/app/Http/Requests/AcceptTeamInviteRequest.php
@@ -0,0 +1,39 @@
+|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();
+ }
+}
diff --git a/app/Http/Requests/AddTeamMemberRequest.php b/app/Http/Requests/AddTeamMemberRequest.php
new file mode 100644
index 0000000..a57f977
--- /dev/null
+++ b/app/Http/Requests/AddTeamMemberRequest.php
@@ -0,0 +1,21 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'email' => 'required|string|email|max:255',
+ ];
+ }
+}
diff --git a/app/Models/TeamInvite.php b/app/Models/TeamInvite.php
new file mode 100644
index 0000000..5b36eed
--- /dev/null
+++ b/app/Models/TeamInvite.php
@@ -0,0 +1,64 @@
+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