revoke team member invite
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
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\TeamInviteResource;
|
||||
use App\Http\Resources\TeamResource;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -28,10 +28,14 @@ class TeamController extends Controller
|
||||
|
||||
public function show(Request $request, Team $team)
|
||||
{
|
||||
$teamManager = auth()->user()->can('manage', $team);
|
||||
|
||||
return Inertia::render('team/Show', [
|
||||
'team' => TeamResource::make($team)->resolve($request),
|
||||
'canManageTeam' => auth()->user()->can('manage', $team),
|
||||
]);
|
||||
'canManageTeam' => $teamManager,
|
||||
] + ($teamManager ? [
|
||||
'teamInvites' => TeamInviteResource::collection($team->invites)->resolve($request),
|
||||
] : []));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,18 +79,6 @@ 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.
|
||||
*/
|
||||
@@ -104,15 +96,8 @@ class TeamController extends Controller
|
||||
Inertia::flash('error', 'Team could not be updated.');
|
||||
DB::rollBack();
|
||||
}
|
||||
return to_route('teams.show', $team);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Team $team)
|
||||
{
|
||||
//
|
||||
return to_route('teams.show', $team);
|
||||
}
|
||||
|
||||
public function nameToSlug(Request $request)
|
||||
@@ -132,4 +117,42 @@ class TeamController extends Controller
|
||||
'taken' => $slug !== $ignoredSlug && Team::whereSlug($slug)->exists(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function invite(Request $request, Team $team)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'email' => 'required|email|max:255',
|
||||
]);
|
||||
|
||||
abort_unless(auth()->user()->can('manage', $team), 403);
|
||||
|
||||
$team->invites()->create([
|
||||
'invited_email' => Arr::get($validated, 'email'),
|
||||
'invited_by_user_id' => auth()->user()->id,
|
||||
]);
|
||||
|
||||
Inertia::flash('success', 'User invited to your team.');
|
||||
}
|
||||
|
||||
public function revokeInvite(Request $request, Team $team)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'email' => 'required|email|max:255|exists:team_invites,invited_email',
|
||||
]);
|
||||
|
||||
abort_unless(auth()->user()->can('manage', $team), 403);
|
||||
|
||||
$teamInvite = $team->invites()->whereInvitedEmail(Arr::get($validated, 'email'))->firstOrFail();
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($teamInvite) {
|
||||
$teamInvite->delete();
|
||||
});
|
||||
Inertia::flash('success', 'successfully deleted user invite.');
|
||||
} catch (\Throwable $e) {
|
||||
Inertia::flash('error', 'could not delete user invite.');
|
||||
DB::rollBack();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\TeamInvite;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin TeamInvite
|
||||
*/
|
||||
class TeamInviteResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'invitedEmail' => $this->invited_email,
|
||||
'team' => TeamResource::make($this->team)->resolve($request),
|
||||
'invitedBy' => UserResource::make($this->invitedBy)->resolve($request),
|
||||
'invitedAt' => $this->created_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,15 @@ namespace App\Http\Resources;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin User
|
||||
*/
|
||||
class TeamMemberResource extends JsonResource
|
||||
class TeamMemberResource extends UserResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
|
||||
return parent::toArray($request) + [
|
||||
'role' => $this->pivot->role,
|
||||
'joined_at' => $this->pivot->created_at->toDateTimeString(),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin User
|
||||
*/
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+14
-7
@@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
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\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@@ -34,13 +36,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
#[Fillable(['name', 'slug', 'require_2fa'])]
|
||||
class Team extends Model
|
||||
{
|
||||
protected function casts()
|
||||
{
|
||||
return [
|
||||
'require_2fa' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function members(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'team_members')
|
||||
@@ -48,11 +43,23 @@ class Team extends Model
|
||||
->withPivot('role')
|
||||
->withTimestamps();
|
||||
}
|
||||
public function invites(): HasMany
|
||||
{
|
||||
return $this->hasMany(TeamInvite::class);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
public function scopeVisibleTo(Builder $query, User $user): Builder
|
||||
{
|
||||
return $query->whereHas('members', function (Builder $query) use ($user): void {
|
||||
$query->whereKey($user->getKey());
|
||||
});
|
||||
}
|
||||
|
||||
protected function casts()
|
||||
{
|
||||
return [
|
||||
'require_2fa' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import teamRoutes from '@/routes/teams';
|
||||
import {Head, Link, setLayoutProps} from '@inertiajs/vue3';
|
||||
import type {Team} from '@/types/models';
|
||||
import type {Team, TeamInvite} from '@/types/models';
|
||||
import {Card, CardContent} from '@/components/ui/card';
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Pencil} from '@lucide/vue';
|
||||
import {Pencil, Trash} from '@lucide/vue';
|
||||
|
||||
const props = defineProps<{
|
||||
team: Team;
|
||||
canManageTeam: boolean;
|
||||
teamInvites?: TeamInvite[];
|
||||
}>();
|
||||
|
||||
setLayoutProps({
|
||||
@@ -25,6 +26,12 @@ setLayoutProps({
|
||||
],
|
||||
});
|
||||
|
||||
const revokeInvite = (teamInvite: TeamInvite) => fetch(teamRoutes.invite.revoke(teamInvite.team.slug).url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({email: teamInvite.invitedEmail})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -61,7 +68,7 @@ setLayoutProps({
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card class="w-full mb-8">
|
||||
<CardContent>
|
||||
<h2 class="font-semibold text-lg">Mitglieder</h2>
|
||||
<Table>
|
||||
@@ -86,5 +93,35 @@ setLayoutProps({
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card v-if="teamInvites" class="w-full mb-8">
|
||||
<CardContent>
|
||||
<h2 class="font-semibold text-lg">Ausstehende Invites</h2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Invited</TableHead>
|
||||
<TableHead>Invited von</TableHead>
|
||||
<TableHead>Invited at</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="invite in teamInvites">
|
||||
<TableCell>{{ invite.invitedEmail }}</TableCell>
|
||||
<TableCell>{{ invite.invitedBy.name }}</TableCell>
|
||||
<TableCell>{{ invite.invitedAt }}</TableCell>
|
||||
<Link
|
||||
:href="teamRoutes.invite.revoke({team: invite.team.slug})"
|
||||
class="cursor-pointer"
|
||||
@click="revokeInvite(invite)"
|
||||
>
|
||||
<TableCell>
|
||||
<component :is="Trash" size="18"/>
|
||||
</TableCell>
|
||||
</Link>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,10 +8,20 @@ export interface Team {
|
||||
|
||||
export type TeamMemberRole = 'admin' | 'member';
|
||||
|
||||
export interface TeamMember {
|
||||
export interface User {
|
||||
name: string;
|
||||
email: string;
|
||||
has_multi_factor: boolean;
|
||||
}
|
||||
|
||||
export interface TeamMember extends User {
|
||||
role: TeamMemberRole;
|
||||
joined_at: string
|
||||
joined_at: string;
|
||||
}
|
||||
|
||||
export interface TeamInvite {
|
||||
invitedEmail: string;
|
||||
team: Team;
|
||||
invitedBy: User;
|
||||
invitedAt: string;
|
||||
}
|
||||
|
||||
+5
-3
@@ -10,9 +10,9 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::inertia('dashboard', 'Dashboard')->name('dashboard');
|
||||
|
||||
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
|
||||
Route::controller(TeamInviteAcceptController::class)->prefix('invite')->name('invite.')->group(function () {
|
||||
Route::get('/accept', 'create')->name('create');
|
||||
Route::post('/accept', 'store')->name('store');
|
||||
Route::prefix('invite')->name('invite.')->group(function () {
|
||||
Route::get('/accept', [TeamInviteAcceptController::class, 'create'])->name('create');
|
||||
Route::post('/accept', [TeamInviteAcceptController::class, 'store'])->name('store');
|
||||
});
|
||||
|
||||
Route::get('/', 'index')->name('index');
|
||||
@@ -22,6 +22,8 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::patch('/{team:slug}', 'update')->name('update');
|
||||
Route::post('/name-to-slug', 'nameToSlug')->name('nameToSlug');
|
||||
Route::get('/{team:slug}', 'show')->name('show');
|
||||
Route::post('{team:slug}/invite', [TeamController::class, 'invite'])->name('invite');
|
||||
Route::post('{team:slug}/invite/revoke', [TeamController::class, 'revokeInvite'])->name('invite.revoke');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user