diff --git a/routes/web.php b/routes/web.php index fda56e4955b..4a063c014b1 100755 --- a/routes/web.php +++ b/routes/web.php @@ -15,6 +15,8 @@ Route::post('login', 'UserController@login')->name('login'); Route::get('logout', 'UserController@logout')->name('logout'); Route::post('register', 'UserController@register')->name('register'); + Route::post('profile', 'UserController@profile')->name('profile'); + Route::post('password', 'UserController@password')->name('password'); Route::post('password/email', 'ForgotPasswordController@sendResetLinkEmail')->name('password.email'); Route::get('password/reset/{token}', 'ResetPasswordController@showResetForm')->name('password.reset'); diff --git a/src/Auth/UserTags.php b/src/Auth/UserTags.php index 0a78c7b0836..b718400e4c9 100644 --- a/src/Auth/UserTags.php +++ b/src/Auth/UserTags.php @@ -177,6 +177,86 @@ public function registerForm() return $html; } + /** + * Output a profile form. + * + * Maps to {{ user:profile_form }} + * + * @return string + */ + public function profileForm() + { + if (session()->has('status')) { + return $this->parse(['success' => true]); + } + + $data = $this->getFormSession('user.profile'); + + $data['fields'] = $this->getProfileFields(); + + $knownParams = ['redirect', 'error_redirect', 'allow_request_redirect']; + + $html = $this->formOpen(route('statamic.profile'), 'POST', $knownParams); + + $params = []; + + if ($redirect = $this->getRedirectUrl()) { + $params['redirect'] = $this->parseRedirect($redirect); + } + + if ($errorRedirect = $this->getErrorRedirectUrl()) { + $params['error_redirect'] = $this->parseRedirect($errorRedirect); + } + + $html .= $this->formMetaFields($params); + + $html .= $this->parse($data); + + $html .= $this->formClose(); + + return $html; + } + + /** + * Output a password change form. + * + * Maps to {{ user:password_form }} + * + * @return string + */ + public function passwordForm() + { + if (session()->has('status')) { + return $this->parse(['success' => true]); + } + + $data = $this->getFormSession('user.password'); + + $data['fields'] = $this->getPasswordFields(); + + $knownParams = ['redirect', 'error_redirect', 'allow_request_redirect']; + + $html = $this->formOpen(route('statamic.password'), 'POST', $knownParams); + + $params = []; + + if ($redirect = $this->getRedirectUrl()) { + $params['redirect'] = $this->parseRedirect($redirect); + } + + if ($errorRedirect = $this->getErrorRedirectUrl()) { + $params['error_redirect'] = $this->parseRedirect($errorRedirect); + } + + $html .= $this->formMetaFields($params); + + $html .= $this->parse($data); + + $html .= $this->formClose(); + + return $html; + } + /** * Alias of {{ user:register_form }}. * @@ -562,4 +642,59 @@ protected function getAdditionalRegistrationFields() ->values() ->all(); } + + /** + * Get fields with extra data for looping over and rendering. + * + * @return array + */ + protected function getProfileFields() + { + $user = User::current(); + + $values = $user + ? $user->data()->merge(['email' => $user->email()])->all() + : []; + + return User::blueprint()->fields()->addValues($values)->preProcess()->all() + ->reject(function ($field) { + return in_array($field->handle(), ['password', 'password_confirmation', 'roles', 'groups']) + || $field->fieldtype()->handle() === 'assets'; + }) + ->map(function ($field) { + return $this->getRenderableField($field, 'user.profile'); + }) + ->values() + ->all(); + } + + /** + * Get fields with extra data for looping over and rendering. + * + * @return array + */ + protected function getPasswordFields() + { + return collect() + ->put('current_password', new Field('current_password', [ + 'type' => 'text', + 'input_type' => 'password', + 'display' => __('Current Password'), + ])) + ->put('password', new Field('password', [ + 'type' => 'text', + 'input_type' => 'password', + 'display' => __('Password'), + ])) + ->put('password_confirmation', new Field('password_confirmation', [ + 'type' => 'text', + 'input_type' => 'password', + 'display' => __('Password Confirmation'), + ])) + ->map(function ($field) { + return $this->getRenderableField($field, 'user.password'); + }) + ->values() + ->all(); + } } diff --git a/src/Http/Controllers/UserController.php b/src/Http/Controllers/UserController.php index d98013953fe..3ae3d9356ca 100644 --- a/src/Http/Controllers/UserController.php +++ b/src/Http/Controllers/UserController.php @@ -11,6 +11,7 @@ use Statamic\Events\UserRegistered; use Statamic\Events\UserRegistering; use Statamic\Exceptions\SilentFormFailureException; +use Statamic\Exceptions\UnauthorizedHttpException; use Statamic\Facades\User; class UserController extends Controller @@ -56,20 +57,24 @@ public function register(Request $request) { $blueprint = User::blueprint(); - $fields = $blueprint->fields()->addValues($request->all()); + $fields = $blueprint->fields(); + $values = $this->valuesWithoutAssetFields($fields, $request); + $fields = $fields->addValues($values); $fieldRules = $fields->validator()->withRules([ 'email' => ['required', 'email', 'unique_user_value'], 'password' => ['required', 'confirmed', PasswordDefaults::rules()], ])->rules(); - $validator = Validator::make($request->all(), $fieldRules); + $validator = Validator::make($values, $fieldRules); if ($validator->fails()) { return $this->userRegistrationFailure($validator->errors()); } - $values = $fields->process()->values()->except(['email', 'groups', 'roles']); + $values = $fields->process()->values() + ->only(array_keys($values)) + ->except(['email', 'groups', 'roles', 'super']); $user = User::make() ->email($request->email) @@ -101,6 +106,66 @@ public function register(Request $request) return $this->userRegistrationSuccess(); } + public function profile(Request $request) + { + throw_unless($user = User::current(), new UnauthorizedHttpException(403)); + + $blueprint = User::blueprint(); + + $fields = $blueprint->fields(); + $values = $this->valuesWithoutAssetFields($fields, $request); + $fields = $fields->addValues($values); + + $fieldRules = $fields->validator()->withRules([ + 'email' => ['required', 'email', 'unique_user_value:'.$user->id()], + ])->rules(); + + $validator = Validator::make($values, $fieldRules); + + if ($validator->fails()) { + return $this->userProfileFailure($validator->errors()); + } + + $values = $fields->process()->values() + ->only(array_keys($values)) + ->except(['email', 'password', 'groups', 'roles', 'super']); + + if ($request->email) { + $user->email($request->email); + } + foreach ($values as $key => $value) { + $user->set($key, $value); + } + + $user->save(); + + session()->flash('user.profile.success', __('Update successful.')); + + return $this->userProfileSuccess(); + } + + public function password(Request $request) + { + throw_unless($user = User::current(), new UnauthorizedHttpException(403)); + + $validator = Validator::make($request->all(), [ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', 'confirmed', PasswordDefaults::rules()], + ]); + + if ($validator->fails()) { + return $this->userPasswordFailure($validator->errors()); + } + + $user->password($request->password); + + $user->save(); + + session()->flash('user.password.success', __('Change successful.')); + + return $this->userPasswordSuccess(); + } + public function username() { return 'email'; @@ -122,4 +187,45 @@ private function userRegistrationSuccess(bool $silentFailure = false) 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(); + + 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(); + + 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); + } } diff --git a/src/Tags/Concerns/RendersForms.php b/src/Tags/Concerns/RendersForms.php index 345191628d5..bdf16472952 100644 --- a/src/Tags/Concerns/RendersForms.php +++ b/src/Tags/Concerns/RendersForms.php @@ -134,6 +134,7 @@ protected function getRenderableField($field, $errorBag = 'default', $manipulate $data = array_merge($field->toArray(), [ 'error' => $errors->first($field->handle()) ?: null, + 'default' => $field->value() ?? $field->defaultValue(), 'old' => old($field->handle()), 'value' => $value, ]); diff --git a/tests/Tags/User/PasswordFormTest.php b/tests/Tags/User/PasswordFormTest.php new file mode 100644 index 00000000000..94074785fee --- /dev/null +++ b/tests/Tags/User/PasswordFormTest.php @@ -0,0 +1,294 @@ +actingAs(User::make()->password('mypassword')->save()); + + $output = $this->tag('{{ user:password_form }}{{ /user:password_form }}'); + + $this->assertStringStartsWith('
', $output); + $this->assertStringContainsString('', $output); + $this->assertStringEndsWith('
', $output); + } + + /** @test */ + public function it_renders_form_with_params() + { + $this->actingAs(User::make()->password('mypassword')->save()); + + $output = $this->tag('{{ user:password_form redirect="/submitted" error_redirect="/errors" class="form" id="form" }}{{ /user:password_form }}'); + + $this->assertStringStartsWith('
', $output); + $this->assertStringContainsString('', $output); + $this->assertStringContainsString('', $output); + } + + /** @test */ + public function it_renders_form_with_redirects_to_anchor() + { + $this->actingAs(User::make()->password('mypassword')->save()); + + $output = $this->tag('{{ user:password_form redirect="#form" error_redirect="#form" }}{{ /user:password_form }}'); + + $this->assertStringContainsString('', $output); + $this->assertStringContainsString('', $output); + } + + /** @test */ + public function it_renders_form_with_fields_array() + { + $this->actingAs(User::make() + ->email('test@example.com') + ->data(['name' => 'Test User']) + ->save()); + + $output = $this->normalizeHtml($this->tag(<<<'EOT' +{{ user:password_form }} + {{ fields }} + {{ field }} + {{ /fields }} +{{ /user:password_form }} +EOT +)); + + preg_match_all('/