Skip to content

[5.x] Support Laravel precognition on user forms #8924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
23ba29e
Support precognition on user forms
ryanmitchell Nov 2, 2023
7037b4d
Bug fixes
ryanmitchell Nov 2, 2023
6471e34
Revert attributes in register form (but maybe the tests are wrong?)
ryanmitchell Nov 2, 2023
4979711
:beer:
ryanmitchell Nov 2, 2023
cb319d5
Bring in line with changes in main PR
ryanmitchell Nov 16, 2023
76d9c3e
:beer:
ryanmitchell Nov 16, 2023
3eba41f
Merge branch '4.x' into feature/precognition-for-user-forms
ryanmitchell Dec 1, 2023
367a9fe
Merge branch '4.x' into feature/precognition-for-user-forms
jasonvarga Jan 31, 2024
1fc8ac2
Make things actually work and add test coverage
ryanmitchell Feb 1, 2024
3e7d8d9
Handle lack of withPrecognition on earlier laravel versions
ryanmitchell Feb 1, 2024
e39d822
:beer:
ryanmitchell Feb 1, 2024
dda85d7
Check wantsJson() too
ryanmitchell Feb 16, 2024
65475c3
Handle json responses
ryanmitchell Feb 16, 2024
61e0fae
Change approach to mirror https://github.com/statamic/cms/pull/9629
ryanmitchell Mar 1, 2024
cb539cd
Specify error bag name in user login
ryanmitchell Mar 14, 2024
0444e45
Merge branch '5.x' into feature/precognition-for-user-forms
ryanmitchell May 10, 2024
1f8c6f2
Fix uniqueuservalue validation
ryanmitchell May 10, 2024
0e62380
Fix UserProfileTest
ryanmitchell May 10, 2024
dadcbda
Merge branch '5.x' into feature/precognition-for-user-forms
jasonvarga May 14, 2024
d61eb6a
throw the exception
jasonvarga May 14, 2024
4ba88fb
Login form didnt have a custom error bag ...
jasonvarga May 14, 2024
3852b41
throw
jasonvarga May 14, 2024
04802e5
Test for json with precognition and defer to parent for validation
ryanmitchell May 15, 2024
566cd37
Return a validator provided by the fields instance. Need the password…
jasonvarga May 15, 2024
ea6e56b
dont need to return. skipping is enough.
jasonvarga May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
Route::post('protect/password', [PasswordProtectController::class, 'store'])->name('protect.password.store');

Route::group(['prefix' => 'auth', 'middleware' => [AuthGuard::class]], function () {
Route::post('login', [UserController::class, 'login'])->name('login');
Route::get('logout', [UserController::class, 'logout'])->name('logout');
Route::post('register', [UserController::class, 'register'])->name('register');
Route::post('profile', [UserController::class, 'profile'])->name('profile');
Route::post('password', [UserController::class, 'password'])->name('password');

Route::group(['middleware' => [HandlePrecognitiveRequests::class]], function () {
Route::post('login', [UserController::class, 'login'])->name('login');
Route::post('register', [UserController::class, 'register'])->name('register');
Route::post('profile', [UserController::class, 'profile'])->name('profile');
Route::post('password', [UserController::class, 'password'])->name('password');
});

Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Expand Down
170 changes: 63 additions & 107 deletions src/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,40 @@

namespace Statamic\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationException;
use Statamic\Auth\ThrottlesLogins;
use Statamic\Events\UserRegistered;
use Statamic\Events\UserRegistering;
use Statamic\Exceptions\SilentFormFailureException;
use Statamic\Exceptions\UnauthorizedHttpException;
use Statamic\Facades\User;
use Statamic\Rules\UniqueUserValue;
use Statamic\Http\Requests\UserLoginRequest;
use Statamic\Http\Requests\UserPasswordRequest;
use Statamic\Http\Requests\UserProfileRequest;
use Statamic\Http\Requests\UserRegisterRequest;

class UserController extends Controller
{
use ThrottlesLogins;

private $request;

public function login(Request $request)
public function login(UserLoginRequest $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required',
'password' => 'required',
]);
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);

if ($validator->passes()) {
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);

return $this->sendLockoutResponse($request);
}

