revoke team member invite
This commit is contained in:
@@ -3,9 +3,9 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Enums\TeamMemberRole;
|
use App\Enums\TeamMemberRole;
|
||||||
use App\Http\Requests\AddTeamMemberRequest;
|
|
||||||
use App\Http\Requests\StoreTeamRequest;
|
use App\Http\Requests\StoreTeamRequest;
|
||||||
use App\Http\Requests\UpdateTeamRequest;
|
use App\Http\Requests\UpdateTeamRequest;
|
||||||
|
use App\Http\Resources\TeamInviteResource;
|
||||||
use App\Http\Resources\TeamResource;
|
use App\Http\Resources\TeamResource;
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -28,10 +28,14 @@ class TeamController extends Controller
|
|||||||
|
|
||||||
public function show(Request $request, Team $team)
|
public function show(Request $request, Team $team)
|
||||||
{
|
{
|
||||||
|
$teamManager = auth()->user()->can('manage', $team);
|
||||||
|
|
||||||
return Inertia::render('team/Show', [
|
return Inertia::render('team/Show', [
|
||||||
'team' => TeamResource::make($team)->resolve($request),
|
'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.
|
* Update the specified resource in storage.
|
||||||
*/
|
*/
|
||||||
@@ -104,15 +96,8 @@ class TeamController extends Controller
|
|||||||
Inertia::flash('error', 'Team could not be updated.');
|
Inertia::flash('error', 'Team could not be updated.');
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
}
|
}
|
||||||
return to_route('teams.show', $team);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return to_route('teams.show', $team);
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(Team $team)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nameToSlug(Request $request)
|
public function nameToSlug(Request $request)
|
||||||
@@ -132,4 +117,42 @@ class TeamController extends Controller
|
|||||||
'taken' => $slug !== $ignoredSlug && Team::whereSlug($slug)->exists(),
|
'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 App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @mixin User
|
* @mixin User
|
||||||
*/
|
*/
|
||||||
class TeamMemberResource extends JsonResource
|
class TeamMemberResource extends UserResource
|
||||||
{
|
{
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return parent::toArray($request) + [
|
||||||
'name' => $this->name,
|
|
||||||
'email' => $this->email,
|
|
||||||
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
|
|
||||||
'role' => $this->pivot->role,
|
'role' => $this->pivot->role,
|
||||||
'joined_at' => $this->pivot->created_at->toDateTimeString(),
|
'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;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@@ -34,13 +36,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|||||||
#[Fillable(['name', 'slug', 'require_2fa'])]
|
#[Fillable(['name', 'slug', 'require_2fa'])]
|
||||||
class Team extends Model
|
class Team extends Model
|
||||||
{
|
{
|
||||||
protected function casts()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'require_2fa' => 'boolean',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function members(): BelongsToMany
|
public function members(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class, 'team_members')
|
return $this->belongsToMany(User::class, 'team_members')
|
||||||
@@ -48,11 +43,23 @@ class Team extends Model
|
|||||||
->withPivot('role')
|
->withPivot('role')
|
||||||
->withTimestamps();
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
|
public function invites(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(TeamInvite::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Scope]
|
||||||
public function scopeVisibleTo(Builder $query, User $user): Builder
|
public function scopeVisibleTo(Builder $query, User $user): Builder
|
||||||
{
|
{
|
||||||
return $query->whereHas('members', function (Builder $query) use ($user): void {
|
return $query->whereHas('members', function (Builder $query) use ($user): void {
|
||||||
$query->whereKey($user->getKey());
|
$query->whereKey($user->getKey());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function casts()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'require_2fa' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import teamRoutes from '@/routes/teams';
|
import teamRoutes from '@/routes/teams';
|
||||||
import {Head, Link, setLayoutProps} from '@inertiajs/vue3';
|
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 {Card, CardContent} from '@/components/ui/card';
|
||||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||||
import {Button} from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
import {Pencil} from '@lucide/vue';
|
import {Pencil, Trash} from '@lucide/vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
team: Team;
|
team: Team;
|
||||||
canManageTeam: boolean;
|
canManageTeam: boolean;
|
||||||
|
teamInvites?: TeamInvite[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
setLayoutProps({
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -61,7 +68,7 @@ setLayoutProps({
|
|||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card class="w-full mb-8">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<h2 class="font-semibold text-lg">Mitglieder</h2>
|
<h2 class="font-semibold text-lg">Mitglieder</h2>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -86,5 +93,35 @@ setLayoutProps({
|
|||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,10 +8,20 @@ export interface Team {
|
|||||||
|
|
||||||
export type TeamMemberRole = 'admin' | 'member';
|
export type TeamMemberRole = 'admin' | 'member';
|
||||||
|
|
||||||
export interface TeamMember {
|
export interface User {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
has_multi_factor: boolean;
|
has_multi_factor: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamMember extends User {
|
||||||
role: TeamMemberRole;
|
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::inertia('dashboard', 'Dashboard')->name('dashboard');
|
||||||
|
|
||||||
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
|
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
|
||||||
Route::controller(TeamInviteAcceptController::class)->prefix('invite')->name('invite.')->group(function () {
|
Route::prefix('invite')->name('invite.')->group(function () {
|
||||||
Route::get('/accept', 'create')->name('create');
|
Route::get('/accept', [TeamInviteAcceptController::class, 'create'])->name('create');
|
||||||
Route::post('/accept', 'store')->name('store');
|
Route::post('/accept', [TeamInviteAcceptController::class, 'store'])->name('store');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/', 'index')->name('index');
|
Route::get('/', 'index')->name('index');
|
||||||
@@ -22,6 +22,8 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
Route::patch('/{team:slug}', 'update')->name('update');
|
Route::patch('/{team:slug}', 'update')->name('update');
|
||||||
Route::post('/name-to-slug', 'nameToSlug')->name('nameToSlug');
|
Route::post('/name-to-slug', 'nameToSlug')->name('nameToSlug');
|
||||||
Route::get('/{team:slug}', 'show')->name('show');
|
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