From 35d031626f431dfb52f8692bbb256f75179f31fa Mon Sep 17 00:00:00 2001 From: Tajim Date: Wed, 26 Aug 2026 12:17:55 +0600 Subject: [PATCH 1/5] feat(db): add username column with default student_{id} assignment --- app/Models/User.php | 12 +++++++ ..._26_120400_add_username_to_users_table.php | 33 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 database/migrations/2026_08_26_120400_add_username_to_users_table.php diff --git a/app/Models/User.php b/app/Models/User.php index 206f211..107ea09 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -16,6 +16,7 @@ /** * @property int $id * @property string $name + * @property string|null $username * @property string $email * @property Carbon|null $email_verified_at * @property string $password @@ -38,6 +39,7 @@ class User extends Authenticatable protected $fillable = [ 'name', + 'username', 'email', 'receive_emails', 'google_id', @@ -51,6 +53,16 @@ class User extends Authenticatable 'github', ]; + protected static function booted(): void + { + static::created(function (User $user) { + if (! $user->username) { + $user->updateQuietly(['username' => "student_{$user->id}"]); + $user->username = "student_{$user->id}"; + } + }); + } + protected $appends = [ 'image_url', ]; diff --git a/database/migrations/2026_08_26_120400_add_username_to_users_table.php b/database/migrations/2026_08_26_120400_add_username_to_users_table.php new file mode 100644 index 0000000..fc37b18 --- /dev/null +++ b/database/migrations/2026_08_26_120400_add_username_to_users_table.php @@ -0,0 +1,33 @@ +string('username')->nullable()->unique()->after('name'); + }); + + User::whereNull('username')->each(function ($user) { + $user->updateQuietly(['username' => "student_{$user->id}"]); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('username'); + }); + } +}; From edb72cf5c7df6eb0c0b0547be5e8adda8120eb09 Mon Sep 17 00:00:00 2001 From: Tajim Date: Wed, 26 Aug 2026 12:17:58 +0600 Subject: [PATCH 2/5] feat(profile): add username validation and edit field in profile settings --- .../Requests/Profile/UpdateProfileRequest.php | 9 ++++ resources/js/pages/Profile.vue | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/app/Http/Requests/Profile/UpdateProfileRequest.php b/app/Http/Requests/Profile/UpdateProfileRequest.php index 867d782..a503ee0 100644 --- a/app/Http/Requests/Profile/UpdateProfileRequest.php +++ b/app/Http/Requests/Profile/UpdateProfileRequest.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class UpdateProfileRequest extends FormRequest { @@ -24,6 +25,14 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], + 'username' => [ + 'sometimes', + 'string', + 'min:3', + 'max:30', + 'regex:/^[a-zA-Z0-9_]+$/', + Rule::unique('users', 'username')->ignore($this->user()->id), + ], 'file' => ['sometimes', 'nullable', 'image', 'max:2048'], 'about' => ['sometimes', 'nullable', 'string'], 'institution' => ['sometimes', 'nullable', 'string', 'max:255'], diff --git a/resources/js/pages/Profile.vue b/resources/js/pages/Profile.vue index 7ab6e00..fe1696d 100644 --- a/resources/js/pages/Profile.vue +++ b/resources/js/pages/Profile.vue @@ -32,6 +32,7 @@ const showConfirmModal = ref(false); const form = useForm({ _method: 'PUT', name: user.value?.name || '', + username: user.value?.username || '', file: null as File | null, about: user.value?.about || '', institution: user.value?.institution || '', @@ -178,6 +179,47 @@ const submitForm = () => {

+
+ +
+ + @ + + +
+

+ {{ form.errors.username }} +

+

+ Letters, numbers, and underscores (3–30 chars). +

+
+
- -
+ +
+
+ +
+ + @ + + +
+

+ {{ form.errors.username }} +

+
+
@@ -323,15 +323,19 @@ onBeforeUnmount(() => { v-if="user" class="mb-3 flex items-center gap-3 rounded-2xl border border-slate-200/80 bg-slate-50/80 p-3 dark:border-gray-800 dark:bg-gray-900/80" > - + class="h-10 w-10 shrink-0 overflow-hidden rounded-full ring-2 ring-indigo-600/20" + > + +
{{ user.name.charAt(0).toUpperCase() }}
@@ -344,7 +348,7 @@ onBeforeUnmount(() => {

- {{ user.email }} + {{ user.username ? '@' + user.username : user.email }}

From 502df2f05e5a2f02e98e279bd82e1a3d12cf6818 Mon Sep 17 00:00:00 2001 From: Tajim Date: Wed, 26 Aug 2026 12:18:10 +0600 Subject: [PATCH 5/5] test(user): add tests for username assignment and profile update --- tests/Feature/ProfileUsernameUpdateTest.php | 63 +++++++++++++++++++++ tests/Feature/UsernameAssignmentTest.php | 23 ++++++++ 2 files changed, 86 insertions(+) create mode 100644 tests/Feature/ProfileUsernameUpdateTest.php create mode 100644 tests/Feature/UsernameAssignmentTest.php diff --git a/tests/Feature/ProfileUsernameUpdateTest.php b/tests/Feature/ProfileUsernameUpdateTest.php new file mode 100644 index 0000000..5afddfb --- /dev/null +++ b/tests/Feature/ProfileUsernameUpdateTest.php @@ -0,0 +1,63 @@ +create([ + 'username' => 'student_1', + ]); + + $response = $this->actingAs($user)->put('/profile', [ + 'name' => $user->name, + 'username' => 'custom_tajim', + ]); + + $response->assertRedirect(route('profile.edit')); + expect($user->fresh()->username)->toBe('custom_tajim'); +}); + +test('user cannot update username to one already taken by someone else', function () { + User::factory()->create([ + 'username' => 'existing_user', + ]); + + $user = User::factory()->create([ + 'username' => 'my_user', + ]); + + $response = $this->actingAs($user)->put('/profile', [ + 'name' => $user->name, + 'username' => 'existing_user', + ]); + + $response->assertSessionHasErrors(['username']); + expect($user->fresh()->username)->toBe('my_user'); +}); + +test('user can submit profile without changing their existing username', function () { + $user = User::factory()->create([ + 'username' => 'my_handle', + ]); + + $response = $this->actingAs($user)->put('/profile', [ + 'name' => 'Updated Name', + 'username' => 'my_handle', + ]); + + $response->assertRedirect(route('profile.edit')); + expect($user->fresh()->name)->toBe('Updated Name'); + expect($user->fresh()->username)->toBe('my_handle'); +}); + +test('username must follow alphanumeric and underscore format', function () { + $user = User::factory()->create([ + 'username' => 'valid_username', + ]); + + $response = $this->actingAs($user)->put('/profile', [ + 'name' => $user->name, + 'username' => 'invalid user!@#', + ]); + + $response->assertSessionHasErrors(['username']); +}); diff --git a/tests/Feature/UsernameAssignmentTest.php b/tests/Feature/UsernameAssignmentTest.php new file mode 100644 index 0000000..38300b1 --- /dev/null +++ b/tests/Feature/UsernameAssignmentTest.php @@ -0,0 +1,23 @@ + 'Tajim Ahmed', + 'email' => 'tajim@example.com', + ]); + + expect($user->username)->toBe("student_{$user->id}"); + expect($user->username)->toMatch('/^student_\d+$/'); +}); + +test('explicitly provided username is preserved on creation', function () { + $user = User::create([ + 'name' => 'Custom Name', + 'username' => 'custom_handle_99', + 'email' => 'custom@example.com', + ]); + + expect($user->username)->toBe('custom_handle_99'); +});