Add logging for failed registration

This commit is contained in:
DariusIII
2026-03-10 12:10:36 +01:00
parent 18e1614395
commit 7115b55e56
3 changed files with 137 additions and 14 deletions
@@ -128,6 +128,14 @@ class RegisterController extends Controller
]);
if ($validator->fails()) {
$this->logRegistrationFailure(
$request,
'validation_failed',
'The registration request failed validation.',
'warning',
['validation_errors' => $validator->errors()->toArray()]
);
return redirect()->back()
->withErrors($validator)
->withInput($request->except('password', 'password_confirmation'));
@@ -146,7 +154,8 @@ class RegisterController extends Controller
if ($existingUser && $existingUser->trashed()) {
return $this->redirectBackWithRegistrationError(
$request,
'This email address belongs to a deactivated account. Please contact us if you need help reactivating it.'
'This email address belongs to a deactivated account. Please contact us if you need help reactivating it.',
'deactivated_account'
);
}
@@ -171,7 +180,8 @@ class RegisterController extends Controller
if (! empty($invitation->email) && ! $this->emailsMatch($invitation->email, $email)) {
return $this->redirectBackWithRegistrationError(
$request,
'The email address you entered does not match the invitation. Please use the invited email address to register.'
'The email address you entered does not match the invitation. Please use the invited email address to register.',
'invitation_email_mismatch'
);
}
}
@@ -203,16 +213,15 @@ class RegisterController extends Controller
}
});
} catch (Throwable $exception) {
Log::error('User registration failed', [
'username' => $userName,
'email' => $email,
'ip' => $request->ip(),
'error' => $exception->getMessage(),
]);
return $this->redirectBackWithRegistrationError(
$request,
'We could not complete your registration right now. Please try again in a few minutes. If the problem continues, contact support.'
'We could not complete your registration right now. Please try again in a few minutes. If the problem continues, contact support.',
'unexpected_exception',
'error',
[
'error' => $exception->getMessage(),
'exception' => $exception::class,
]
);
}
@@ -223,6 +232,11 @@ class RegisterController extends Controller
}
$error = 'Invalid or expired invitation token!';
$this->logRegistrationFailure(
$request,
'invalid_or_expired_invitation',
'The registration attempt used an invalid or expired invitation token.'
);
break;
case 'view':
@@ -328,14 +342,47 @@ class RegisterController extends Controller
return $invitation !== null;
}
private function redirectBackWithRegistrationError(Request $request, string $message): RedirectResponse
{
/**
* @param array<string, mixed> $context
*/
private function redirectBackWithRegistrationError(
Request $request,
string $message,
string $reason = 'registration_failed',
string $level = 'warning',
array $context = []
): RedirectResponse {
$this->logRegistrationFailure($request, $reason, $message, $level, $context);
return redirect()
->back()
->withErrors(['registration' => $message])
->withInput($request->except('password', 'password_confirmation'));
}
/**
* @param array<string, mixed> $context
*/
private function logRegistrationFailure(
Request $request,
string $reason,
string $message,
string $level = 'warning',
array $context = []
): void {
$logContext = array_merge([
'reason' => $reason,
'message' => $message,
'username' => $request->input('username'),
'email' => $request->input('email'),
'ip' => $request->ip(),
'registration_status' => Settings::settingValue('registerstatus'),
'has_invite_code' => $request->filled('invitecode') || $request->filled('token'),
], $context);
Log::channel('registration')->{$level}("Registration attempt failed: {$reason}", $logContext);
}
private function emailsMatch(string $firstEmail, string $secondEmail): bool
{
return strcasecmp(trim($firstEmail), trim($secondEmail)) === 0;
+11 -2
View File
@@ -38,7 +38,7 @@ return [
'papertrail' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => SyslogUdpHandler::class,
'handler' => Monolog\Handler\SyslogUdpHandler::class,
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
@@ -46,7 +46,7 @@ return [
],
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'handler' => Monolog\Handler\StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
@@ -87,6 +87,15 @@ return [
'permission' => 0775,
'locking' => false,
],
'registration' => [
'driver' => 'daily',
'path' => storage_path('logs/registration.log'),
'level' => 'debug',
'days' => 7,
'bubble' => true,
'permission' => 0775,
'locking' => false,
],
'crc_oso' => [
'driver' => 'daily',
'path' => storage_path('logs/crc_oso.log'),
+67
View File
@@ -10,7 +10,9 @@ use Illuminate\Contracts\Validation\UncompromisedVerifier;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
@@ -132,6 +134,26 @@ class RegisterControllerTest extends TestCase
public function test_registration_failure_shows_explanation_for_deactivated_account(): void
{
Log::partialMock();
$registrationLogger = Mockery::mock();
Log::shouldReceive('channel')
->once()
->with('registration')
->andReturn($registrationLogger);
$registrationLogger->shouldReceive('warning')
->once()
->with(
'Registration attempt failed: deactivated_account',
Mockery::on(function (array $context): bool {
return $context['reason'] === 'deactivated_account'
&& $context['email'] === 'disabled@example.com'
&& $context['username'] === 'freshmember'
&& $context['registration_status'] === 0;
})
);
DB::table('users')->insert([
'username' => 'oldmember',
'email' => 'disabled@example.com',
@@ -158,4 +180,49 @@ class RegisterControllerTest extends TestCase
'registration' => 'This email address belongs to a deactivated account. Please contact us if you need help reactivating it.',
]);
}
public function test_registration_exception_is_logged_to_registration_channel(): void
{
Log::partialMock();
$registrationLogger = Mockery::mock();
Log::shouldReceive('channel')
->once()
->with('registration')
->andReturn($registrationLogger);
$registrationLogger->shouldReceive('error')
->once()
->with(
'Registration attempt failed: unexpected_exception',
Mockery::on(function (array $context): bool {
return $context['reason'] === 'unexpected_exception'
&& $context['email'] === 'broken@example.com'
&& $context['username'] === 'brokenmember'
&& $context['exception'] === \RuntimeException::class
&& $context['registration_status'] === 0;
})
);
$this->partialMock(RegisterController::class, function (MockInterface $mock): void {
$mock->shouldAllowMockingProtectedMethods();
$mock->shouldReceive('create')
->once()
->andThrow(new \RuntimeException('Simulated registration failure'));
});
$response = $this->from(route('register'))->post(route('register.post'), [
'action' => 'submit',
'username' => 'brokenmember',
'email' => 'broken@example.com',
'password' => 'ValidPass1!',
'password_confirmation' => 'ValidPass1!',
'terms' => 'on',
]);
$response->assertRedirect(route('register'));
$response->assertSessionHasErrors([
'registration' => 'We could not complete your registration right now. Please try again in a few minutes. If the problem continues, contact support.',
]);
}
}