From 79f5be4170821644fe0597eb464853a5d411c2da Mon Sep 17 00:00:00 2001
From: Angelo Geant Gaviola <43493935+KiDxS@users.noreply.github.com>
Date: Tue, 14 May 2024 17:04:23 +0800
Subject: [PATCH 01/11] Fixed notification preferences URL in email
---
.../Notifications/Messages/BaseActivityNotification.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/Activity/Notifications/Messages/BaseActivityNotification.php b/app/Activity/Notifications/Messages/BaseActivityNotification.php
index ca86eb81b..067cd8f66 100644
--- a/app/Activity/Notifications/Messages/BaseActivityNotification.php
+++ b/app/Activity/Notifications/Messages/BaseActivityNotification.php
@@ -43,7 +43,7 @@ abstract class BaseActivityNotification extends MailNotification
protected function buildReasonFooterLine(LocaleDefinition $locale): LinkedMailMessageLine
{
return new LinkedMailMessageLine(
- url('/preferences/notifications'),
+ url('/my-account/notifications'),
$locale->trans('notifications.footer_reason'),
$locale->trans('notifications.footer_reason_link'),
);
From 38913288d81fb1c8fcc62873499a9f31f4fe46e4 Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Thu, 16 May 2024 14:15:26 +0100
Subject: [PATCH 02/11] Devdocs: Fixed visual theme system lang folder
reference
Made some other minor updates while there.
Fixes #4998
---
dev/docs/visual-theme-system.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/dev/docs/visual-theme-system.md b/dev/docs/visual-theme-system.md
index 47ee57c0e..6e7105a9e 100644
--- a/dev/docs/visual-theme-system.md
+++ b/dev/docs/visual-theme-system.md
@@ -6,14 +6,14 @@ This theme system itself is maintained and supported but usages of this system,
## Getting Started
-*[Video Guide](https://www.youtube.com/watch?v=gLy_2GBse48)*
+*[Video Guide](https://foss.video/w/ibNY6bGmKFV1tva3Jz4KfA)*
This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.
You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.
## Customizing View Files
-Content placed in your `themes//` folder will override the original view files found in the `resources/views` folder. These files are typically [Laravel Blade](https://laravel.com/docs/8.x/blade) files.
+Content placed in your `themes//` folder will override the original view files found in the `resources/views` folder. These files are typically [Laravel Blade](https://laravel.com/docs/10.x/blade) files.
As an example, I could override the `resources/views/books/parts/list-item.blade.php` file with my own template at the path `themes//books/parts/list-item.blade.php`.
## Customizing Icons
@@ -22,7 +22,7 @@ SVG files placed in a `themes//icons` folder will override any icons
## Customizing Text Content
-Folders with PHP translation files placed in a `themes//lang` folder will override translations defined within the `resources/lang` folder. Custom translations are merged with the original files so you only need to override the select translations you want to override, you don't need to copy the whole original file. Note that you'll need to include the language folder.
+Folders with PHP translation files placed in a `themes//lang` folder will override translations defined within the `lang` folder. Custom translations are merged with the original files, so you only need to override the select translations you want to override, you don't need to copy the whole original file. Note that you'll need to include the language folder.
As an example, Say I wanted to change 'Search' to 'Find'; Within a `themes//lang/en/common.php` file I'd set the following:
From 570af500f4155488265d5606a1b31314c1b0c77b Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Thu, 16 May 2024 14:54:44 +0100
Subject: [PATCH 03/11] WYSIWYG: Added justify cell range cleanup
To help override & gain control of setting text alignment in tables.
- Adds support of clearing "align" attributes in certain operations.
- Updates cell range action handling to dedupe execcommand handling.
- Adds clearing of additional alignment classes on direction control.
Closes #5011
---
resources/js/wysiwyg/fixes.js | 91 ++++++++++++++++++++++++-----------
1 file changed, 64 insertions(+), 27 deletions(-)
diff --git a/resources/js/wysiwyg/fixes.js b/resources/js/wysiwyg/fixes.js
index 61cace660..b6d627ff7 100644
--- a/resources/js/wysiwyg/fixes.js
+++ b/resources/js/wysiwyg/fixes.js
@@ -55,6 +55,19 @@ export function handleEmbedAlignmentChanges(editor) {
});
}
+/**
+ * Cleans up and removes text-alignment specific properties on all child elements.
+ * @param {HTMLElement} element
+ */
+function cleanChildAlignment(element) {
+ const alignedChildren = element.querySelectorAll('[align],[style*="text-align"],.align-center,.align-left,.align-right');
+ for (const child of alignedChildren) {
+ child.removeAttribute('align');
+ child.style.textAlign = null;
+ child.classList.remove('align-center', 'align-right', 'align-left');
+ }
+}
+
/**
* Cleans up the direction property for an element.
* Removes all inline direction control from child elements.
@@ -62,16 +75,23 @@ export function handleEmbedAlignmentChanges(editor) {
* @param {HTMLElement} element
*/
function cleanElementDirection(element) {
- const directionChildren = element.querySelectorAll('[dir],[style*="direction"],[style*="text-align"]');
+ const directionChildren = element.querySelectorAll('[dir],[style*="direction"]');
for (const child of directionChildren) {
child.removeAttribute('dir');
child.style.direction = null;
- child.style.textAlign = null;
}
+
+ cleanChildAlignment(element);
element.style.direction = null;
element.style.textAlign = null;
+ element.removeAttribute('align');
}
+/**
+ * @typedef {Function} TableCellHandler
+ * @param {HTMLTableCellElement} cell
+ */
+
/**
* This tracks table cell range selection, so we can apply custom handling where
* required to actions applied to such selections.
@@ -90,34 +110,51 @@ export function handleTableCellRangeEvents(editor) {
selectedCells = [];
});
- // TinyMCE does not seem to do a great job on clearing styles in complex
- // scenarios (like copied word content) when a range of table cells
- // are selected. Here we watch for clear formatting events, so some manual
- // cleanup can be performed.
- const attrsToRemove = ['class', 'style', 'width', 'height'];
- editor.on('ExecCommand', event => {
- if (event.command === 'RemoveFormat') {
- for (const cell of selectedCells) {
- for (const attr of attrsToRemove) {
- cell.removeAttribute(attr);
- }
+ /**
+ * @type {Object}
+ */
+ const actionByCommand = {
+ // TinyMCE does not seem to do a great job on clearing styles in complex
+ // scenarios (like copied word content) when a range of table cells
+ // are selected. Here we watch for clear formatting events, so some manual
+ // cleanup can be performed.
+ RemoveFormat: cell => {
+ const attrsToRemove = ['class', 'style', 'width', 'height', 'align'];
+ for (const attr of attrsToRemove) {
+ cell.removeAttribute(attr);
}
- }
- });
+ },
- // TinyMCE does not apply direction events to table cell range selections
- // so here we hastily patch in that ability by setting the direction ourselves
- // when a direction event is fired.
- editor.on('ExecCommand', event => {
- const command = event.command;
- if (command !== 'mceDirectionLTR' && command !== 'mceDirectionRTL') {
- return;
- }
-
- const dir = command === 'mceDirectionLTR' ? 'ltr' : 'rtl';
- for (const cell of selectedCells) {
- cell.setAttribute('dir', dir);
+ // TinyMCE does not apply direction events to table cell range selections
+ // so here we hastily patch in that ability by setting the direction ourselves
+ // when a direction event is fired.
+ mceDirectionLTR: cell => {
+ cell.setAttribute('dir', 'ltr');
cleanElementDirection(cell);
+ },
+ mceDirectionRTL: cell => {
+ cell.setAttribute('dir', 'rtl');
+ cleanElementDirection(cell);
+ },
+
+ // The "align" attribute can exist on table elements so this clears
+ // the attribute, and also clears common child alignment properties,
+ // when a text direction action is made for a table cell range.
+ JustifyLeft: cell => {
+ cell.removeAttribute('align');
+ cleanChildAlignment(cell);
+ },
+ JustifyRight: this.JustifyLeft,
+ JustifyCenter: this.JustifyLeft,
+ JustifyFull: this.JustifyLeft,
+ };
+
+ editor.on('ExecCommand', event => {
+ const action = actionByCommand[event.command];
+ if (action) {
+ for (const cell of selectedCells) {
+ action(cell);
+ }
}
});
}
From fc236f930b6836f49db1a584585581e70288af0a Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Sat, 18 May 2024 20:37:49 +0100
Subject: [PATCH 04/11] Dark Mode: Fixed setting labels missing dark mode
handling
Fixes #5018
---
resources/sass/_forms.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss
index e6d062ce8..a6f890e3c 100644
--- a/resources/sass/_forms.scss
+++ b/resources/sass/_forms.scss
@@ -321,6 +321,7 @@ input[type=color] {
}
}
.setting-list-label {
+ @include lightDark(color, #222, #DDD);
color: #222;
font-size: 1rem;
}
From 5651d2c43d3e5e1ba34b74d566b017c45921f069 Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Sat, 18 May 2024 20:40:26 +0100
Subject: [PATCH 05/11] Config: Reverted change to cache directory
Change made during Laravel 10 updates to align (Laravel made this change
much earlier in 5.x series) but it caused issues due to folder not
pre-existing and due to potentiall permission issues.
(CLI could create this during update, with non-compatible permissions
for webserver).
For #4999
---
app/Config/cache.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/Config/cache.php b/app/Config/cache.php
index 2ba16059a..b588437ff 100644
--- a/app/Config/cache.php
+++ b/app/Config/cache.php
@@ -53,8 +53,8 @@ return [
'file' => [
'driver' => 'file',
- 'path' => storage_path('framework/cache/data'),
- 'lock_path' => storage_path('framework/cache/data'),
+ 'path' => storage_path('framework/cache'),
+ 'lock_path' => storage_path('framework/cache'),
],
'memcached' => [
From 72c5141decb1d83910a81fd0c4bf77f4a463ce5c Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Sat, 18 May 2024 21:18:15 +0100
Subject: [PATCH 06/11] File Uploads: Added basic validation response
formatting
Tested via app-level validation file limit, and then also with nginx
file post limit.
For #4996
---
resources/js/components/dropzone.js | 5 +----
resources/js/services/http.js | 29 +++++++++++++++++++++++++++++
resources/sass/_components.scss | 4 ++++
3 files changed, 34 insertions(+), 4 deletions(-)
diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js
index 1cac09b4a..93e93a251 100644
--- a/resources/js/components/dropzone.js
+++ b/resources/js/components/dropzone.js
@@ -181,10 +181,7 @@ export class Dropzone extends Component {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
upload.markSuccess(component.successMessage);
} else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {
- const content = this.responseText;
- const data = content.startsWith('{') ? JSON.parse(content) : {message: content};
- const message = data?.message || data?.error || content;
- upload.markError(message);
+ upload.markError(window.$http.formatErrorResponseText(this.responseText));
}
},
});
diff --git a/resources/js/services/http.js b/resources/js/services/http.js
index 49d5b6df4..d95e4a59a 100644
--- a/resources/js/services/http.js
+++ b/resources/js/services/http.js
@@ -207,3 +207,32 @@ async function performDelete(url, data = null) {
}
export {performDelete as delete};
+
+/**
+ * Parse the response text for an error response to a user
+ * presentable string. Handles a range of errors responses including
+ * validation responses & server response text.
+ * @param {String} text
+ * @returns {String}
+ */
+export function formatErrorResponseText(text) {
+ const data = text.startsWith('{') ? JSON.parse(text) : {message: text};
+ if (!data) {
+ return text;
+ }
+
+ if (data.message || data.error) {
+ return data.message || data.error;
+ }
+
+ const values = Object.values(data);
+ const isValidation = values.every(val => {
+ return Array.isArray(val) || val.every(x => typeof x === 'string');
+ });
+
+ if (isValidation) {
+ return values.flat().join(' ');
+ }
+
+ return text;
+}
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index fc4ddeba4..18d1bc18f 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -321,6 +321,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
background-color: var(--color-primary);
transition: width ease-in-out 240ms;
}
+.dropzone-file-item-label {
+ line-height: 1.2;
+ margin-bottom: .2rem;
+}
.dropzone-file-item-label,
.dropzone-file-item-status {
align-items: center;
From 69af9e0dbdefd8c6c951e8afbe2bba141d454beb Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Mon, 20 May 2024 14:00:58 +0100
Subject: [PATCH 07/11] Routes: Added throttling to a range of auth-related
endpoints
Some already throttled in some means, but this adds a simple ip-based
non-request-specific layer to many endpoints.
Related to #4993
---
.../Controllers/ForgotPasswordController.php | 5 +++
.../Controllers/ResetPasswordController.php | 9 ++--
app/App/Providers/RouteServiceProvider.php | 4 ++
routes/web.php | 12 +++---
tests/Auth/RegistrationTest.php | 29 +++++++++++++
tests/Auth/ResetPasswordTest.php | 42 +++++++++++++++++++
tests/Auth/UserInviteTest.php | 20 +++++++++
7 files changed, 109 insertions(+), 12 deletions(-)
diff --git a/app/Access/Controllers/ForgotPasswordController.php b/app/Access/Controllers/ForgotPasswordController.php
index 86fbe8fa3..36dd97755 100644
--- a/app/Access/Controllers/ForgotPasswordController.php
+++ b/app/Access/Controllers/ForgotPasswordController.php
@@ -6,6 +6,7 @@ use BookStack\Activity\ActivityType;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
+use Illuminate\Support\Sleep;
class ForgotPasswordController extends Controller
{
@@ -32,6 +33,10 @@ class ForgotPasswordController extends Controller
'email' => ['required', 'email'],
]);
+ // Add random pause to the response to help avoid time-base sniffing
+ // of valid resets via slower email send handling.
+ Sleep::for(random_int(1000, 3000))->milliseconds();
+
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
diff --git a/app/Access/Controllers/ResetPasswordController.php b/app/Access/Controllers/ResetPasswordController.php
index a8a45dddf..3af65d17f 100644
--- a/app/Access/Controllers/ResetPasswordController.php
+++ b/app/Access/Controllers/ResetPasswordController.php
@@ -15,14 +15,11 @@ use Illuminate\Validation\Rules\Password as PasswordRule;
class ResetPasswordController extends Controller
{
- protected LoginService $loginService;
-
- public function __construct(LoginService $loginService)
- {
+ public function __construct(
+ protected LoginService $loginService
+ ) {
$this->middleware('guest');
$this->middleware('guard:standard');
-
- $this->loginService = $loginService;
}
/**
diff --git a/app/App/Providers/RouteServiceProvider.php b/app/App/Providers/RouteServiceProvider.php
index 3a155920e..d7c1cb737 100644
--- a/app/App/Providers/RouteServiceProvider.php
+++ b/app/App/Providers/RouteServiceProvider.php
@@ -81,5 +81,9 @@ class RouteServiceProvider extends ServiceProvider
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
+
+ RateLimiter::for('public', function (Request $request) {
+ return Limit::perMinute(10)->by($request->ip());
+ });
}
}
diff --git a/routes/web.php b/routes/web.php
index 03595288f..58b8f4e54 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -317,8 +317,8 @@ Route::get('/register/confirm', [AccessControllers\ConfirmEmailController::class
Route::get('/register/confirm/awaiting', [AccessControllers\ConfirmEmailController::class, 'showAwaiting']);
Route::post('/register/confirm/resend', [AccessControllers\ConfirmEmailController::class, 'resend']);
Route::get('/register/confirm/{token}', [AccessControllers\ConfirmEmailController::class, 'showAcceptForm']);
-Route::post('/register/confirm/accept', [AccessControllers\ConfirmEmailController::class, 'confirm']);
-Route::post('/register', [AccessControllers\RegisterController::class, 'postRegister']);
+Route::post('/register/confirm/accept', [AccessControllers\ConfirmEmailController::class, 'confirm'])->middleware('throttle:public');
+Route::post('/register', [AccessControllers\RegisterController::class, 'postRegister'])->middleware('throttle:public');
// SAML routes
Route::post('/saml2/login', [AccessControllers\Saml2Controller::class, 'login']);
@@ -338,16 +338,16 @@ Route::get('/oidc/callback', [AccessControllers\OidcController::class, 'callback
Route::post('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
// User invitation routes
-Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);
-Route::post('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'setPassword']);
+Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword'])->middleware('throttle:public');
+Route::post('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'setPassword'])->middleware('throttle:public');
// Password reset link request routes
Route::get('/password/email', [AccessControllers\ForgotPasswordController::class, 'showLinkRequestForm']);
-Route::post('/password/email', [AccessControllers\ForgotPasswordController::class, 'sendResetLinkEmail']);
+Route::post('/password/email', [AccessControllers\ForgotPasswordController::class, 'sendResetLinkEmail'])->middleware('throttle:public');
// Password reset routes
Route::get('/password/reset/{token}', [AccessControllers\ResetPasswordController::class, 'showResetForm']);
-Route::post('/password/reset', [AccessControllers\ResetPasswordController::class, 'reset']);
+Route::post('/password/reset', [AccessControllers\ResetPasswordController::class, 'reset'])->middleware('throttle:public');
// Metadata routes
Route::view('/help/wysiwyg', 'help.wysiwyg');
diff --git a/tests/Auth/RegistrationTest.php b/tests/Auth/RegistrationTest.php
index 60ae17573..42d1120e4 100644
--- a/tests/Auth/RegistrationTest.php
+++ b/tests/Auth/RegistrationTest.php
@@ -203,4 +203,33 @@ class RegistrationTest extends TestCase
$resp = $this->followRedirects($resp);
$this->withHtml($resp)->assertElementExists('form input[name="username"].text-neg');
}
+
+ public function test_registration_endpoint_throttled()
+ {
+ $this->setSettings(['registration-enabled' => 'true']);
+
+ for ($i = 0; $i < 11; $i++) {
+ $response = $this->post('/register/', [
+ 'name' => "Barry{$i}",
+ 'email' => "barry{$i}@example.com",
+ 'password' => "barryIsTheBest{$i}",
+ ]);
+ auth()->logout();
+ }
+
+ $response->assertStatus(429);
+ }
+
+ public function test_registration_confirmation_throttled()
+ {
+ $this->setSettings(['registration-enabled' => 'true']);
+
+ for ($i = 0; $i < 11; $i++) {
+ $response = $this->post('/register/confirm/accept', [
+ 'token' => "token{$i}",
+ ]);
+ }
+
+ $response->assertStatus(429);
+ }
}
diff --git a/tests/Auth/ResetPasswordTest.php b/tests/Auth/ResetPasswordTest.php
index d2af17b9c..026f8c5ba 100644
--- a/tests/Auth/ResetPasswordTest.php
+++ b/tests/Auth/ResetPasswordTest.php
@@ -4,11 +4,19 @@ namespace Tests\Auth;
use BookStack\Access\Notifications\ResetPasswordNotification;
use BookStack\Users\Models\User;
+use Carbon\CarbonInterval;
use Illuminate\Support\Facades\Notification;
+use Illuminate\Support\Sleep;
use Tests\TestCase;
class ResetPasswordTest extends TestCase
{
+ protected function setUp(): void
+ {
+ parent::setUp();
+ Sleep::fake();
+ }
+
public function test_reset_flow()
{
Notification::fake();
@@ -75,6 +83,17 @@ class ResetPasswordTest extends TestCase
->assertSee('The password reset token is invalid for this email address.');
}
+ public function test_reset_request_with_not_found_user_still_has_delay()
+ {
+ $this->followingRedirects()->post('/password/email', [
+ 'email' => 'barrynotfoundrandomuser@example.com',
+ ]);
+
+ Sleep::assertSlept(function (CarbonInterval $duration): bool {
+ return $duration->totalMilliseconds > 999;
+ }, 1);
+ }
+
public function test_reset_page_shows_sign_links()
{
$this->setSettings(['registration-enabled' => 'true']);
@@ -98,4 +117,27 @@ class ResetPasswordTest extends TestCase
Notification::assertSentTimes(ResetPasswordNotification::class, 1);
$resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
}
+
+ public function test_reset_request_with_not_found_user_is_throttled()
+ {
+ for ($i = 0; $i < 11; $i++) {
+ $response = $this->post('/password/email', [
+ 'email' => 'barrynotfoundrandomuser@example.com',
+ ]);
+ }
+
+ $response->assertStatus(429);
+ }
+
+ public function test_reset_call_is_throttled()
+ {
+ for ($i = 0; $i < 11; $i++) {
+ $response = $this->post('/password/reset', [
+ 'email' => "arandomuser{$i}@example.com",
+ 'token' => "randomtoken{$i}",
+ ]);
+ }
+
+ $response->assertStatus(429);
+ }
}
diff --git a/tests/Auth/UserInviteTest.php b/tests/Auth/UserInviteTest.php
index a9dee0007..434de6aa6 100644
--- a/tests/Auth/UserInviteTest.php
+++ b/tests/Auth/UserInviteTest.php
@@ -137,4 +137,24 @@ class UserInviteTest extends TestCase
$setPasswordPageResp->assertRedirect('/password/email');
$setPasswordPageResp->assertSessionHas('error', 'This invitation link has expired. You can instead try to reset your account password.');
}
+
+ public function test_set_password_view_is_throttled()
+ {
+ for ($i = 0; $i < 11; $i++) {
+ $response = $this->get("/register/invite/tokenhere{$i}");
+ }
+
+ $response->assertStatus(429);
+ }
+
+ public function test_set_password_post_is_throttled()
+ {
+ for ($i = 0; $i < 11; $i++) {
+ $response = $this->post("/register/invite/tokenhere{$i}", [
+ 'password' => 'my test password',
+ ]);
+ }
+
+ $response->assertStatus(429);
+ }
}
From d133f904d369cf2ca964a5c9f54e4e19843507c2 Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Mon, 20 May 2024 17:23:15 +0100
Subject: [PATCH 08/11] Auth: Changed email confirmations to use login attempt
user
Negates the need for a public confirmation resend form
since we can instead just send direct to the last session login attempter.
---
.../Controllers/ConfirmEmailController.php | 25 +++++---
.../Controllers/HandlesPartialLogins.php | 2 +-
app/Access/EmailConfirmationService.php | 2 +-
.../StoppedAuthenticationException.php | 14 ++---
lang/en/errors.php | 1 +
...hp => register-confirm-awaiting.blade.php} | 10 +---
tests/Auth/RegistrationTest.php | 58 +++++++++++++++++++
7 files changed, 83 insertions(+), 29 deletions(-)
rename resources/views/auth/{user-unconfirmed.blade.php => register-confirm-awaiting.blade.php} (66%)
diff --git a/app/Access/Controllers/ConfirmEmailController.php b/app/Access/Controllers/ConfirmEmailController.php
index 94647e06e..d71b8f450 100644
--- a/app/Access/Controllers/ConfirmEmailController.php
+++ b/app/Access/Controllers/ConfirmEmailController.php
@@ -32,13 +32,17 @@ class ConfirmEmailController extends Controller
/**
* Shows a notice that a user's email address has not been confirmed,
- * Also has the option to re-send the confirmation email.
+ * along with the option to re-send the confirmation email.
*/
public function showAwaiting()
{
$user = $this->loginService->getLastLoginAttemptUser();
+ if ($user === null) {
+ $this->showErrorNotification(trans('errors.login_user_not_found'));
+ return redirect('/login');
+ }
- return view('auth.user-unconfirmed', ['user' => $user]);
+ return view('auth.register-confirm-awaiting');
}
/**
@@ -90,19 +94,24 @@ class ConfirmEmailController extends Controller
/**
* Resend the confirmation email.
*/
- public function resend(Request $request)
+ public function resend()
{
- $this->validate($request, [
- 'email' => ['required', 'email', 'exists:users,email'],
- ]);
- $user = $this->userRepo->getByEmail($request->get('email'));
+ $user = $this->loginService->getLastLoginAttemptUser();
+ if ($user === null) {
+ $this->showErrorNotification(trans('errors.login_user_not_found'));
+ return redirect('/login');
+ }
try {
$this->emailConfirmationService->sendConfirmation($user);
+ } catch (ConfirmationEmailException $e) {
+ $this->showErrorNotification($e->getMessage());
+
+ return redirect('/login');
} catch (Exception $e) {
$this->showErrorNotification(trans('auth.email_confirm_send_error'));
- return redirect('/register/confirm');
+ return redirect('/register/awaiting');
}
$this->showSuccessNotification(trans('auth.email_confirm_resent'));
diff --git a/app/Access/Controllers/HandlesPartialLogins.php b/app/Access/Controllers/HandlesPartialLogins.php
index c5554e473..47a63d19b 100644
--- a/app/Access/Controllers/HandlesPartialLogins.php
+++ b/app/Access/Controllers/HandlesPartialLogins.php
@@ -17,7 +17,7 @@ trait HandlesPartialLogins
$user = auth()->user() ?? $loginService->getLastLoginAttemptUser();
if (!$user) {
- throw new NotFoundException('A user for this action could not be found');
+ throw new NotFoundException(trans('errors.login_user_not_found'));
}
return $user;
diff --git a/app/Access/EmailConfirmationService.php b/app/Access/EmailConfirmationService.php
index de9ea69b8..1a5156d3e 100644
--- a/app/Access/EmailConfirmationService.php
+++ b/app/Access/EmailConfirmationService.php
@@ -17,7 +17,7 @@ class EmailConfirmationService extends UserTokenService
*
* @throws ConfirmationEmailException
*/
- public function sendConfirmation(User $user)
+ public function sendConfirmation(User $user): void
{
if ($user->email_confirmed) {
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
diff --git a/app/Exceptions/StoppedAuthenticationException.php b/app/Exceptions/StoppedAuthenticationException.php
index 51e48aa1c..8a917bc52 100644
--- a/app/Exceptions/StoppedAuthenticationException.php
+++ b/app/Exceptions/StoppedAuthenticationException.php
@@ -9,16 +9,10 @@ use Illuminate\Http\Request;
class StoppedAuthenticationException extends \Exception implements Responsable
{
- protected $user;
- protected $loginService;
-
- /**
- * StoppedAuthenticationException constructor.
- */
- public function __construct(User $user, LoginService $loginService)
- {
- $this->user = $user;
- $this->loginService = $loginService;
+ public function __construct(
+ protected User $user,
+ protected LoginService $loginService
+ ) {
parent::__construct();
}
diff --git a/lang/en/errors.php b/lang/en/errors.php
index 8773a78cb..752eb5672 100644
--- a/lang/en/errors.php
+++ b/lang/en/errors.php
@@ -37,6 +37,7 @@ return [
'social_driver_not_found' => 'Social driver not found',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
+ 'login_user_not_found' => 'A user for this action could not be found.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
diff --git a/resources/views/auth/user-unconfirmed.blade.php b/resources/views/auth/register-confirm-awaiting.blade.php
similarity index 66%
rename from resources/views/auth/user-unconfirmed.blade.php
rename to resources/views/auth/register-confirm-awaiting.blade.php
index 2f780b8a3..e5d6f61b2 100644
--- a/resources/views/auth/user-unconfirmed.blade.php
+++ b/resources/views/auth/register-confirm-awaiting.blade.php
@@ -14,15 +14,7 @@