diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php deleted file mode 100644 index 1fa95d1e7b..0000000000 --- a/app/Actions/Fortify/ResetUserPassword.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 - */ - -namespace App\Actions\Fortify; - -use App\Models\Group; -use App\Models\User; -use App\Services\Unit3dAnnounce; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Validator; -use Illuminate\Validation\ValidationException; -use Laravel\Fortify\Contracts\ResetsUserPasswords; - -class ResetUserPassword implements ResetsUserPasswords -{ - use PasswordValidationRules; - - /** - * Validate and reset the user's forgotten password. - * - * @param array $input - * @throws ValidationException - */ - public function reset(User $user, array $input): void - { - Validator::make($input, [ - 'password' => $this->passwordRules(), - ])->validate(); - - $user->forceFill([ - 'password' => Hash::make($input['password']), - ]); - - $validatingGroup = cache()->rememberForever('validating_group', fn () => Group::query()->where('slug', '=', 'validating')->pluck('id')); - $memberGroup = cache()->rememberForever('member_group', fn () => Group::query()->where('slug', '=', 'user')->pluck('id')); - - if ($user->group_id === $validatingGroup[0]) { - $user->group_id = $memberGroup[0]; - - cache()->forget('user:'.$user->passkey); - - Unit3dAnnounce::addUser($user); - } - - if (!$user->hasVerifiedEmail()) { - $user->markEmailAsVerified(); - } - - $user->save(); - - $user->passwordResetHistories()->create(); - } -} diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000000..f180aec1c2 --- /dev/null +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,84 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +namespace App\Http\Controllers\Auth; + +use App\Http\Controllers\Controller; +use App\Models\Group; +use App\Models\User; +use App\Services\Unit3dAnnounce; +use Illuminate\Auth\Events\PasswordReset; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Password; +use Illuminate\Validation\Rules\Password as RulesPassword; + +class NewPasswordController extends Controller +{ + /** + * Create new password. + */ + public function create(string $token): \Illuminate\Contracts\View\Factory|\Illuminate\View\View + { + return view('auth.reset-password', ['token' => $token]); + } + + /** + * Store a new password. + */ + public function store(Request $request): \Illuminate\Http\RedirectResponse + { + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => RulesPassword::min(12)->mixedCase()->letters()->numbers()->uncompromised(), + ]); + + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function (User $user, string $password): void { + $user->forceFill([ + 'password' => Hash::make($password) + ]); + + $validatingGroup = cache()->rememberForever('validating_group', fn () => Group::query()->where('slug', '=', 'validating')->pluck('id')); + $memberGroup = cache()->rememberForever('member_group', fn () => Group::query()->where('slug', '=', 'user')->pluck('id')); + + if ($user->group_id === $validatingGroup[0]) { + $user->group_id = $memberGroup[0]; + + cache()->forget('user:'.$user->passkey); + + Unit3dAnnounce::addUser($user); + } + + if (!$user->hasVerifiedEmail()) { + $user->markEmailAsVerified(); + } + + $user->save(); + + $user->passwordResetHistories()->create(); + + event(new PasswordReset($user)); + } + ); + + return $status === Password::PasswordReset + ? redirect()->route('login')->with('status', __($status)) + : back()->withErrors(['email' => [__($status)]]); + } +} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000000..bad3c60a52 --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,48 @@ + + * @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 + */ + +namespace App\Http\Controllers\Auth; + +use App\Http\Controllers\Controller; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Password; + +class PasswordResetLinkController extends Controller +{ + /** + * Show form to submit to receive new password reset link. + */ + public function create(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View + { + return view('auth.forgot-password'); + } + + /** + * Send a new password reset link. + */ + public function store(Request $request): \Illuminate\Http\RedirectResponse + { + $request->validate(['email' => 'required|email']); + + $_status = Password::sendResetLink( + $request->only('email') + ); + + // Return successful status regardless of if the user exists or if they're throttled or not. + // (Account enumeration) + return back()->with(['status' => __('passwords.sent')]); + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 8d77fc5fe8..c06342f8d8 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -17,7 +17,6 @@ namespace App\Providers; use App\Actions\Fortify\CreateNewUser; -use App\Actions\Fortify\ResetUserPassword; use App\Actions\Fortify\UpdateUserPassword; use App\Actions\Fortify\UpdateUserProfileInformation; use App\Models\FailedLoginAttempt; @@ -28,7 +27,6 @@ use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rule; @@ -37,8 +35,6 @@ use Laravel\Fortify\Contracts\RegisterViewResponse; use Laravel\Fortify\Contracts\VerifyEmailResponse; use Laravel\Fortify\Fortify; -use Laravel\Fortify\Http\Responses\FailedPasswordResetLinkRequestResponse; -use Laravel\Fortify\Http\Responses\SuccessfulPasswordResetLinkRequestResponse; class FortifyServiceProvider extends ServiceProvider { @@ -142,8 +138,6 @@ public function toResponse($request): \Illuminate\Http\RedirectResponse|\Illumin ->withErrors(trans('auth.activation-error')); } }); - - $this->app->extend(FailedPasswordResetLinkRequestResponse::class, fn () => new SuccessfulPasswordResetLinkRequestResponse(Password::RESET_LINK_SENT)); } /** @@ -155,15 +149,9 @@ public function boot(): void RateLimiter::for('fortify-login-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-login'.$request->ip())); RateLimiter::for('fortify-register-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-register'.$request->ip())); RateLimiter::for('fortify-register-post', fn (Request $request) => Limit::perMinute(5)->by('fortify-register'.$request->ip())); - RateLimiter::for('fortify-forgot-password-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-forgot-password'.$request->ip())); - RateLimiter::for('fortify-forgot-password-post', fn (Request $request) => Limit::perMinute(5)->by('fortify-forgot-password'.$request->ip())); - RateLimiter::for('fortify-reset-password-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-reset-password'.$request->ip())); - RateLimiter::for('fortify-reset-password-post', fn (Request $request) => Limit::perMinute(5)->by('fortify-reset-password'.$request->ip())); RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by('fortify-two-factor'.$request->session()->get('login.id'))); Fortify::loginView(fn () => view('auth.login')); - Fortify::requestPasswordResetLinkView(fn () => view('auth.passwords.email')); - Fortify::resetPasswordView(fn (Request $request) => view('auth.passwords.reset', ['request' => $request])); Fortify::confirmPasswordView(fn () => view('auth.confirm-password')); Fortify::twoFactorChallengeView(fn () => view('auth.two-factor-challenge')); Fortify::verifyEmailView(fn () => view('auth.verify-email')); @@ -171,7 +159,6 @@ public function boot(): void Fortify::createUsersUsing(CreateNewUser::class); Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); - Fortify::resetUserPasswordsUsing(ResetUserPassword::class); Fortify::authenticateUsing(function (Request $request): \Illuminate\Database\Eloquent\Model { $request->validate([ diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index e0759b5350..364a726975 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -91,6 +91,8 @@ protected function configureRateLimiting(): void RateLimiter::for('search', fn (Request $request): Limit => Limit::perMinute(100)->by('search:'.$request->user()->id)); RateLimiter::for('tmdb', fn (): Limit => Limit::perSecond(2)); RateLimiter::for('igdb', fn (): Limit => Limit::perSecond(2)); + RateLimiter::for('forgot-password', fn (Request $request) => Limit::perMinute(5)->by('forgot-password'.$request->ip())); + RateLimiter::for('reset-password', fn (Request $request) => Limit::perMinute(5)->by('reset-password'.$request->ip())); } protected function removeIndexPhpFromUrl(): void diff --git a/config/fortify.php b/config/fortify.php index e1dd588c69..fe89184b78 100644 --- a/config/fortify.php +++ b/config/fortify.php @@ -141,7 +141,6 @@ 'features' => [ Features::registration(), - Features::resetPasswords(), Features::emailVerification(), Features::updateProfileInformation(), Features::updatePasswords(), diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/forgot-password.blade.php similarity index 100% rename from resources/views/auth/passwords/email.blade.php rename to resources/views/auth/forgot-password.blade.php diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/reset-password.blade.php similarity index 99% rename from resources/views/auth/passwords/reset.blade.php rename to resources/views/auth/reset-password.blade.php index b5f74455e5..065dcfea75 100644 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -31,7 +31,7 @@ class="auth-form__form" action="{{ route('password.update') }}" > @csrf - + diff --git a/routes/web.php b/routes/web.php index 45ca08ab8a..58b050a8bb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,8 +17,6 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\URL; use Laravel\Fortify\Http\Controllers\AuthenticatedSessionController; -use Laravel\Fortify\Http\Controllers\NewPasswordController; -use Laravel\Fortify\Http\Controllers\PasswordResetLinkController; use Laravel\Fortify\Http\Controllers\RegisteredUserController; use Laravel\Fortify\RoutePath; @@ -58,22 +56,6 @@ Route::post(RoutePath::for('register', '/register'), [RegisteredUserController::class, 'store']) ->middleware(['throttle:'.config('fortify.limiters.fortify-register-post')]); - - Route::get(RoutePath::for('password.request', '/forgot-password'), [PasswordResetLinkController::class, 'create']) - ->middleware(['throttle:'.config('fortify.limiters.fortify-forgot-password-get')]) - ->name('password.request'); - - Route::get(RoutePath::for('password.reset', '/reset-password/{token}'), [NewPasswordController::class, 'create']) - ->middleware(['throttle:'.config('fortify.limiters.fortify-reset-password-get')]) - ->name('password.reset'); - - Route::post(RoutePath::for('password.email', '/forgot-password'), [PasswordResetLinkController::class, 'store']) - ->middleware(['throttle:'.config('fortify.limiters.fortify-forgot-password-post')]) - ->name('password.email'); - - Route::post(RoutePath::for('password.update', '/reset-password'), [NewPasswordController::class, 'store']) - ->middleware(['throttle:'.config('fortify.limiters.fortify-reset-password-post')]) - ->name('password.update'); }); /* @@ -86,6 +68,12 @@ Route::get('/application', [App\Http\Controllers\Auth\ApplicationController::class, 'create'])->name('application.create'); Route::post('/application', [App\Http\Controllers\Auth\ApplicationController::class, 'store'])->name('application.store'); + // Password resets + Route::get('/forgot-password', [App\Http\Controllers\Auth\PasswordResetLinkController::class, 'create'])->middleware('throttle:forgot-password')->name('password.request'); + Route::post('/forgot-password', [App\Http\Controllers\Auth\PasswordResetLinkController::class, 'store'])->middleware(['throttle:forgot-password'])->name('password.email'); + Route::get('/reset-password/{token}', [App\Http\Controllers\Auth\NewPasswordController::class, 'create'])->middleware('throttle:reset-password')->name('password.reset'); + Route::post('/reset-password', [App\Http\Controllers\Auth\NewPasswordController::class, 'store'])->middleware('throttle:reset-password')->name('password.update'); + // This redirect must be kept until all invite emails that use the old syntax have expired // Hack so that Fortify can be used (allows query parameters but not route parameters) Route::get('/register/{code?}', fn (string $code) => to_route('register', ['code' => $code]));