Initial commit
This commit is contained in:
321
app/Http/Livewire/ProfileBank/UpdateProfileBankForm.php
Normal file
321
app/Http/Livewire/ProfileBank/UpdateProfileBankForm.php
Normal file
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\ProfileBank;
|
||||
|
||||
use App\Models\Bank;
|
||||
use App\Traits\FormHelpersTrait;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class UpdateProfileBankForm extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
use HasProfilePhoto;
|
||||
use FormHelpersTrait;
|
||||
|
||||
public $state = [];
|
||||
public $bank;
|
||||
public $photo;
|
||||
public $languages;
|
||||
public $website;
|
||||
|
||||
//Important
|
||||
// Authorization is handled in mount() method instead of middleware to support multi-guard system
|
||||
// protected $middleware = [
|
||||
// 'can:manage banks',
|
||||
// ];
|
||||
|
||||
protected $listeners = ['languagesToParent'];
|
||||
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'photo' => timebank_config('rules.profile_bank.profile_photo'),
|
||||
'state.about' => timebank_config('rules.profile_bank.about', 400),
|
||||
'state.about_short' => timebank_config('rules.profile_bank.about_short', 150),
|
||||
'state.motivation' => timebank_config('rules.profile_bank.motivation', 300),
|
||||
'languages' => timebank_config('rules.profile_bank.languages', 'required'),
|
||||
'languages.id' => timebank_config('rules.profile_bank.languages_id', 'int'),
|
||||
'state.date_of_birth' => timebank_config('rules.profile_bank.date_of_birth', 'nullable|date'),
|
||||
'website' => timebank_config('rules.profile_bank.website', 'nullable|regex:/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function mount()
|
||||
{
|
||||
// --- Check roles and permissions --- //
|
||||
$activeProfile = getActiveProfile();
|
||||
|
||||
// Check if active profile is a Bank
|
||||
if (!($activeProfile instanceof \App\Models\Bank)) {
|
||||
abort(403, 'Unauthorized action.');
|
||||
}
|
||||
|
||||
// Check if web user (who owns the bank) has permission or bank manager role
|
||||
// Permissions are assigned to Users (web guard), not to Banks
|
||||
$webUser = Auth::guard('web')->user();
|
||||
|
||||
// User is authorized if ANY of these conditions are true:
|
||||
// 1. Has global "manage banks" permission (admin)
|
||||
// 2. Has bank manager role for this specific bank
|
||||
// 3. Is linked to this bank (owner/member)
|
||||
$authorized = ($webUser && (
|
||||
$webUser->can('manage banks') ||
|
||||
$webUser->hasRole('Bank\\' . $activeProfile->id . '\\bank-manager') ||
|
||||
$webUser->banksManaged()->where('bank_user.bank_id', $activeProfile->id)->exists()
|
||||
));
|
||||
|
||||
if (!$authorized) {
|
||||
abort(403, 'Unauthorized action.');
|
||||
}
|
||||
|
||||
|
||||
$this->state = Bank::find(session('activeProfileId'))->toArray();
|
||||
$this->website = $this->state['website'];
|
||||
$this->bank = Bank::find(session('activeProfileId'));
|
||||
|
||||
$this->getLanguages();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the profile photo URL for the bank
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProfilePhotoUrlProperty()
|
||||
{
|
||||
if (!$this->bank) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use asset() for app-images, Storage::url() for uploaded photos
|
||||
if (str_starts_with($this->bank->profile_photo_path, 'app-images/')) {
|
||||
return asset('storage/' . $this->bank->profile_photo_path);
|
||||
}
|
||||
|
||||
return url(Storage::url($this->bank->profile_photo_path));
|
||||
}
|
||||
|
||||
|
||||
public function getLanguages()
|
||||
{
|
||||
// Create a language options collection that combines all language and competence options
|
||||
$langOptions = DB::table('languages')->get(['id','name']);
|
||||
$compOptions = DB::table('language_competences')->get(['id','name']);
|
||||
$langOptions = collect(Arr::crossJoin($langOptions, $compOptions));
|
||||
$langOptions = $langOptions->Map(function ($language, $key) {
|
||||
return [
|
||||
'id' => $key, // index key is needed to select values in dropdown (option-value)
|
||||
'langId' => $language[0]->id,
|
||||
'compId' => $language[1]->id,
|
||||
'name' => trans($language[0]->name) . ' - ' . trans($language[1]->name),
|
||||
];
|
||||
});
|
||||
|
||||
// Create an array of the pre-selected language options
|
||||
$languages = $this->bank->languages;
|
||||
$languages = $languages->map(function ($language, $key) use ($langOptions) {
|
||||
$competence = DB::table('language_competences')->find($language->pivot->competence);
|
||||
$langSelected = collect($langOptions)->where('name', trans($language->name) . ' - ' . trans($competence->name));
|
||||
return [
|
||||
$langSelected->keys()
|
||||
];
|
||||
});
|
||||
$languages = $languages->flatten();
|
||||
|
||||
// Create a selected language collection that holds the selected languages with their selected competences
|
||||
$this->languages = collect($langOptions)->whereIn('id', $languages)->values();
|
||||
}
|
||||
|
||||
|
||||
public function languagesToParent($values)
|
||||
{
|
||||
$this->languages = $values;
|
||||
$this->validateOnly('languages');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate a single field when updated.
|
||||
* This is the 1st validation method on this form.
|
||||
*
|
||||
* @param mixed $field
|
||||
* @return void
|
||||
*/
|
||||
public function updated($field)
|
||||
{
|
||||
if ($field == 'website') {
|
||||
// If website is not empty, add URL scheme
|
||||
if (!empty($this->website)) {
|
||||
$this->website = $this->addUrlScheme($this->website);
|
||||
} else {
|
||||
// If website is empty, remove 'https://' prefix
|
||||
$this->website = str_replace('https://', '', $this->website);
|
||||
}
|
||||
}
|
||||
|
||||
$this->validateOnly($field);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the bank's profile contact information.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateProfilePersonalForm()
|
||||
{
|
||||
$bank = getActiveProfile();
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($bank);
|
||||
|
||||
if (isset($this->photo)) {
|
||||
$bank->updateProfilePhoto($this->photo); // Trait (use HasProfilePhoto) needs to attached to Bank model for this to work
|
||||
}
|
||||
|
||||
$this->validate(); // 2nd validation, just before save method
|
||||
|
||||
$bank->about = $this->state['about'];
|
||||
$bank->about_short = $this->state['about_short'];
|
||||
$bank->motivation = $this->state['motivation'];
|
||||
$bank->website = str_replace(['http://', 'https://', ], '', $this->website);
|
||||
|
||||
if (isset($this->languages)) {
|
||||
|
||||
$languages = collect($this->languages)->Map(function ($lang, $key) use ($bank) {
|
||||
return [
|
||||
'language_id' => $lang['langId'],
|
||||
'competence' => $lang['compId'],
|
||||
'languagable_type' => Bank::class,
|
||||
'languagable_id' => $bank->id,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$bank->languages()->detach(); // Remove all languages of this bank before inserting the new ones
|
||||
DB::table('languagables')->insert($languages);
|
||||
}
|
||||
|
||||
$bank->save();
|
||||
$this->dispatch('saved');
|
||||
session(['activeProfilePhoto' => $bank->profile_photo_path ]);
|
||||
redirect()->route('profile.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the bank's profile photo.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteProfilePhoto()
|
||||
{
|
||||
$bank = getActiveProfile();
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($bank);
|
||||
|
||||
if (! Features::managesProfilePhotos()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_null($bank->profile_photo_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultPath = timebank_config('profiles.bank.profile_photo_path_default');
|
||||
|
||||
// Delete uploaded photos (profile-photos/) and reset to default
|
||||
if (str_starts_with($bank->profile_photo_path, 'profile-photos/')) {
|
||||
Storage::disk(isset($_ENV['VAPOR_ARTIFACT_NAME']) ? 's3' : config('jetstream.profile_photo_disk', 'public'))->delete($bank->profile_photo_path);
|
||||
|
||||
$bank->forceFill([
|
||||
'profile_photo_path' => $defaultPath,
|
||||
])->save();
|
||||
|
||||
Session(['activeProfilePhoto'=> $bank->profile_photo_path ]);
|
||||
}
|
||||
// If current path is app-images but not the correct default, update it
|
||||
elseif (str_starts_with($bank->profile_photo_path, 'app-images/') && $bank->profile_photo_path !== $defaultPath) {
|
||||
$bank->forceFill([
|
||||
'profile_photo_path' => $defaultPath,
|
||||
])->save();
|
||||
|
||||
Session(['activeProfilePhoto'=> $bank->profile_photo_path ]);
|
||||
}
|
||||
|
||||
$this->dispatch('saved');
|
||||
redirect()->route('profile.edit');
|
||||
}
|
||||
|
||||
|
||||
public function addUrlScheme($url, $scheme = 'https://')
|
||||
{
|
||||
return parse_url($url, PHP_URL_SCHEME) === null ?
|
||||
$scheme . $url : $url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the label for the "about_short" input field, including a character counter if applicable.
|
||||
* The character counter needs to use the `characterLeftCounter` method from the `FormHelpersTrait`.
|
||||
*
|
||||
* @return string The label for the "about" field, optionally including the remaining character count.
|
||||
*/
|
||||
public function getAboutLabelProperty()
|
||||
{
|
||||
$maxInput = timebank_config('rules.profile_bank.about_max_input');
|
||||
$baseLabel = __('Introduce your bank in a few sentences');
|
||||
$counter = $this->characterLeftCounter($this->state['about'] ?? '', $maxInput);
|
||||
|
||||
return $counter ? $baseLabel . ' (' . $counter . ')' : $baseLabel;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the label for the "about_short" input field, including a character counter if applicable.
|
||||
* The character counter needs to use the `characterLeftCounter` method from the `FormHelpersTrait`.
|
||||
*
|
||||
* @return string The label for the "about_short" field, optionally including the remaining character count.
|
||||
*/
|
||||
public function getAboutShortLabelProperty()
|
||||
{
|
||||
$maxInput = timebank_config('rules.profile_bank.about_short_max_input');
|
||||
$baseLabel = __('Introduction in one sentence');
|
||||
$counter = $this->characterLeftCounter($this->state['about_short'] ?? '', $maxInput);
|
||||
|
||||
return $counter ? $baseLabel . ' (' . $counter . ')' : $baseLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the label for the "motivation" input field, including a character counter if applicable.
|
||||
* The character counter needs to use the `characterLeftCounter` method from the `FormHelpersTrait`.
|
||||
*
|
||||
* @return string The label for the "about_short" field, optionally including the remaining character count.
|
||||
*/
|
||||
public function getMotivationLabelProperty()
|
||||
{
|
||||
$maxInput = timebank_config('rules.profile_bank.motivation_max_input');
|
||||
$baseLabel = __('What is your motivation to start a ' . platform_name_short() . '?');
|
||||
$counter = $this->characterLeftCounter($this->state['motivation'] ?? '', $maxInput);
|
||||
|
||||
return $counter ? $baseLabel . ' (' . $counter . ')' : $baseLabel;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.profile-bank.update-profile-bank-form');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user