if (Auth::attempt($request->only('email', 'password'), $request->has('remember'))) {
return redirect($request->input('_redirect', '/'))->withSuccess(__('Login successful.'));
}
return $this->sendLockoutResponse($request);
}

$this->incrementLoginAttempts($request);
if (Auth::attempt($request->only('email', 'password'), $request->has('remember'))) {
return redirect($request->input('_redirect', '/'))->withSuccess(__('Login successful.'));
}

$this->incrementLoginAttempts($request);

$errorResponse = $request->has('_error_redirect') ? redirect($request->input('_error_redirect')) : back();

return $errorResponse->withInput()->withErrors(__('Invalid credentials.'));
Expand All @@ -55,33 +48,12 @@ public function logout()
return redirect(request()->get('redirect', '/'));
}

public function register(Request $request)
public function register(UserRegisterRequest $request)
{
$blueprint = User::blueprint();

$fields = $blueprint->fields();
$values = $this->valuesWithoutAssetFields($fields, $request);
$fields = $fields->addValues($values);

$fieldRules = $fields->validator()->withRules([
'email' => ['required', 'email', new UniqueUserValue],
'password' => ['required', 'confirmed', Password::default()],
])->rules();

$validator = Validator::make($values, $fieldRules);

if ($validator->fails()) {
return $this->userRegistrationFailure($validator->errors());
}

$values = $fields->process()->values()
->only(array_keys($values))
->except(['email', 'groups', 'roles', 'super']);

$user = User::make()
->email($request->email)
->password($request->password)
->data($values);
->data($request->processedValues());

if ($roles = config('statamic.users.new_user_roles')) {
$user->explicitRoles($roles);
Expand All @@ -98,7 +70,7 @@ public function register(Request $request)

throw_if(UserRegistering::dispatch($user) === false, new SilentFormFailureException);
} catch (ValidationException $e) {
return $this->userRegistrationFailure($e->errors());
return $this->userRegistrationFailure($e);
} catch (SilentFormFailureException $e) {
return $this->userRegistrationSuccess(true);
}
Expand All @@ -112,63 +84,31 @@ public function register(Request $request)
return $this->userRegistrationSuccess();
}

public function profile(Request $request)
public function profile(UserProfileRequest $request)
{
throw_unless($user = User::current(), new UnauthorizedHttpException(403));

$blueprint = User::blueprint();

$fields = $blueprint->fields();
$values = $this->valuesWithoutAssetFields($fields, $request);
$fields = $fields->addValues($values);

try {
$fields
->validator()
->withRules(['email' => ['required', 'email', new UniqueUserValue(except: $user->id())]])
->withReplacements(['id' => $user->id()])
->validate();
} catch (ValidationException $e) {
return $this->userProfileFailure($e->validator->errors());
}

$values = $fields->process()->values()
->only(array_keys($values))
->except(['email', 'password', 'groups', 'roles', 'super']);
$user = User::current();

if ($request->email) {
$user->email($request->email);
}
foreach ($values as $key => $value) {

foreach ($request->processedValues() as $key => $value) {
$user->set($key, $value);
}

$user->save();

session()->flash('user.profile.success', __('Update successful.'));

return $this->userProfileSuccess();
}

public function password(Request $request)
public function password(UserPasswordRequest $request)
{
throw_unless($user = User::current(), new UnauthorizedHttpException(403));

$validator = Validator::make($request->all(), [
'current_password' => ['required', 'current_password'],
'password' => ['required', 'confirmed', Password::default()],
]);

if ($validator->fails()) {
return $this->userPasswordFailure($validator->errors());
}
$user = User::current();

$user->password($request->password);

$user->save();

session()->flash('user.password.success', __('Change successful.'));

return $this->userPasswordSuccess();
}

Expand All @@ -177,8 +117,23 @@ public function username()
return 'email';
}

