From a1493462f7ced95d3e5e9d23e64188687ca24efa Mon Sep 17 00:00:00 2001 From: ghassan Date: Wed, 21 Oct 2026 00:34:32 +0300 Subject: [PATCH] feat(auth): block reserved identifiers at sign-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add App\Rules\ReservedIdentifier for name + email local-part so users cannot impersonate staff (admin, superadmin, support, root, takeone, …). Separators are collapsed so "super_admin"/"super-admin"/"superadmin" all match. Rule wired into RegisteredUserController@store. email_verified_at is already absent from User::$fillable so only the signed verification route can flip it — audited, no code change needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Auth/RegisteredUserController.php | 5 +- app/Rules/ReservedIdentifier.php | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 app/Rules/ReservedIdentifier.php diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index f9d31ca..96dbff0 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Models\User; use App\Notifications\NewUserRegistered; use App\Rules\NotDisposableEmail; +use App\Rules\ReservedIdentifier; use Illuminate\Auth\Events\Registered; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -27,8 +28,8 @@ class RegisteredUserController extends Controller } $request->validate([ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new NotDisposableEmail], + 'name' => ['required', 'string', 'max:255', new ReservedIdentifier('name')], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new NotDisposableEmail, new ReservedIdentifier('email')], 'password' => ['required', 'confirmed', Password::defaults()], 'birthday' => ['required', 'date', 'before:today'], 'gender' => ['required', 'in:male,female'], diff --git a/app/Rules/ReservedIdentifier.php b/app/Rules/ReservedIdentifier.php new file mode 100644 index 0000000..1cd2918 --- /dev/null +++ b/app/Rules/ReservedIdentifier.php @@ -0,0 +1,56 @@ +kind === 'email' + ? strtolower(trim(strstr($value, '@', true) ?: $value)) + : strtolower(trim($value)); + + // Collapse separators so "super_admin", "super-admin" and "superadmin" all match. + $normalized = preg_replace('/[\s._\-]+/', '', $candidate) ?? $candidate; + + foreach (self::RESERVED as $word) { + $target = preg_replace('/[\s._\-]+/', '', $word) ?? $word; + if ($normalized === $target) { + $fail($this->kind === 'email' + ? 'That email address is reserved. Please use a different one.' + : 'That name is reserved. Please choose a different one.'); + return; + } + } + } +}