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

222 lines
8.6 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Events\ProfileSwitchEvent;
use App\Models\Organization;
use App\Models\User;
use App\Traits\SwitchGuardTrait;
use Hash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class OrganizationLoginController extends Controller
{
use SwitchGuardTrait;
/**
* Direct link to organization profile - can be used in emails
* Organizations don't require password re-authentication
* Handles the authentication flow:
* 1. If user not authenticated -> redirect to user login first
* 2. If user authenticated -> verify access and switch to organization
* 3. Redirect to intended URL or main page
*/
public function directLogin(Request $request, $organizationId)
{
// Validate organization exists and load managers
$organization = Organization::with('managers')->find($organizationId);
if (!$organization) {
abort(404, __('Organization not found'));
}
// Get optional intended destination after successful org switch
$intendedUrl = $request->query('intended');
// 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('organization.direct-login', ['organizationId' => $organizationId]);
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 = $organization->managers()->first();
\Log::info('OrganizationLoginController: Redirecting unauthenticated user', [
'organization_id' => $organizationId,
'manager_found' => $firstManager ? 'yes' : 'no',
'manager_name' => $firstManager ? $firstManager->name : null,
]);
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);
\Log::info('OrganizationLoginController: Redirect URL', [
'url' => $loginUrl,
]);
return redirect($loginUrl);
}
return redirect()->route('login');
}
// User is authenticated - verify they own/manage this organization
$user = Auth::guard('web')->user();
$userWithRelations = User::with('organizations')->find($user->id);
if (!$userWithRelations || !$userWithRelations->organizations->contains('id', $organizationId)) {
abort(403, __('You do not have access to this organization'));
}
// Switch to organization guard directly (no password required for organizations)
$this->switchGuard('organization', $organization);
// Set active profile session
session([
'activeProfileType' => get_class($organization),
'activeProfileId' => $organization->id,
'activeProfileName' => $organization->name,
'activeProfilePhoto' => $organization->profile_photo_path,
'last_activity' => now(),
'profile-switched-notification' => true,
]);
// Re-activate profile if inactive
if (timebank_config('profile_inactive.re-activate_at_login')) {
if (!$organization->isActive()) {
$organization->inactive_at = null;
$organization->save();
info('Organization re-activated: ' . $organization->name);
}
}
// Broadcast profile switch event
event(new \App\Events\ProfileSwitchEvent($organization));
// Redirect to intended URL or main page
if ($intendedUrl) {
return redirect($intendedUrl);
}
return redirect()->route('main');
}
public function showLoginForm()
{
$user = Auth::guard('web')->user();
$type = session('intended_profile_switch_type');
$id = session('intended_profile_switch_id');
$profile = $this->getTargetProfileByTypeAndId($user, $type, $id);
return view('profile-organization.login', ['profile' => $profile]);
}
public function login(Request $request)
{
$request->validate([
'password' => 'required',
]);
$user = Auth::guard('web')->user();
$type = session('intended_profile_switch_type');
$id = session('intended_profile_switch_id');
$organization = $this->getTargetProfileByTypeAndId($user, $type, $id);
if (!$organization) {
return back()->withErrors(['index' => __('Organization not found')]);
}
// Legacy Cyclos password support
if (!empty($organization->cyclos_salt)) {
info('Auth attempt using original Cyclos password');
$concatenated = $organization->cyclos_salt . $request->password;
$hashedInputPassword = hash("sha256", $concatenated);
if (strtolower($hashedInputPassword) === strtolower($organization->password)) {
info('Auth success: Password is verified');
// Rehash to Laravel hash and remove salt
$organization->password = \Hash::make($request->password);
$organization->cyclos_salt = null;
$organization->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, $organization->password)) {
$this->switchGuard('organization', $organization); // log in as organization
// 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($organization),
'activeProfileId' => $organization->id,
'activeProfileName' => $organization->name,
'activeProfilePhoto' => $organization->profile_photo_path,
'last_activity' => now(),
'profile-switched-notification' => true,
]);
// Re-activate profile if inactive
if (timebank_config('profile_inactive.re-activate_at_login')) {
if (!$organization->isActive()) {
$organization->inactive_at = null;
$organization->save();
info('Organization re-activated: ' . $organization->name);
}
}
event(new \App\Events\ProfileSwitchEvent($organization));
// Check for intended URL from direct login flow
$intendedUrl = session('organization_login_intended_url');
if ($intendedUrl) {
session()->forget('organization_login_intended_url');
return redirect($intendedUrl);
}
return redirect()->route('main'); // Or your special organization main page
}
info('Auth failed: Input password does not match stored password');
return back()->withErrors(['password' => __('Invalid organization 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;
}
}