private function userRegistrationFailure($errors = null)
private function userRegistrationFailure($validator)
{
$errors = $validator->errors();

if (request()->ajax()) {
return response([
'errors' => (new MessageBag($errors))->all(),
'error' => collect($errors)->map(function ($errors, $field) {
return $errors[0];
})->all(),
], 400);
}

if (request()->wantsJson()) {
return (new ValidationException($validator))->errorBag(new MessageBag($errors));
}

$errorResponse = request()->has('_error_redirect') ? redirect(request()->input('_error_redirect')) : back();

return $errorResponse->withInput()->withErrors($errors, 'user.register');
Expand All @@ -188,50 +143,51 @@ private function userRegistrationSuccess(bool $silentFailure = false)
{
$response = request()->has('_redirect') ? redirect(request()->get('_redirect')) : back();

if (request()->ajax() || request()->wantsJson()) {
return response([
'success' => true,
'user_created' => ! $silentFailure,
'redirect' => $response->getTargetUrl(),
]);
}

session()->flash('user.register.success', __('Registration successful.'));
session()->flash('user.register.user_created', ! $silentFailure);

return $response;
}

private function userProfileFailure($errors = null)
{
$errorResponse = request()->has('_error_redirect') ? redirect(request()->input('_error_redirect')) : back();

return $errorResponse->withInput()->withErrors($errors, 'user.profile');
}

private function userProfileSuccess(bool $silentFailure = false)
{
$response = request()->has('_redirect') ? redirect(request()->get('_redirect')) : back();

if (request()->ajax() || request()->wantsJson()) {
return response([
'success' => true,
'user_updated' => ! $silentFailure,
'redirect' => $response->getTargetUrl(),
]);
}

session()->flash('user.profile.success', __('Update successful.'));

return $response;
}

private function userPasswordFailure($errors = null)
{
$errorResponse = request()->has('_error_redirect') ? redirect(request()->input('_error_redirect')) : back();

return $errorResponse->withInput()->withErrors($errors, 'user.password');
}

private function userPasswordSuccess(bool $silentFailure = false)
{
$response = request()->has('_redirect') ? redirect(request()->get('_redirect')) : back();

if (request()->ajax() || request()->wantsJson()) {
return response([
'success' => true,
'password_updated' => ! $silentFailure,
'redirect' => $response->getTargetUrl(),
]);
}

session()->flash('user.password.success', __('Change successful.'));

return $response;
}

private function valuesWithoutAssetFields($fields, $request)
{
$assets = $fields->all()
->filter(fn ($field) => $field->fieldtype()->handle() === 'assets')
->keys()->all();

return $request->except($assets);
}
}
59 changes: 59 additions & 0 deletions src/Http/Requests/UserLoginRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Statamic\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Traits\Localizable;
use Illuminate\Validation\ValidationException;
use Statamic\Facades\Site;

class UserLoginRequest extends FormRequest
{
use Localizable;

public function authorize(): bool
{
return true;
}

public function rules(): array
{
return [
'email' => 'required',
'password' => 'required',
];
}

protected function failedValidation(Validator $validator)
{
if ($this->isPrecognitive() || $this->wantsJson()) {
throw (new ValidationException($validator))->errorBag($this->errorBag);
}

if ($this->ajax()) {
$errors = $validator->errors();

$response = response([
'errors' => $errors->all(),
'error' => collect($errors->messages())->map(function ($errors, $field) {
return $errors[0];
})->all(),
], 400);

throw (new ValidationException($validator, $response));
}

$errorResponse = $this->has('_error_redirect') ? redirect($this->input('_error_redirect')) : back();

throw (new ValidationException($validator, $errorResponse->withInput()->withErrors(__('Invalid credentials.'))));
}

public function validateResolved()
{
$site = Site::findByUrl(URL::previous()) ?? Site::default();

return $this->withLocale($site->lang(), fn () => parent::validateResolved());
}
}
Loading
Loading