Initial commit
This commit is contained in:
478
app/Http/Livewire/SwitchProfile.php
Normal file
478
app/Http/Livewire/SwitchProfile.php
Normal file
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Events\ProfileSwitchEvent;
|
||||
use App\Models\Admin;
|
||||
use App\Models\Bank;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use App\Traits\SwitchGuardTrait;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Log;
|
||||
use Stevebauman\Location\Facades\Location as IpLocation;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
class SwitchProfile extends Component
|
||||
{
|
||||
use WireUiActions;
|
||||
use SwitchGuardTrait;
|
||||
|
||||
protected $user;
|
||||
public $userName;
|
||||
public $userProfiles = [];
|
||||
public $userProfileIndex;
|
||||
public $notifySwitchProfile;
|
||||
public $activeProfile = [];
|
||||
public $profilesWithUnread = [];
|
||||
|
||||
/**
|
||||
* Get the event listeners for the component.
|
||||
* Listens for the ProfileSwitchEvent event on the switch-profile.{$userId} private Echo channel. When this event is fired, the notifySwitchProfile method of the component will be called.
|
||||
* @return array
|
||||
*/
|
||||
protected function getListeners()
|
||||
{
|
||||
$userId = Auth::guard('web')->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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user