*/ use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, MustVerifyEmailTrait, Notifiable, SoftDeletes; protected $table = 'users'; /** * The attributes that are mass assignable. * * @var list */ protected $fillable = [ 'name', 'email', 'password', 'ic_number', 'position', 'phone_number', 'phone_verified_at', 'image_url', 'status', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at', 'gender', 'marriage_status', 'member_number', 'member_type', 'public_profile_token', 'join_date', 'leave_date', 'birth_date', 'birth_place', 'onboarding_completed_at', ]; /** * The attributes that should be hidden for serialization. * * @var list */ protected $hidden = [ 'password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at', 'public_profile_token', ]; protected static function booted(): void { static::creating(function (User $user) { $expiryDays = self::publicProfileTokenTtlDays(); if (empty($user->public_profile_token)) { $user->public_profile_token = Str::random(48); } if (empty($user->public_profile_token_expires_at)) { $user->public_profile_token_expires_at = now()->addDays($expiryDays); } }); } public static function publicProfileTokenTtlDays(): int { return (int) config('user.public_profile_token_ttl_days', 90); } public function isPublicProfileTokenExpired(): bool { return $this->public_profile_token_expires_at !== null && $this->public_profile_token_expires_at->isPast(); } public function ensurePublicProfileToken(): string { $expiryDays = self::publicProfileTokenTtlDays(); if (blank($this->public_profile_token) || $this->isPublicProfileTokenExpired()) { $this->forceFill([ 'public_profile_token' => Str::random(48), 'public_profile_token_expires_at' => now()->addDays($expiryDays), ])->save(); return $this->public_profile_token; } $this->forceFill([ 'public_profile_token_expires_at' => now()->addDays($expiryDays), ])->save(); return $this->public_profile_token; } /** * Get the attributes that should be cast. * * @return array */ protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'phone_verified_at' => 'datetime', 'password' => 'hashed', 'status' => 'string', 'gender' => 'string', 'marriage_status' => 'string', 'member_number' => 'integer', 'member_type' => 'string', 'public_profile_token_expires_at' => 'datetime', 'join_date' => 'date', 'leave_date' => 'date', 'birth_date' => 'date', 'birth_place' => 'string', 'onboarding_completed_at' => 'datetime', ]; } /** * Role used for permission checks in the current session (Sanctum token). */ public function activeRole(): ?Role { return ActiveRoleService::getActiveRole($this); } public function addresses() { return $this->hasMany(Address::class); } public function employments() { return $this->hasMany(Employment::class); } public function bankDetails() { return $this->hasMany(BankDetail::class); } public function heirs() { return $this->hasMany(Heir::class); } public function hasPermissionAction($action): bool { $role = $this->activeRole(); return $role && $role->permission_actions && in_array($action, $role->permission_actions, true); } public function getActivityLogOptions(): LogOptions { return LogOptions::defaults() ->logAll() ->logOnlyDirty() ->dontSubmitEmptyLogs() ->setDescriptionForEvent(fn (string $eventName) => "User {$this->name} was {$eventName}"); } /** * Override hasPermissionTo to bypass all permission checks for DEVELOPER role */ public function hasPermissionTo($permission, $guardName = null): bool { $activeRole = $this->activeRole(); if ($activeRole?->name === 'DEVELOPER') { return true; } static $isCheckingPermission = false; if ($isCheckingPermission) { return false; } $isCheckingPermission = true; try { $permissionClass = app(PermissionRegistrar::class)->getPermissionClass(); if (is_string($permission)) { $permission = $permissionClass::findByName($permission, $guardName ?? 'api'); } if (is_int($permission)) { $permission = $permissionClass::findById($permission, $guardName ?? 'api'); } if (! $permission instanceof Permission) { return false; } if ($this->permissions->contains('id', $permission->id)) { return true; } if (! $activeRole) { return false; } $activeRole->loadMissing('permissions'); return $activeRole->permissions->contains('id', $permission->id); } catch (\Exception $e) { return false; } finally { $isCheckingPermission = false; } } /** * Scope to exclude current user unless they have developer role */ public function scopeExcludeCurrentUserUnlessDeveloper($query, $currentUser = null) { $currentUser = $currentUser ?? auth()->user(); if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) { return $query->where('id', '!=', $currentUser->id); } return $query; } /** * Scope to exclude users with DEVELOPER role unless current user is DEVELOPER */ public function scopeExcludeDevelopersUnlessDeveloper($query, $currentUser = null) { $currentUser = $currentUser ?? auth()->user(); if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) { return $query->whereDoesntHave('roles', function ($q) { $q->where('name', 'DEVELOPER'); }); } return $query; } /** * Check if the user can impersonate another user */ public function canImpersonate(): bool { // Check permission instead of hardcoded roles return $this->hasPermissionTo('menyamar pengguna'); } /** * Check if the user can be impersonated */ public function canBeImpersonated(): bool { // DEVELOPER cannot be impersonated by anyone if ($this->hasRole('DEVELOPER')) { return false; } // Check if the current user has permission to impersonate $currentUser = auth()->user(); if (!$currentUser) { return false; } // Users with permission can impersonate other users (except DEVELOPER) return $currentUser->hasPermissionTo('menyamar pengguna'); } public function sendEmailVerificationNotification(): void { $this->notify(new VerifyEmailNotification); } /** * Check if user can login based on their status */ public function canLogin(): bool { return $this->status === 'active'; } /** * Whether credentials are valid for issuing a session (includes pending users awaiting admin activation). */ public function canAuthenticate(): bool { return in_array($this->status, ['active', 'pending'], true); } /** * Get the login restriction message based on user status */ public function getLoginRestrictionMessage(): ?string { switch ($this->status) { case 'pending': return 'Akaun anda sedang menunggu pengaktifan dari pentadbir sistem. Sila hubungi pentadbir sistem.'; case 'inactive': return 'Akaun anda tidak aktif. Sila hubungi pentadbir sistem.'; default: return 'Akaun anda tidak dapat mengakses sistem. Sila hubungi pentadbir sistem.'; } } }