327 lines
11 KiB
PHP
327 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Livewire\Profile;
|
||
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Support\Facades\Storage;
|
||
use Illuminate\Support\Str;
|
||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||
|
||
|
||
use Livewire\Component;
|
||
use Livewire\WithFileUploads;
|
||
|
||
|
||
class UpdateSettingsForm extends Component
|
||
{
|
||
use WithFileUploads;
|
||
|
||
|
||
/**
|
||
* The component's state.
|
||
*
|
||
* @var array
|
||
*/
|
||
public $state;
|
||
|
||
|
||
/**
|
||
* The new avatar for the active profile.
|
||
*
|
||
* @var mixed
|
||
*/
|
||
public $photo;
|
||
|
||
|
||
/**
|
||
* Determine if the verification email was sent.
|
||
*
|
||
* @var bool
|
||
*/
|
||
public $verificationLinkSent = false;
|
||
|
||
|
||
/**
|
||
* Prepare the component.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function mount()
|
||
{
|
||
$activeProfile = getActiveProfile();
|
||
|
||
// --- Check roles and permissions --- //
|
||
// Permissions are assigned to Users (web guard), not to Organizations/Banks/Admins
|
||
$webUser = Auth::guard('web')->user();
|
||
|
||
if (!$webUser) {
|
||
abort(403, 'Unauthorized action.');
|
||
}
|
||
|
||
$authorized =
|
||
($activeProfile instanceof \App\Models\User &&
|
||
($webUser->can('manage users') ||
|
||
$webUser->id === $activeProfile->id))
|
||
||
|
||
($activeProfile instanceof \App\Models\Organization &&
|
||
($webUser->can('manage organizations') ||
|
||
$webUser->hasRole('Organization\\' . $activeProfile->id . '\\organization-manager') ||
|
||
$webUser->organizations()->where('organization_user.organization_id', $activeProfile->id)->exists()))
|
||
||
|
||
($activeProfile instanceof \App\Models\Bank &&
|
||
($webUser->can('manage banks') ||
|
||
$webUser->hasRole('Bank\\' . $activeProfile->id . '\\bank-manager') ||
|
||
$webUser->banksManaged()->where('bank_user.bank_id', $activeProfile->id)->exists()))
|
||
||
|
||
($activeProfile instanceof \App\Models\Admin &&
|
||
($webUser->can('manage admins') ||
|
||
$webUser->hasRole('Admin\\' . $activeProfile->id . '\\admin') ||
|
||
$webUser->admins()->where('admin_user.admin_id', $activeProfile->id)->exists()));
|
||
|
||
if (!$authorized) {
|
||
abort(403, 'Unauthorized action.');
|
||
}
|
||
|
||
|
||
$this->state = array_merge([
|
||
'email' => $activeProfile->email,
|
||
'full_name' => $activeProfile->full_name,
|
||
], $activeProfile->withoutRelations()->toArray());
|
||
}
|
||
|
||
|
||
/**
|
||
* Update the active profile's profile information.
|
||
*
|
||
* @param \Laravel\Fortify\Contracts\UpdatesUserProfileInformation $updater
|
||
* @return void
|
||
*/
|
||
public function updateProfileInformation()
|
||
{
|
||
$this->resetErrorBag();
|
||
|
||
$activeProfile = getActiveProfile();
|
||
|
||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||
\App\Helpers\ProfileAuthorizationHelper::authorize($activeProfile);
|
||
|
||
// Determine the profile type and table
|
||
$profileType = get_class($activeProfile);
|
||
$modelKey = match ($profileType) {
|
||
'App\Models\User' => 'user',
|
||
'App\Models\Organization' => 'organization',
|
||
'App\Models\Bank' => 'bank',
|
||
'App\Models\Admin' => 'admin',
|
||
default => 'user',
|
||
};
|
||
$tableName = (new $profileType())->getTable();
|
||
|
||
// Get validation rules from platform config
|
||
$emailRules = timebank_config("rules.profile_{$modelKey}.email");
|
||
$fullNameRules = timebank_config("rules.profile_{$modelKey}.full_name");
|
||
$photoRules = timebank_config("rules.profile_{$modelKey}.profile_photo");
|
||
|
||
// Convert string rules to arrays if needed
|
||
if (is_string($emailRules)) {
|
||
$emailRules = explode('|', $emailRules);
|
||
}
|
||
if (is_string($fullNameRules)) {
|
||
$fullNameRules = explode('|', $fullNameRules);
|
||
}
|
||
if (is_string($photoRules)) {
|
||
$photoRules = explode('|', $photoRules);
|
||
}
|
||
|
||
// Process email rules to handle unique constraint for current profile
|
||
$processedEmailRules = [];
|
||
foreach ($emailRules as $rule) {
|
||
if (is_string($rule) && \Illuminate\Support\Str::startsWith(trim($rule), 'unique:')) {
|
||
// Check if this is the unique rule for the current table
|
||
if (preg_match("/^unique:{$tableName},email(,|$)/", trim($rule))) {
|
||
// Replace with a Rule object that ignores current profile
|
||
$processedEmailRules[] = \Illuminate\Validation\Rule::unique($tableName, 'email')->ignore($activeProfile->id);
|
||
} else {
|
||
// Keep unique rules for other tables
|
||
$processedEmailRules[] = $rule;
|
||
}
|
||
} else {
|
||
$processedEmailRules[] = $rule;
|
||
}
|
||
}
|
||
|
||
// Process full_name rules to handle unique constraint for current profile
|
||
$processedFullNameRules = [];
|
||
if ($fullNameRules) {
|
||
foreach ($fullNameRules as $rule) {
|
||
if (is_string($rule) && \Illuminate\Support\Str::startsWith(trim($rule), 'unique:')) {
|
||
// Check if this is a unique rule for the current table (any column)
|
||
if (preg_match("/^unique:{$tableName},(\w+)(,|$)/", trim($rule), $matches)) {
|
||
$column = $matches[1];
|
||
// Replace with a Rule object that ignores current profile
|
||
$processedFullNameRules[] = \Illuminate\Validation\Rule::unique($tableName, $column)->ignore($activeProfile->id);
|
||
} else {
|
||
// Keep unique rules for other tables
|
||
$processedFullNameRules[] = $rule;
|
||
}
|
||
} else {
|
||
$processedFullNameRules[] = $rule;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Prepare validation rules
|
||
$rules = [
|
||
'state.email' => $processedEmailRules,
|
||
];
|
||
|
||
// Add full_name validation for non-User profiles
|
||
if (!($activeProfile instanceof \App\Models\User) && $processedFullNameRules) {
|
||
$rules['state.full_name'] = $processedFullNameRules;
|
||
}
|
||
|
||
// Add photo validation if a photo is being uploaded
|
||
if ($this->photo && $photoRules) {
|
||
$rules['photo'] = $photoRules;
|
||
}
|
||
|
||
// Validate the input
|
||
$this->validate($rules, [
|
||
'state.email.required' => __('The email field is required.'),
|
||
'state.email.email' => __('Please enter a valid email address.'),
|
||
'state.email.unique' => __('This email address is already in use.'),
|
||
'state.full_name.required' => __('The full name field is required.'),
|
||
'state.full_name.max' => __('The full name must not exceed the maximum length.'),
|
||
'photo.image' => __('The file must be an image.'),
|
||
'photo.max' => __('The image size exceeds the maximum allowed.'),
|
||
]);
|
||
|
||
// Check if the email has changed
|
||
$emailChanged = $this->state['email'] !== $activeProfile->email;
|
||
|
||
if ($this->photo) {
|
||
// Delete old file if it doesn't start with "app-images/" (as those are default images)
|
||
if ($activeProfile->profile_photo_path
|
||
&& !Str::startsWith($activeProfile->profile_photo_path, 'app-images/')) {
|
||
Storage::disk('public')->delete($activeProfile->profile_photo_path);
|
||
}
|
||
|
||
// Store the new file
|
||
$photoPath = $this->photo->store('profile-photos', 'public');
|
||
$this->state['profile_photo_path'] = $photoPath;
|
||
}
|
||
|
||
// Remove protected fields from state to prevent changes
|
||
$updateData = $this->state;
|
||
unset($updateData['name']); // Username is always read-only
|
||
|
||
// Full name is read-only for Users, but editable for Organizations, Banks, and Admins
|
||
if ($activeProfile instanceof \App\Models\User) {
|
||
unset($updateData['full_name']);
|
||
}
|
||
|
||
// Update records of active profile
|
||
$activeProfile->update($updateData);
|
||
|
||
// Refresh the component state with the updated model data
|
||
$activeProfile = $activeProfile->fresh();
|
||
$this->state = $activeProfile->toArray();
|
||
$this->state['email'] = $activeProfile->email;
|
||
$this->state['full_name'] = $activeProfile->full_name;
|
||
|
||
// Update the session variable so the Blade view can display the new photo
|
||
session(['activeProfilePhoto' => $this->state['profile_photo_path']]);
|
||
|
||
// Send email verification if the email has changed
|
||
if ($emailChanged) {
|
||
$activeProfile->forceFill(['email_verified_at' => null])->save();
|
||
$activeProfile->sendEmailVerificationNotification();
|
||
|
||
// Refresh state after email verification changes
|
||
$activeProfile = $activeProfile->fresh();
|
||
$this->state = $activeProfile->toArray();
|
||
$this->state['email'] = $activeProfile->email;
|
||
$this->state['full_name'] = $activeProfile->full_name;
|
||
}
|
||
|
||
if (isset($this->photo)) {
|
||
return redirect()->route('profile.settings');
|
||
}
|
||
|
||
$this->dispatch('saved');
|
||
}
|
||
|
||
|
||
/**
|
||
* Delete active profile's profile photo.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function deleteProfilePhoto()
|
||
{
|
||
$activeProfile = getActiveProfile();
|
||
|
||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||
\App\Helpers\ProfileAuthorizationHelper::authorize($activeProfile);
|
||
|
||
// If the existing photo path is not one of the default images, delete it
|
||
if ($activeProfile->profile_photo_path
|
||
&& !Str::startsWith($activeProfile->profile_photo_path, 'app-images/')) {
|
||
Storage::disk('public')->delete($activeProfile->profile_photo_path);
|
||
}
|
||
|
||
// Set the profile photo path to the configured default in your config file
|
||
$defaultPath = timebank_config('profiles.' . strtolower(getActiveProfileType()) . '.profile_photo_path_default');
|
||
$this->state['profile_photo_path'] = $defaultPath;
|
||
|
||
// Update the active profile’s record
|
||
$activeProfile->update(['profile_photo_path' => $defaultPath]);
|
||
|
||
// Refresh the component state with the updated model data
|
||
$this->state = $activeProfile->fresh()->toArray();
|
||
|
||
// Update the session variable so the Blade view can display the new photo
|
||
session(['activeProfilePhoto' => $defaultPath]);
|
||
|
||
redirect()->route('profile.settings');
|
||
|
||
// Dispatch any events if desired, for example:
|
||
$this->dispatch('saved');
|
||
$this->dispatch('refresh-navigation-menu');
|
||
}
|
||
|
||
|
||
/**
|
||
* Send the email verification.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function sendEmailVerification()
|
||
{
|
||
$activeProfile = getActiveProfile();
|
||
|
||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||
\App\Helpers\ProfileAuthorizationHelper::authorize($activeProfile);
|
||
|
||
$activeProfile->sendEmailVerificationNotification();
|
||
|
||
$this->verificationLinkSent = true;
|
||
}
|
||
|
||
|
||
/**
|
||
* Get the current active profile of the application.
|
||
*
|
||
* @return mixed
|
||
*/
|
||
public function getUserProperty()
|
||
{
|
||
return getActiveProfile();
|
||
}
|
||
|
||
|
||
public function render()
|
||
{
|
||
return view('livewire.profile.update-settings-form');
|
||
}
|
||
}
|