Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,326 @@
<?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');
}
}