id(); // Get the authenticated user's ID for lowest level identification return [ "echo-private:switch-profile.{$userId},ProfileSwitchEvent" => 'notifySwitchProfile', ]; } public function mount() { //TODO NEXT: Add middleware seciruty! $this->user = Auth::guard('web')->user(); $this->userName = $this->user->name; // Eager load profile relationships $userWithRelations = User::with([ 'organizations', 'banksManaged', 'admins' ])->find($this->user->id); // Filter out removed (soft-deleted) items $organizations = $userWithRelations->organizations->filter( fn ($organizations) => $organizations->deleted_at === null || $organizations->deleted_at > now() ); $banks = $userWithRelations->banksManaged->filter( fn ($bank) => $bank->deleted_at === null || $bank->deleted_at > now() ); $admins = $userWithRelations->admins->filter( fn ($admin) => $admin->deleted_at === null || $admin->deleted_at > now() ); // Merge profiles collections $profiles = $organizations ->concat($banks) ->concat($admins); // Map the merged collection to the desired structure $this->userProfiles = $profiles->map(function ($profile) { if ($profile instanceof Organization) { $type = 'organization'; } elseif ($profile instanceof Bank) { $type = 'bank'; } elseif ($profile instanceof Admin) { $type = 'admin'; } else { $type = 'unknown'; } return [ 'id' => $profile->id, 'type' => $type, 'name' => $profile->name, 'photo' => $profile->profile_photo_path, ]; })->values()->toArray(); // Find the index corresponding to the current session profile $activeType = session('activeProfileType'); $activeId = session('activeProfileId'); if ($activeType === \App\Models\User::class && $activeId === $this->user->id) { $this->userProfileIndex = null; // Main user profile } else { foreach ($this->userProfiles as $index => $profile) { $profileClassName = 'App\\Models\\' . ucfirst($profile['type']); if ($profileClassName === $activeType && $profile['id'] === $activeId) { $this->userProfileIndex = $index; break; } } } // If no match found, it defaults to null (main user) // Load unread status for all profiles $this->loadUnreadStatus(); } // Computed property to format options for WireUI select public function getSelectOptionsProperty(): array { $options = [ // Main user option (value null) ['value' => null, 'label' => Str::limit($this->userName, 25, '...')] ]; // Add other profiles from the loop foreach ($this->userProfiles as $index => $profile) { $options[] = [ 'value' => $index, // The original index is the value 'label' => Str::limit($profile['name'], 25, '...') // The limited name is the label ]; } return $options; } public function profileSelected($index = null) { $this->userProfileIndex = $index; $this->switchProfile(); } public function switchProfile() { $index = $this->userProfileIndex; // If index is missing or out of range, switch to the auth user without login if ($index === null || !isset($this->userProfiles[$index])) { return $this->switchToAuthUser(); } $profileArray = $this->userProfiles[$index]; $profileType = ucfirst($profileArray['type']); $profileId = $profileArray['id']; $profileClass = $profile = "App\\Models\\$profileType"; $profile = $profileClass::find($profileId); // If the user is switching to their own User profile, switch without login if ($profileType === 'User' && (int) $profileId === Auth::guard('web')->id()) { return $this->switchToAuthUser(); } // If switching to an Admin profile, check if the user is authenticated as an Admin elseif ($profileType === 'Admin' ) { // Redirect to admin login page session([ 'intended_profile_switch_type' => $profileType, 'intended_profile_switch_id' => $profileId, ]); $this->redirect(route('admin.login')); return; // Explicitly stop further execution } // If switching to an Bank profile, check if the user is authenticated as an Bank elseif ($profileType === 'Bank' ) { // Redirect to bank login page session([ 'intended_profile_switch_type' => $profileType, 'intended_profile_switch_id' => $profileId, ]); $this->redirect(route('bank.login')); return; // Explicitly stop further execution } // No Bank or Admin profile: without login // Otherwise, attempt to find and validate the model $profileClassName = 'App\\Models\\' . $profileType; $profileModel = $profileClassName::find($profileId); $newGuard = strtolower($profileType); // If the model doesn't exist or the user doesn't own it, fall back with a warning // Use userOwnsProfile instead of can() to avoid cross-guard protection during switching if (!$profileModel || !\App\Helpers\ProfileAuthorizationHelper::userOwnsProfile($profileModel) || !class_exists($profileClassName)) { return $this->fallbackToAuthUser(); } $this->switchGuard($newGuard, $profileModel); // Switch to the chosen profile session([ 'activeProfileType' => $profileClassName, 'activeProfileId' => $profileModel->id, 'activeProfileName' => $profileModel->name, 'activeProfilePhoto' => $profileModel->profile_photo_path, 'profile-switched-notification' => true, ]); // Re-activate profile if inactive if (timebank_config('profile_inactive.re-activate_at_login')) { if (!$profileModel->isActive()) { $profileModel->inactive_at = null; $profileModel->save(); info(class_basename($profileClassName) . ' ' . 're-activated: ' . $profileModel->name); } } // Fire the event, redirect, etc. event(new ProfileSwitchEvent($profileModel)); $this->redirect(route('main')); // Use Livewire's redirect method return; // Explicitly stop further execution } /** * Switch to the authenticated user's own profile without logging in. */ protected function switchToAuthUser() { $this->logoutNonWebGuards(); $user = Auth::guard('web')->user(); session([ 'activeProfileType' => \App\Models\User::class, 'activeProfileId' => $user->id, 'activeProfileName' => $user->name, 'activeProfilePhoto' => $user->profile_photo_path, 'active_guard' => 'web', // Explicitly set active guard 'profile-switched-notification' => true, ]); event(new ProfileSwitchEvent($user)); $this->redirect(route('main')); // Use Livewire's redirect method return; // Explicitly stop further execution } /** * If user tampered with the front-end, we fall back to the default user profile. */ protected function fallbackToAuthUser() { $this->logoutNonWebGuards(); $user = Auth::guard('web')->user(); Session([ 'activeProfileType' => \App\Models\User::class, 'activeProfileId' => $user->id, 'activeProfileName' => $user->name, 'activeProfilePhoto' => $user->profile_photo_path, 'activeProfileAccounts' => $user->accounts()->pluck('id')->toArray(), 'active_guard' => 'web', // Explicitly set active guard ]); $activeProfile = [ 'userId' => $user->id, 'type' => session('activeProfileType'), 'id' => session('activeProfileId'), 'name' => session('activeProfileName'), 'photo' => session('activeProfilePhoto'), ]; $warningMessage = 'Unauthorized profile switch attempt'; $this->logAndReport($warningMessage); session()->flash('error', __($warningMessage) . '. ' . __('This event has been logged') . '!'); session(['unauthorizedAction' => __($warningMessage) . '. ' . __('This event has been logged') . '!']); return event(new ProfileSwitchEvent($activeProfile)); } public function notifySwitchProfile($activeProfile) { $this->notifySwitchProfile = true; Session([ // 'active_guard' => $activeProfile['guard'], 'activeProfileType' => $activeProfile['type'], 'activeProfileId' => $activeProfile['id'], 'activeProfileName' => $activeProfile['name'], 'activeProfilePhoto' => $activeProfile['photo'], 'profile-switched-notification' => true, ]); return redirect()->route('main'); } /** * Logs a warning message and reports it via email to the system administrator. * * This method logs a warning message with detailed information about the event, * including account details, user details, IP address, and location. It also * sends an email to the system administrator with the same information. */ private function logAndReport($warningMessage, $error = '') { $ip = request()->ip(); $ipLocationInfo = IpLocation::get($ip); // Escape ipLocation errors when not in production if (!$ipLocationInfo || App::environment(['local', 'development', 'staging'])) { $ipLocationInfo = (object) [ 'cityName' => 'local City', 'regionName' => 'local Region', 'countryName' => 'local Country', ]; } $eventTime = now()->toDateTimeString(); // Log this event and mail to admin Log::warning($warningMessage, [ 'userId' => Auth::guard('web')->id(), 'userName' => Auth::user()->name, 'activeProfileId' => session('activeProfileId'), 'activeProfileType' => session('activeProfileType'), 'activeProfileName' => session('activeProfileName'), 'IP address' => $ip, 'IP location' => $ipLocationInfo->cityName . ', ' . $ipLocationInfo->regionName . ', ' . $ipLocationInfo->countryName, 'Event Time' => $eventTime, 'Message' => $error, ]); Mail::raw( $warningMessage . '.' . "\n\n" . 'User ID: ' . Auth::guard('web')->id() . "\n" . 'User Name: ' . Auth::guard('web')->user()->name . "\n" . 'Active Profile ID: ' . session('activeProfileId') . "\n" . 'Active Profile Type: ' . session('activeProfileType') . "\n" . 'Active Profile Name: ' . session('activeProfileName') . "\n" . 'IP address: ' . $ip . "\n" . 'IP location: ' . $ipLocationInfo->cityName . ', ' . $ipLocationInfo->regionName . ', ' . $ipLocationInfo->countryName . "\n" . 'Event Time: ' . $eventTime . "\n\n" . $error, function ($message) use ($warningMessage) { $message->to(timebank_config('mail.system_admin.email'))->subject($warningMessage); }, ); session()->flash('error', __($warningMessage) . '. ' . __('This event has been logged and reported to our system administrator') . '.'); } /** * Load unread status for all switchable profiles */ private function loadUnreadStatus() { $webUser = Auth::guard('web')->user(); if (!$webUser) { return; } // Collect all profile IDs $profileIds = [ 'user' => [$webUser->id], 'admin' => [], 'bank' => [], 'organization' => [], ]; foreach ($this->userProfiles as $profile) { $profileIds[$profile['type']][] = $profile['id']; } // Check for unread messages $this->profilesWithUnread = $this->checkUnreadMessages($profileIds); } /** * Check which profiles have unread messages (boolean check only) * Returns array like: ['user' => true, 'admin' => [2 => true], 'bank' => [3 => false], ...] */ private function checkUnreadMessages(array $profileIds): array { $result = [ 'user' => false, 'admin' => [], 'bank' => [], 'organization' => [], ]; // Collect all profile type/id pairs $profilePairs = []; foreach ($profileIds as $type => $ids) { foreach ($ids as $id) { $modelClass = $this->getModelClass($type); $profilePairs[] = ['type' => $modelClass, 'id' => $id]; } } if (empty($profilePairs)) { return $result; } // Use parameter binding with IN clause for better escaping $typeIds = collect($profilePairs)->groupBy('type')->map(function ($items) { return $items->pluck('id')->toArray(); })->toArray(); // Check each type separately and combine results foreach ($typeIds as $modelClass => $ids) { $unreadForType = DB::table('wirechat_participants as p') ->select('p.participantable_type', 'p.participantable_id') ->join('wirechat_conversations as c', 'p.conversation_id', '=', 'c.id') ->where('p.participantable_type', '=', $modelClass) ->whereIn('p.participantable_id', $ids) ->whereNull('p.deleted_at') ->whereExists(function ($query) { $query->select(DB::raw(1)) ->from('wirechat_messages as m') ->whereColumn('m.conversation_id', 'p.conversation_id') ->whereNull('m.deleted_at') ->where(function ($q) { $q->whereNull('p.conversation_read_at') ->orWhereColumn('m.created_at', '>', 'p.conversation_read_at'); }) ->where(function ($q) { $q->where('m.sendable_id', '!=', DB::raw('p.participantable_id')) ->orWhere('m.sendable_type', '!=', DB::raw('p.participantable_type')); }); }) ->get(); foreach ($unreadForType as $row) { $type = $this->getTypeFromModelClass($row->participantable_type); $id = $row->participantable_id; if ($type === 'user') { $result['user'] = true; } else { $result[$type][$id] = true; } } } return $result; } /** * Map profile type to model class (return with single backslash for database comparison) */ private function getModelClass(string $type): string { return match($type) { 'user' => 'App\Models\User', 'admin' => 'App\Models\Admin', 'bank' => 'App\Models\Bank', 'organization' => 'App\Models\Organization', }; } /** * Map model class to profile type */ private function getTypeFromModelClass(string $class): string { return match($class) { 'App\\Models\\User', 'App\Models\User' => 'user', 'App\\Models\\Admin', 'App\Models\Admin' => 'admin', 'App\\Models\\Bank', 'App\Models\Bank' => 'bank', 'App\\Models\\Organization', 'App\Models\Organization' => 'organization', default => 'unknown' }; } public function render() { return view('livewire.switch-profile'); } }