Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\ProfileSwitchEvent;
|
||||
use App\Models\Bank;
|
||||
use App\Models\User;
|
||||
use App\Traits\SwitchGuardTrait;
|
||||
use Hash;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
class BankLoginController extends Controller
|
||||
{
|
||||
use SwitchGuardTrait;
|
||||
|
||||
/**
|
||||
* Direct link to bank login - can be used in emails
|
||||
* Handles the layered authentication flow:
|
||||
* 1. If user not authenticated -> redirect to user login first
|
||||
* 2. If user authenticated -> set intent and redirect to bank login
|
||||
* 3. After bank login -> redirect to intended URL or main page
|
||||
*/
|
||||
public function directLogin(Request $request, $bankId)
|
||||
{
|
||||
// Validate bank exists and load managers
|
||||
$bank = Bank::with('managers')->find($bankId);
|
||||
if (!$bank) {
|
||||
abort(404, __('Bank not found'));
|
||||
}
|
||||
|
||||
// Get optional intended destination after successful bank login.
|
||||
// Validate it is a local URL to prevent open redirect attacks.
|
||||
$intendedParam = $request->query('intended');
|
||||
$intendedUrl = $this->validateLocalUrl($intendedParam, $request);
|
||||
|
||||
// Store intended URL in session if provided
|
||||
if ($intendedUrl) {
|
||||
session(['bank_login_intended_url' => $intendedUrl]);
|
||||
}
|
||||
|
||||
// Check if user is authenticated on web guard
|
||||
if (!Auth::guard('web')->check()) {
|
||||
// User not logged in - redirect to user login with return URL
|
||||
$returnUrl = route('bank.direct-login', ['bankId' => $bankId]);
|
||||
if ($intendedUrl) {
|
||||
$returnUrl .= '?intended=' . urlencode($intendedUrl);
|
||||
}
|
||||
// Store in session for Laravel to redirect after login
|
||||
session()->put('url.intended', $returnUrl);
|
||||
|
||||
// Get the first manager's name to pre-populate the login form
|
||||
$firstManager = $bank->managers()->first();
|
||||
|
||||
if ($firstManager) {
|
||||
// Build the login URL with the name parameter and localization
|
||||
$loginRoute = route('login') . '?name=' . urlencode($firstManager->name);
|
||||
$loginUrl = \Mcamara\LaravelLocalization\Facades\LaravelLocalization::getLocalizedURL(null, $loginRoute);
|
||||
return redirect($loginUrl);
|
||||
}
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
// User is authenticated - verify they manage this bank
|
||||
$user = Auth::guard('web')->user();
|
||||
$userWithRelations = User::with('banksManaged')->find($user->id);
|
||||
|
||||
if (!$userWithRelations || !$userWithRelations->banksManaged->contains('id', $bankId)) {
|
||||
// Wrong user is logged in — log out all guards and redirect to login
|
||||
foreach (['web', 'organization', 'bank', 'admin'] as $guard) {
|
||||
Auth::guard($guard)->logout();
|
||||
}
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
$returnUrl = route('bank.direct-login', ['bankId' => $bankId]);
|
||||
if ($intendedUrl) {
|
||||
$returnUrl .= '?intended=' . urlencode($intendedUrl);
|
||||
}
|
||||
session()->put('url.intended', $returnUrl);
|
||||
|
||||
$firstManager = $bank->managers()->first();
|
||||
if ($firstManager) {
|
||||
$loginRoute = route('login') . '?name=' . urlencode($firstManager->name);
|
||||
$loginUrl = \Mcamara\LaravelLocalization\Facades\LaravelLocalization::getLocalizedURL(null, $loginRoute);
|
||||
return redirect($loginUrl);
|
||||
}
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
// Set the profile switch intent in session (kept for showLoginForm)
|
||||
session([
|
||||
'intended_profile_switch_type' => 'Bank',
|
||||
'intended_profile_switch_id' => $bankId,
|
||||
]);
|
||||
|
||||
// Redirect to bank login form — pass bankId in URL so login() doesn't
|
||||
// depend solely on session values that may be lost by Livewire writes.
|
||||
return redirect()->route('bank.login', ['bank_id' => $bankId]);
|
||||
}
|
||||
|
||||
public function showLoginForm(Request $request)
|
||||
{
|
||||
$user = Auth::guard('web')->user();
|
||||
// Prefer URL param (reliable), fall back to session (may be lost)
|
||||
$id = $request->query('bank_id') ?? session('intended_profile_switch_id');
|
||||
$type = $id ? 'bank' : session('intended_profile_switch_type');
|
||||
$profile = $this->getTargetProfileByTypeAndId($user, $type, $id);
|
||||
|
||||
return view('profile-bank.login', [
|
||||
'profile' => $profile,
|
||||
'bankId' => $id,
|
||||
'canResetPassword' => $this->profileCanResetPassword($user, 'Bank', $id),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
$user = Auth::guard('web')->user();
|
||||
// Prefer POST field (reliable), fall back to session (may be lost to Livewire writes)
|
||||
$id = $request->input('bank_id') ?? session('intended_profile_switch_id');
|
||||
$type = $id ? 'bank' : session('intended_profile_switch_type');
|
||||
$bank = $this->getTargetProfileByTypeAndId($user, $type, $id);
|
||||
|
||||
if (!$bank) {
|
||||
return back()->withErrors(['index' => __('Bank not found')]);
|
||||
}
|
||||
|
||||
// Legacy Cyclos password support
|
||||
if (!empty($bank->cyclos_salt)) {
|
||||
info('Auth attempt using original Cyclos password');
|
||||
$concatenated = $bank->cyclos_salt . $request->password;
|
||||
$hashedInputPassword = hash("sha256", $concatenated);
|
||||
|
||||
if (strtolower($hashedInputPassword) === strtolower($bank->password)) {
|
||||
info('Auth success: Password is verified');
|
||||
// Rehash to Laravel hash and remove salt
|
||||
$bank->password = \Hash::make($request->password);
|
||||
$bank->cyclos_salt = null;
|
||||
$bank->save();
|
||||
info('Auth success: Cyclos password has been rehashed for next login');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the provided password matches the hashed password in the database
|
||||
if (\Hash::check($request->password, $bank->password)) {
|
||||
|
||||
$this->switchGuard('bank', $bank); // Log in as bank
|
||||
|
||||
// Remove intended switch from session
|
||||
session()->forget(['intended_profile_switch_type', 'intended_profile_switch_id']);
|
||||
|
||||
// Set active profile session as before
|
||||
session([
|
||||
'activeProfileType' => get_class($bank),
|
||||
'activeProfileId' => $bank->id,
|
||||
'activeProfileName' => $bank->name,
|
||||
'activeProfilePhoto' => $bank->profile_photo_path,
|
||||
'activeProfileRoleLabel' => getProfileRoleLabel($bank, $user),
|
||||
'last_activity' => now(),
|
||||
'profile-switched-notification' => true,
|
||||
]);
|
||||
|
||||
|
||||
// Re-activate profile if inactive
|
||||
if (timebank_config('profile_inactive.re-activate_at_login')) {
|
||||
if (!$bank->isActive()) {
|
||||
$bank->inactive_at = null;
|
||||
$bank->save();
|
||||
info('Bank re-activated: ' . $bank->name);
|
||||
}
|
||||
}
|
||||
|
||||
event(new \App\Events\ProfileSwitchEvent($bank));
|
||||
|
||||
// Check for intended URL from direct login flow
|
||||
$intendedUrl = session('bank_login_intended_url');
|
||||
if ($intendedUrl) {
|
||||
session()->forget('bank_login_intended_url');
|
||||
return redirect($intendedUrl);
|
||||
}
|
||||
|
||||
return redirect()->route('main'); // TODO: Or special bank main page
|
||||
}
|
||||
|
||||
info('Auth failed: Input password does not match stored password');
|
||||
return back()->withErrors(['password' => __('Invalid bank password')]);
|
||||
}
|
||||
|
||||
//TODO: Move to Trait as suggested below.
|
||||
/**
|
||||
* Helper method to get the target profile model based on the index.
|
||||
* Note: This duplicates logic from ProfileSelect::mount. Consider refactoring
|
||||
* this logic into a service or trait if used in multiple places.
|
||||
*/
|
||||
private function getTargetProfileByTypeAndId($user, $type, $id)
|
||||
{
|
||||
if (!$type || !$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userWithRelations = User::with(['organizations', 'banksManaged', 'admins'])->find($user->id);
|
||||
if (!$userWithRelations) return null;
|
||||
|
||||
if (strtolower($type) === 'organization') {
|
||||
return $userWithRelations->organizations->firstWhere('id', $id);
|
||||
} elseif (strtolower($type) === 'bank') {
|
||||
return $userWithRelations->banksManaged->firstWhere('id', $id);
|
||||
} elseif (strtolower($type) === 'admin') {
|
||||
return $userWithRelations->admins->firstWhere('id', $id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function profileCanResetPassword($user, string $profileType, $profileId): bool
|
||||
{
|
||||
if (!$user || !$profileId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix = "{$profileType}\\{$profileId}\\";
|
||||
$scopedRoles = $user->roles->filter(fn($r) => str_starts_with($r->name, $prefix));
|
||||
|
||||
foreach ($scopedRoles as $spatieRole) {
|
||||
$roleName = str_replace($prefix, '', $spatieRole->name);
|
||||
$orgRole = \App\Models\OrganizationRole::where('name', $roleName)->first();
|
||||
if ($orgRole && in_array('org change-password', $orgRole->permissions ?? [])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user