Files
timebank-cc-public/app/Http/Livewire/Profile/UpdateSettingsForm.php
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

327 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 profiles 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');
}
}