Skip to content
Merged
6 changes: 3 additions & 3 deletions app/Concerns/PasswordValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@

namespace App\Concerns;

use Illuminate\Validation\Rule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Validation\Rules\Password;

trait PasswordValidationRules
{
/** @return array<int, Rule|array<mixed>|string> */
/** @return array<int, ValidationRule|array<mixed>|string> */
protected function passwordRules(): array
{
return ['required', 'string', Password::default(), 'confirmed'];
}

/** @return array<int, Rule|array<mixed>|string> */
/** @return array<int, ValidationRule|array<mixed>|string> */
protected function currentPasswordRules(): array
{
return ['required', 'string', 'current_password'];
Expand Down
7 changes: 4 additions & 3 deletions app/Concerns/ProfileValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
namespace App\Concerns;

use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Validation\Rule;

trait ProfileValidationRules
{
/** @return array<string, array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>> */
/** @return array<string, array<int, ValidationRule|array<mixed>|string>> */
protected function profileRules(?int $userId = null): array
{
return [
Expand All @@ -18,13 +19,13 @@ protected function profileRules(?int $userId = null): array
];
}

/** @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string> */
/** @return array<int, ValidationRule|array<mixed>|string> */
protected function nameRules(): array
{
return ['required', 'string', 'max:255'];
}

/** @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string> */
/** @return array<int, ValidationRule|array<mixed>|string> */
protected function emailRules(?int $userId = null): array
{
return [
Expand Down
31 changes: 31 additions & 0 deletions app/Http/Responses/Concerns/RedirectsToCurrentTeam.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace App\Http\Responses\Concerns;

use App\Models\Team;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\URL;

trait RedirectsToCurrentTeam
{
protected function redirectPathForCurrentTeam(Request $request, string $redirect): string
{
$team = $this->currentTeam($request);

URL::defaults(['current_team' => $team->slug]);

return "/{$team->slug}{$redirect}";
}

protected function currentTeam(Request $request): Team
{
$user = $request->user();
$team = $user?->currentTeam ?? $user?->personalTeam();

abort_unless($team, 403);

return $team;
}
}
14 changes: 5 additions & 9 deletions app/Http/Responses/LoginResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@

namespace App\Http\Responses;

use App\Http\Responses\Concerns\RedirectsToCurrentTeam;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Fortify;
use Symfony\Component\HttpFoundation\Response;

class LoginResponse implements LoginResponseContract
{
use RedirectsToCurrentTeam;

public function toResponse($request): Response
{
$user = $request->user();
$team = $user?->currentTeam ?? $user?->personalTeam();

abort_unless($team, 403);

URL::defaults(['current_team' => $team->slug]);

return $request->wantsJson()
? new JsonResponse(['two_factor' => false], 200)
: redirect()->intended(route('dashboard'));
: redirect()->intended($this->redirectPathForCurrentTeam($request, Fortify::redirects('login')));
}
}
25 changes: 25 additions & 0 deletions app/Http/Responses/PasskeyLoginResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace App\Http\Responses;

use App\Http\Responses\Concerns\RedirectsToCurrentTeam;
use Illuminate\Http\JsonResponse;
use Laravel\Fortify\Fortify;
use Laravel\Passkeys\Contracts\PasskeyLoginResponse as PasskeyLoginResponseContract;
use Symfony\Component\HttpFoundation\Response;

class PasskeyLoginResponse implements PasskeyLoginResponseContract
{
use RedirectsToCurrentTeam;

public function toResponse($request): Response
{
$redirect = $this->redirectPathForCurrentTeam($request, Fortify::redirects('login'));

return $request->wantsJson()
? new JsonResponse(['redirect' => redirect()->intended($redirect)->getTargetUrl()], 200)
: redirect()->intended($redirect);
}
}
14 changes: 5 additions & 9 deletions app/Http/Responses/RegisterResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@

namespace App\Http\Responses;

use App\Http\Responses\Concerns\RedirectsToCurrentTeam;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
use Laravel\Fortify\Fortify;
use Symfony\Component\HttpFoundation\Response;

class RegisterResponse implements RegisterResponseContract
{
use RedirectsToCurrentTeam;

public function toResponse($request): Response
{
$user = $request->user();
$team = $user?->currentTeam ?? $user?->personalTeam();

abort_unless($team, 403);

URL::defaults(['current_team' => $team->slug]);

return $request->wantsJson()
? new JsonResponse(['two_factor' => false], 201)
: redirect()->intended(route('dashboard'));
: redirect()->intended($this->redirectPathForCurrentTeam($request, Fortify::redirects('register')));
}
}
14 changes: 5 additions & 9 deletions app/Http/Responses/TwoFactorLoginResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@

namespace App\Http\Responses;

use App\Http\Responses\Concerns\RedirectsToCurrentTeam;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
use Laravel\Fortify\Fortify;
use Symfony\Component\HttpFoundation\Response;

class TwoFactorLoginResponse implements TwoFactorLoginResponseContract
{
use RedirectsToCurrentTeam;

public function toResponse($request): Response
{
$user = $request->user();
$team = $user?->currentTeam ?? $user?->personalTeam();

abort_unless($team, 403);

URL::defaults(['current_team' => $team->slug]);

return $request->wantsJson()
? new JsonResponse(['two_factor' => false], 200)
: redirect()->intended(route('dashboard'));
: redirect()->intended($this->redirectPathForCurrentTeam($request, Fortify::redirects('login')));
}
}
14 changes: 5 additions & 9 deletions app/Http/Responses/VerifyEmailResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@

namespace App\Http\Responses;

use App\Http\Responses\Concerns\RedirectsToCurrentTeam;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\Contracts\VerifyEmailResponse as VerifyEmailResponseContract;
use Laravel\Fortify\Fortify;
use Symfony\Component\HttpFoundation\Response;

class VerifyEmailResponse implements VerifyEmailResponseContract
{
use RedirectsToCurrentTeam;

public function toResponse($request): Response
{
$user = $request->user();
$team = $user?->currentTeam ?? $user?->personalTeam();

abort_unless($team, 403);

URL::defaults(['current_team' => $team->slug]);

return $request->wantsJson()
? new JsonResponse('', 204)
: redirect()->intended(route('dashboard') . '?verified=1');
: redirect()->intended($this->redirectPathForCurrentTeam($request, Fortify::redirects('email-verification')) . '?verified=1');
}
}
5 changes: 4 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;

#[Fillable(['name', 'email', 'password', 'current_team_id'])]
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
class User extends Authenticatable
class User extends Authenticatable implements PasskeyUser
{
use HasApiTokens;
/** @use HasFactory<UserFactory> */
use HasFactory;
use HasTeams;
use Notifiable;
use PasskeyAuthenticatable;
use TwoFactorAuthenticatable;

public function initials(): string
Expand Down
11 changes: 11 additions & 0 deletions app/Providers/FortifyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Http\Responses\LoginResponse;
use App\Http\Responses\PasskeyLoginResponse;
use App\Http\Responses\RegisterResponse;
use App\Http\Responses\TwoFactorLoginResponse;
use App\Http\Responses\VerifyEmailResponse;
Expand All @@ -20,12 +21,14 @@
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
use Laravel\Fortify\Contracts\VerifyEmailResponse as VerifyEmailResponseContract;
use Laravel\Fortify\Fortify;
use Laravel\Passkeys\Contracts\PasskeyLoginResponse as PasskeyLoginResponseContract;

class FortifyServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(LoginResponseContract::class, LoginResponse::class);
$this->app->singleton(PasskeyLoginResponseContract::class, PasskeyLoginResponse::class);
$this->app->singleton(RegisterResponseContract::class, RegisterResponse::class);
$this->app->singleton(TwoFactorLoginResponseContract::class, TwoFactorLoginResponse::class);
$this->app->singleton(VerifyEmailResponseContract::class, VerifyEmailResponse::class);
Expand Down Expand Up @@ -66,5 +69,13 @@ private function configureRateLimiting(): void

return Limit::perMinute(5)->by($throttleKey);
});

RateLimiter::for('passkeys', function (Request $request) {
$credentialId = $request->input('credential.id');

return Limit::perMinute(10)->by(
($credentialId ?: $request->session()->getId()) . '|' . $request->ip(),
);
});
}
}
9 changes: 5 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@
"bacon/bacon-qr-code": "^3.0",
"bentonow/bento-laravel-sdk": "^1.3",
"dedoc/scramble": "^0.13.14",
"laravel/fortify": "^1.30",
"laravel/framework": "^13.0",
"laravel/chisel": "^0.1.0",
"laravel/fortify": "^1.37.2",
"laravel/framework": "^13.7",
"laravel/nightwatch": "^1.24",
"laravel/pennant": "^1.23",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^3.0",
"league/flysystem-aws-s3-v3": "^3.0",
"livewire/flux": "^2.9.0",
"livewire/flux": "^2.13.1",
"livewire/flux-pro": "^2.13",
"livewire/livewire": "^4.0",
"livewire/livewire": "^4.1",
"nunomaduro/essentials": "^1.2",
"spatie/simple-excel": "^3.9"
},
Expand Down
54 changes: 53 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading