diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php
index eefa734d..d458f46a 100644
--- a/app/Http/Controllers/BlogController.php
+++ b/app/Http/Controllers/BlogController.php
@@ -56,13 +56,13 @@ public function show(Blog $blog)
{
abort_unless($blog->is_published, 404);
- $blog->load('user:id,name,username,image_path');
+ $blog->load('user:id,name,username,image_path,is_verified');
$blog->increment('views');
$reactionsCount = $blog->reactions()->count();
$reactors = $blog->reactions()
- ->with(['user:id,name,username,image_path,institution'])
+ ->with(['user:id,name,username,image_path,institution,is_verified'])
->latest('id')
->limit(50)
->get()
@@ -71,7 +71,7 @@ public function show(Blog $blog)
->values();
$comments = $blog->comments()
- ->with(['user:id,name,username,image_path,institution'])
+ ->with(['user:id,name,username,image_path,institution,is_verified'])
->latest()
->get();
diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php
index cddfb34c..5be50636 100644
--- a/app/Http/Controllers/ChatController.php
+++ b/app/Http/Controllers/ChatController.php
@@ -55,9 +55,9 @@ public function index(Request $request)
// Fetch last X messages in chronological order
$messages = ChatMessage::with([
- 'user:id,name,username,image_path,institution',
+ 'user:id,name,username,image_path,institution,is_verified',
'user.roles:id,name',
- 'reactions.user:id,name,username,image_path,institution',
+ 'reactions.user:id,name,username,image_path,institution,is_verified',
'reactions.user.roles:id,name',
])
->latest('id')
@@ -243,7 +243,7 @@ public function store(Request $request)
// Log without failing response
}
- $message->loadMissing(['user:id,name,username,image_path,institution', 'user.roles:id,name']);
+ $message->loadMissing(['user:id,name,username,image_path,institution,is_verified', 'user.roles:id,name']);
return response()->json([
'id' => $message->id,
diff --git a/app/Http/Controllers/ForumController.php b/app/Http/Controllers/ForumController.php
index 7b19f775..595bf0ec 100644
--- a/app/Http/Controllers/ForumController.php
+++ b/app/Http/Controllers/ForumController.php
@@ -42,7 +42,7 @@ public function index(Request $request): Response
$postsQuery = ForumPost::query()
->when(! ($myPosts && auth()->check()), fn ($q) => $q->approved())
->with([
- 'user:id,name,username,image_path,institution',
+ 'user:id,name,username,image_path,institution,is_verified',
'subject:id,name,course,slug',
'node:id,name,slug',
])
@@ -93,16 +93,16 @@ public function show(Request $request, ForumPost $post): Response
}
$post->load([
- 'user:id,name,username,image_path,institution',
+ 'user:id,name,username,image_path,institution,is_verified',
'subject:id,name,course,slug',
'node:id,name,slug',
]);
$answers = $post->directAnswers()
->with([
- 'user:id,name,username,image_path,institution',
+ 'user:id,name,username,image_path,institution,is_verified',
'replies' => fn ($q) => $q->with([
- 'user:id,name,username,image_path,institution',
+ 'user:id,name,username,image_path,institution,is_verified',
'parent:id,user_id',
'parent.user:id,name,username',
])->orderBy('created_at', 'asc'),
@@ -149,7 +149,7 @@ public function show(Request $request, ForumPost $post): Response
$upvoters = ForumVote::where('voteable_type', ForumPost::class)
->where('voteable_id', $post->id)
->where('value', 1)
- ->with('user:id,name,username,image_path,institution')
+ ->with('user:id,name,username,image_path,institution,is_verified')
->latest()
->get()
->pluck('user')
diff --git a/app/Http/Controllers/NodeController.php b/app/Http/Controllers/NodeController.php
index 60197bea..0eb7861d 100644
--- a/app/Http/Controllers/NodeController.php
+++ b/app/Http/Controllers/NodeController.php
@@ -51,7 +51,7 @@ public function show(Subject $subject, $path)
: null;
$upvoters = $node->upvotes()
- ->with(['user:id,name,username,image_path,institution', 'user.roles:id,name'])
+ ->with('user:id,name,username,image_path,institution,is_verified')
->latest('id')
->limit(50)
->get()
diff --git a/app/Http/Controllers/ResourceController.php b/app/Http/Controllers/ResourceController.php
index bf4cf797..900d8819 100644
--- a/app/Http/Controllers/ResourceController.php
+++ b/app/Http/Controllers/ResourceController.php
@@ -38,7 +38,7 @@ public function show($id)
$completionsCount = ResourceCompletion::where('resource_id', $id)->count();
$completers = ResourceCompletion::where('resource_id', $id)
- ->with(['user:id,name,username,image_path,institution'])
+ ->with(['user:id,name,username,image_path,institution,is_verified'])
->latest()
->take(10)
->get()
diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php
index a54c87be..17b3731f 100644
--- a/app/Http/Controllers/UserProfileController.php
+++ b/app/Http/Controllers/UserProfileController.php
@@ -53,13 +53,13 @@ public function show(string $username)
: false;
$appreciators = $user->appreciators()
- ->select(['users.id', 'users.name', 'users.username', 'users.image_path', 'users.institution'])
+ ->select(['users.id', 'users.name', 'users.username', 'users.image_path', 'users.institution', 'users.is_verified'])
->inRandomOrder()
->take(30)
->get();
$appreciating = $user->appreciatingUsers()
- ->select(['users.id', 'users.name', 'users.username', 'users.image_path', 'users.institution'])
+ ->select(['users.id', 'users.name', 'users.username', 'users.image_path', 'users.institution', 'users.is_verified'])
->inRandomOrder()
->take(30)
->get();
@@ -180,8 +180,8 @@ public function show(string $username)
// Suggested / Discover community members: 2 contributors + 2 general users
$contributorUsers = User::where('id', '!=', $user->id)
->whereNotNull('username')
- ->whereHas('roles')
- ->select(['id', 'name', 'username', 'institution', 'image_path', 'about'])
+ ->where('is_verified', true)
+ ->select(['id', 'name', 'username', 'institution', 'image_path', 'about', 'is_verified'])
->inRandomOrder()
->take(2)
->get();
@@ -191,7 +191,7 @@ public function show(string $username)
$randomUsers = User::whereNotIn('id', $excludedIds)
->whereNotNull('username')
- ->select(['id', 'name', 'username', 'institution', 'image_path', 'about'])
+ ->select(['id', 'name', 'username', 'institution', 'image_path', 'about', 'is_verified'])
->inRandomOrder()
->take($remainingNeeded)
->get();
@@ -211,7 +211,6 @@ public function show(string $username)
'github' => $user->github,
'created_at' => $user->created_at?->format('M Y') ?? '2026',
'is_verified' => $user->is_verified,
- 'is_staff' => $user->is_verified,
],
'stats' => [
'questionsCount' => $questionsCount,
diff --git a/app/Http/Requests/User/StoreUserRequest.php b/app/Http/Requests/User/StoreUserRequest.php
index ab4a6a5d..82d38140 100644
--- a/app/Http/Requests/User/StoreUserRequest.php
+++ b/app/Http/Requests/User/StoreUserRequest.php
@@ -28,6 +28,7 @@ public function rules(): array
'name' => ['required', 'string', 'max:255', new CleanText],
'username' => ['nullable', 'string', 'min:3', 'max:30', 'regex:/^[a-zA-Z0-9_]+$/', 'unique:users,username', new CleanText],
'email' => ['required', 'email', 'unique:users,email'],
+ 'is_verified' => ['sometimes', 'boolean'],
'role' => ['nullable', 'string'],
'permissions' => ['nullable', 'array'],
'permissions.*' => ['string', 'exists:permissions,name'],
diff --git a/app/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php
index dba159e7..060bf0c3 100644
--- a/app/Http/Requests/User/UpdateUserRequest.php
+++ b/app/Http/Requests/User/UpdateUserRequest.php
@@ -45,6 +45,7 @@ public function rules(): array
'facebook' => ['sometimes', 'nullable', 'string', 'max:255'],
'instagram' => ['sometimes', 'nullable', 'string', 'max:255'],
'github' => ['sometimes', 'nullable', 'string', 'max:255'],
+ 'is_verified' => ['sometimes', 'boolean'],
'role' => ['sometimes', 'nullable', 'string'],
'permissions' => ['sometimes', 'nullable', 'array'],
'permissions.*' => ['string', 'exists:permissions,name'],
diff --git a/app/Models/ChatMessage.php b/app/Models/ChatMessage.php
index 3e2bf149..fb21b13c 100644
--- a/app/Models/ChatMessage.php
+++ b/app/Models/ChatMessage.php
@@ -71,7 +71,7 @@ public function getFormattedReactions(?int $currentUserId = null): array
'image_url' => $r->user->image_url,
'image_path' => $r->user->image_path,
'institution' => $r->user->institution,
- 'is_verified' => (bool) ($r->user->is_verified || ($r->user->relationLoaded('roles') && $r->user->roles->isNotEmpty())),
+ 'is_verified' => (bool) ($r->user->is_verified ?? false),
'roles' => $r->user->relationLoaded('roles') ? $r->user->roles->pluck('name')->toArray() : [],
];
})->filter()->values()->toArray(),
diff --git a/app/Models/User.php b/app/Models/User.php
index ed25af7b..866a5966 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -18,6 +18,7 @@
* @property string $name
* @property string $email
* @property Carbon|null $email_verified_at
+ * @property bool $is_verified
* @property string $password
* @property string|null $remember_token
* @property Carbon|null $created_at
@@ -41,6 +42,7 @@ class User extends Authenticatable
'username',
'email',
'receive_emails',
+ 'is_verified',
'google_id',
'email_verified_at',
'banned_until',
@@ -65,17 +67,9 @@ protected static function booted(): void
protected $appends = [
'image_url',
- 'is_verified',
'is_banned',
];
- public function getIsVerifiedAttribute(): bool
- {
- return $this->relationLoaded('roles')
- ? $this->roles->isNotEmpty()
- : $this->roles()->exists();
- }
-
public function isBanned(): bool
{
return $this->banned_until !== null && $this->banned_until->isFuture();
@@ -109,6 +103,7 @@ protected function casts(): array
'banned_until' => 'datetime',
'password' => 'hashed',
'receive_emails' => 'boolean',
+ 'is_verified' => 'boolean',
];
}
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
index 30be883e..1185f4d6 100644
--- a/database/factories/UserFactory.php
+++ b/database/factories/UserFactory.php
@@ -23,10 +23,21 @@ public function definition(): array
'email' => fake()->unique()->safeEmail(),
'google_id' => fake()->unique()->uuid(),
'email_verified_at' => now(),
+ 'is_verified' => false,
'remember_token' => Str::random(10),
];
}
+ /**
+ * Indicate that the user is verified.
+ */
+ public function verified(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'is_verified' => true,
+ ]);
+ }
+
/**
* Indicate that the model's email address should be unverified.
*/
diff --git a/database/migrations/2026_09_01_224500_add_is_verified_to_users_table.php b/database/migrations/2026_09_01_224500_add_is_verified_to_users_table.php
new file mode 100644
index 00000000..cc4cddaa
--- /dev/null
+++ b/database/migrations/2026_09_01_224500_add_is_verified_to_users_table.php
@@ -0,0 +1,49 @@
+boolean('is_verified')->default(false)->after('email_verified_at');
+ });
+
+ $modelHasRolesTable = config('permission.table_names.model_has_roles', 'model_has_roles');
+ $modelMorphKey = config('permission.column_names.model_morph_key', 'model_id');
+
+ if (Schema::hasTable($modelHasRolesTable)) {
+ $userIdsWithRoles = DB::table($modelHasRolesTable)
+ ->where(function ($query) {
+ $query->where('model_type', User::class)
+ ->orWhere('model_type', 'App\Models\User');
+ })
+ ->distinct()
+ ->pluck($modelMorphKey);
+
+ if ($userIdsWithRoles->isNotEmpty()) {
+ DB::table('users')
+ ->whereIn('id', $userIdsWithRoles)
+ ->update(['is_verified' => true]);
+ }
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('is_verified');
+ });
+ }
+};
diff --git a/database/seeders/AdminSeeder.php b/database/seeders/AdminSeeder.php
index 92abb518..ecaf4a89 100644
--- a/database/seeders/AdminSeeder.php
+++ b/database/seeders/AdminSeeder.php
@@ -14,6 +14,7 @@ public function run(): void
[
'name' => config('app.admin_name', 'ADMIN'),
'email_verified_at' => now(),
+ 'is_verified' => true,
]
);
diff --git a/resources/js/components/UserListItem.vue b/resources/js/components/UserListItem.vue
index 9495d9ef..e18ac111 100644
--- a/resources/js/components/UserListItem.vue
+++ b/resources/js/components/UserListItem.vue
@@ -14,7 +14,6 @@ const props = withDefaults(
institution?: string | null;
title?: string | null;
is_verified?: boolean;
- roles?: Array<{ id?: number; name: string }>;
};
theme?: 'indigo' | 'emerald' | 'rose';
subtitle?: string | null;
@@ -30,10 +29,7 @@ const profileUrl = computed(() => {
});
const isVerified = computed(() => {
- return Boolean(
- props.user.is_verified ??
- (props.user.roles && props.user.roles.length > 0),
- );
+ return Boolean(props.user.is_verified);
});
const avatarBgClass = computed(() => {
diff --git a/resources/js/components/admin/UserRow.vue b/resources/js/components/admin/UserRow.vue
index 735cde9b..e0f24acb 100644
--- a/resources/js/components/admin/UserRow.vue
+++ b/resources/js/components/admin/UserRow.vue
@@ -4,6 +4,7 @@ import { LogIn, Pencil, Trash2, Ban } from 'lucide-vue-next';
import { ref } from 'vue';
import ChatBanModal from '@/components/ChatBanModal.vue';
import StatusBadge from '@/components/StatusBadge.vue';
+import VerifiedBadge from '@/components/VerifiedBadge.vue';
import { usePermissions } from '@/lib/usePermissions';
const { can } = usePermissions();
@@ -81,6 +82,8 @@ const deleteUser = (id: number) => {
{{ user.name }}
+