diff --git a/app/Http/Controllers/TeamController.php b/app/Http/Controllers/TeamController.php index 3a88786..786967b 100644 --- a/app/Http/Controllers/TeamController.php +++ b/app/Http/Controllers/TeamController.php @@ -8,63 +8,63 @@ use App\Http\Requests\UpdateTeamRequest; use App\Http\Resources\TeamInviteResource; use App\Http\Resources\TeamResource; use App\Models\Team; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; use Inertia\Inertia; -use Str; +use Inertia\Response; class TeamController extends Controller { /** * Display a listing of the resource. */ - public function index(Request $request) + public function index(Request $request): Response { return Inertia::render('team/Index', [ - 'teams' => TeamResource::collection(Team::visibleTo(auth()->user())->get())->resolve($request), + 'teams' => TeamResource::collection(Team::visibleTo($request->user())->with('members')->get())->resolve($request), ]); } - public function show(Request $request, Team $team) + public function show(Request $request, Team $team): Response { - $teamManager = auth()->user()->can('manage', $team); + $teamManager = $request->user()->can('manage', $team); + $team->load('members'); return Inertia::render('team/Show', [ 'team' => TeamResource::make($team)->resolve($request), 'canManageTeam' => $teamManager, ] + ($teamManager ? [ - 'teamInvites' => TeamInviteResource::collection($team->invites)->resolve($request), + 'teamInvites' => TeamInviteResource::collection($team->invites()->with('invitedBy')->get())->resolve($request), ] : [])); } /** * Store a newly created resource in storage. */ - public function store(StoreTeamRequest $request) + public function store(StoreTeamRequest $request): RedirectResponse { - $validated = $request->validated(); - try { - DB::transaction(function () use ($validated) { - $team = Team::create($validated); - $team->members()->attach( - request()->user()->id, - ['role' => TeamMemberRole::Admin] - ); + DB::transaction(function () use ($request): void { + $team = Team::create($request->validated()); + $team->members()->attach($request->user(), ['role' => TeamMemberRole::Admin]); }); - Inertia::flash('success', 'Team successfully created.'); - - return to_route('teams.index'); } catch (\Exception $exception) { + report($exception); Inertia::flash('error', 'Team could not be created.'); - DB::rollBack(); - return back(); + return back()->withInput(); } + + Inertia::flash('success', 'Team successfully created.'); + + return to_route('teams.index'); } - public function create() + public function create(): Response { return Inertia::render('team/Create'); } @@ -72,7 +72,7 @@ class TeamController extends Controller /** * Show the form for editing the specified resource. */ - public function edit(Request $request, Team $team) + public function edit(Request $request, Team $team): Response { return Inertia::render('team/Edit', [ 'team' => TeamResource::make($team)->resolve($request), @@ -82,25 +82,23 @@ class TeamController extends Controller /** * Update the specified resource in storage. */ - public function update(UpdateTeamRequest $request, Team $team) + public function update(UpdateTeamRequest $request, Team $team): RedirectResponse { - $validated = $request->validated(); - try { - DB::transaction(function () use ($validated, $team) { - $team->update($validated); - }); - Inertia::flash('success', 'Team successfully updated.'); - + $team->update($request->validated()); } catch (\Exception $exception) { + report($exception); Inertia::flash('error', 'Team could not be updated.'); - DB::rollBack(); + + return back()->withInput(); } + Inertia::flash('success', 'Team successfully updated.'); + return to_route('teams.show', $team); } - public function nameToSlug(Request $request) + public function nameToSlug(Request $request): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', @@ -114,7 +112,7 @@ class TeamController extends Controller return response()->json([ 'name' => $name, 'slug' => $slug, - 'taken' => $slug !== $ignoredSlug && Team::whereSlug($slug)->exists(), + 'taken' => $slug === 'create' || ($slug !== $ignoredSlug && Team::whereSlug($slug)->exists()), ]); } @@ -134,25 +132,25 @@ class TeamController extends Controller Inertia::flash('success', 'User invited to your team.'); } - public function revokeInvite(Request $request, Team $team) + public function revokeInvite(Request $request, Team $team): RedirectResponse { $validated = $request->validate([ - 'email' => 'required|email|max:255|exists:team_invites,invited_email', + 'email' => 'required|email|max:255', ]); - abort_unless(auth()->user()->can('manage', $team), 403); - - $teamInvite = $team->invites()->whereInvitedEmail(Arr::get($validated, 'email'))->firstOrFail(); + $teamInvite = $team->invites()->whereInvitedEmail($validated['email'])->firstOrFail(); try { - DB::transaction(function () use ($teamInvite) { - $teamInvite->delete(); - }); - Inertia::flash('success', 'successfully deleted user invite.'); - } catch (\Throwable $e) { + $teamInvite->delete(); + } catch (\Exception $exception) { + report($exception); Inertia::flash('error', 'could not delete user invite.'); - DB::rollBack(); + + return to_route('teams.show', $team); } + Inertia::flash('success', 'successfully deleted user invite.'); + + return to_route('teams.show', $team); } } diff --git a/app/Http/Requests/StoreTeamRequest.php b/app/Http/Requests/StoreTeamRequest.php index 3352198..065d19d 100644 --- a/app/Http/Requests/StoreTeamRequest.php +++ b/app/Http/Requests/StoreTeamRequest.php @@ -5,6 +5,7 @@ namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Str; +use Illuminate\Validation\Rule; class StoreTeamRequest extends FormRequest { @@ -17,7 +18,7 @@ class StoreTeamRequest extends FormRequest { return [ 'name' => ['required', 'string', 'max:255'], - 'slug' => ['required', 'string', 'max:255', 'unique:teams,slug'], + 'slug' => ['required', 'string', 'max:255', Rule::notIn(['create']), Rule::unique('teams', 'slug')], 'require_2fa' => ['required', 'boolean'], ]; } @@ -25,9 +26,8 @@ class StoreTeamRequest extends FormRequest public function messages(): array { return [ - 'slug.string' => '', - 'slug.required' => '', - 'slug.max' => '', + 'slug.required' => 'The name must contain letters or numbers.', + 'slug.not_in' => 'This name is reserved.', 'slug.unique' => 'The name has already been taken.', ]; } @@ -35,7 +35,7 @@ class StoreTeamRequest extends FormRequest protected function prepareForValidation(): void { $this->merge([ - 'slug' => Str::slug($this->name), + 'slug' => is_string($this->input('name')) ? Str::slug($this->input('name')) : null, ]); } } diff --git a/app/Http/Requests/UpdateTeamRequest.php b/app/Http/Requests/UpdateTeamRequest.php index 5263aa6..97fba60 100644 --- a/app/Http/Requests/UpdateTeamRequest.php +++ b/app/Http/Requests/UpdateTeamRequest.php @@ -5,6 +5,7 @@ namespace App\Http\Requests; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Str; +use Illuminate\Validation\Rule; class UpdateTeamRequest extends FormRequest { @@ -16,8 +17,8 @@ class UpdateTeamRequest extends FormRequest public function rules(): array { return [ - 'name' => ['nullable', 'string', 'max:255'], - 'slug' => ['nullable', 'required_with:name', 'string', 'max:255', 'unique:teams,slug,'.$this->route('team')->id], + 'name' => ['sometimes', 'required', 'string', 'max:255'], + 'slug' => [Rule::excludeIf(! $this->has('name')), 'required', 'string', 'max:255', Rule::notIn(['create']), Rule::unique('teams', 'slug')->ignore($this->route('team'))], 'require_2fa' => ['required', 'boolean'], ]; } @@ -25,9 +26,8 @@ class UpdateTeamRequest extends FormRequest public function messages(): array { return [ - 'slug.string' => '', - 'slug.required' => '', - 'slug.max' => '', + 'slug.required' => 'The name must contain letters or numbers.', + 'slug.not_in' => 'This name is reserved.', 'slug.unique' => 'The name has already been taken.', ]; } @@ -35,7 +35,7 @@ class UpdateTeamRequest extends FormRequest protected function prepareForValidation(): void { $this->merge([ - 'slug' => Str::slug($this->name), + 'slug' => is_string($this->input('name')) ? Str::slug($this->input('name')) : null, ]); } } diff --git a/app/Http/Resources/TeamInviteResource.php b/app/Http/Resources/TeamInviteResource.php index e0b18ff..28a1596 100644 --- a/app/Http/Resources/TeamInviteResource.php +++ b/app/Http/Resources/TeamInviteResource.php @@ -20,7 +20,6 @@ class TeamInviteResource extends JsonResource { return [ 'invitedEmail' => $this->invited_email, - 'team' => TeamResource::make($this->team)->resolve($request), 'invitedBy' => UserResource::make($this->invitedBy)->resolve($request), 'invitedAt' => $this->created_at->toDateTimeString(), ]; diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 2f40320..a07caca 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -21,7 +21,7 @@ class UserResource extends JsonResource return [ 'name' => $this->name, 'email' => $this->email, - 'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(), + 'has_multi_factor' => $this->hasEnabledTwoFactorAuthentication(), ]; } } diff --git a/database/factories/TeamFactory.php b/database/factories/TeamFactory.php new file mode 100644 index 0000000..a2ff1a6 --- /dev/null +++ b/database/factories/TeamFactory.php @@ -0,0 +1,22 @@ + */ +class TeamFactory extends Factory +{ + public function definition(): array + { + $name = fake()->unique()->company(); + + return [ + 'name' => $name, + 'slug' => Str::slug($name), + 'require_2fa' => false, + ]; + } +} diff --git a/database/factories/TeamInviteFactory.php b/database/factories/TeamInviteFactory.php new file mode 100644 index 0000000..c755a83 --- /dev/null +++ b/database/factories/TeamInviteFactory.php @@ -0,0 +1,21 @@ + */ +class TeamInviteFactory extends Factory +{ + public function definition(): array + { + return [ + 'team_id' => TeamFactory::new(), + 'invited_by_user_id' => UserFactory::new(), + 'invited_email' => fake()->unique()->safeEmail(), + 'invite_token' => hash('sha256', Str::random(128)), + ]; + } +} diff --git a/resources/js/components/TeamSlug.vue b/resources/js/components/TeamSlug.vue index 53d7843..f640c0a 100644 --- a/resources/js/components/TeamSlug.vue +++ b/resources/js/components/TeamSlug.vue @@ -1,53 +1,70 @@ - diff --git a/resources/js/pages/team/Create.vue b/resources/js/pages/team/Create.vue index b156d4c..433c540 100644 --- a/resources/js/pages/team/Create.vue +++ b/resources/js/pages/team/Create.vue @@ -1,15 +1,15 @@