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,243 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Auth\DockerSessionGuard;
|
||||
use App\Models\Bank;
|
||||
use App\Policies\BankPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
// Set remember me duration from platform configuration
|
||||
$rememberMeDays = timebank_config('auth.remember_me_days', 90);
|
||||
$rememberMeMinutes = $rememberMeDays * 24 * 60; // Convert days to minutes
|
||||
|
||||
// Use custom guard in Docker that doesn't migrate sessions
|
||||
if (env('IS_DOCKER', false)) {
|
||||
Auth::extend('session', function ($app, $name, array $config) use ($rememberMeMinutes) {
|
||||
$provider = Auth::createUserProvider($config['provider']);
|
||||
$guard = new DockerSessionGuard($name, $provider, $app['session.store']);
|
||||
|
||||
// Set the cookie jar on the guard
|
||||
$guard->setCookieJar($app['cookie']);
|
||||
|
||||
// If a request is available, set it on the guard
|
||||
if (method_exists($guard, 'setRequest')) {
|
||||
$guard->setRequest($app->refresh('request', $guard, 'setRequest'));
|
||||
}
|
||||
|
||||
// Set remember me duration
|
||||
$guard->setRememberDuration($rememberMeMinutes);
|
||||
|
||||
return $guard;
|
||||
});
|
||||
}
|
||||
|
||||
// Set remember me duration for all standard guards
|
||||
foreach (['web', 'organization', 'bank', 'admin'] as $guardName) {
|
||||
$guard = Auth::guard($guardName);
|
||||
if (method_exists($guard, 'setRememberDuration')) {
|
||||
$guard->setRememberDuration($rememberMeMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
// All permissions are strictly scoped to the active profile session.
|
||||
//
|
||||
// Rules:
|
||||
// - Active profile = Admin{id}: only the scoped role Admin\{id}\{roleName}
|
||||
// determines permissions. Roles from other linked admin profiles are ignored.
|
||||
// - Active profile = Organization/Bank: only scoped Org/Bank roles apply.
|
||||
// Admin-level permissions are NOT accessible when acting as Org/Bank.
|
||||
// - Active profile = User or null/unknown: no admin permissions granted.
|
||||
|
||||
// Shared factory: returns a Gate closure that checks $permission on the active
|
||||
// profile's single scoped role. Used for both manage and granular verbs.
|
||||
$makeGate = function (string $permission) {
|
||||
return function ($user) use ($permission) {
|
||||
// Roles live on the User model (web guard). When the active guard is
|
||||
// 'admin' (or another profile guard), Gate receives an Admin/Bank/Org
|
||||
// model instead. Always resolve the web user explicitly.
|
||||
if (!($user instanceof \App\Models\User)) {
|
||||
$user = \Illuminate\Support\Facades\Auth::guard('web')->user();
|
||||
if (!($user instanceof \App\Models\User)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$activeType = session('activeProfileType');
|
||||
$activeId = session('activeProfileId');
|
||||
|
||||
if (!$activeType || !$activeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefixMap = [
|
||||
'Admin' => "Admin\\{$activeId}\\",
|
||||
'Organization' => "Organization\\{$activeId}\\",
|
||||
'Bank' => "Bank\\{$activeId}\\",
|
||||
];
|
||||
|
||||
$activeClass = class_basename($activeType);
|
||||
$prefix = $prefixMap[$activeClass] ?? null;
|
||||
|
||||
if (!$prefix) {
|
||||
return false; // User profile or unknown: no permissions
|
||||
}
|
||||
|
||||
$scopedRoles = $user->roles
|
||||
->filter(fn($r) => str_starts_with($r->name, $prefix));
|
||||
|
||||
if ($scopedRoles->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Grant if any of the user's scoped roles for this profile has the permission.
|
||||
// Also honour the manage→verb implication for granular checks.
|
||||
$managePermission = null;
|
||||
$parts = explode(' ', $permission, 2);
|
||||
if (count($parts) === 2 && $parts[0] !== 'manage') {
|
||||
$managePermission = 'manage ' . $parts[1];
|
||||
}
|
||||
|
||||
foreach ($scopedRoles as $scopedRole) {
|
||||
try {
|
||||
if ($scopedRole->hasPermissionTo($permission, 'web')) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
|
||||
// permission doesn't exist in DB, skip
|
||||
}
|
||||
|
||||
if ($managePermission) {
|
||||
try {
|
||||
if ($scopedRole->hasPermissionTo($managePermission, 'web')) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
|
||||
// manage permission doesn't exist, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// manage {resource} — umbrella permission (checked by route middleware + mount())
|
||||
foreach ([
|
||||
'banks', 'organizations', 'admins', 'calls', 'posts', 'polls', 'tags',
|
||||
'categories', 'mailings', 'profiles', 'roles', 'permissions',
|
||||
'users', 'user_projects', 'accounts', 'maintenance',
|
||||
] as $resource) {
|
||||
Gate::define("manage {$resource}", $makeGate("manage {$resource}"));
|
||||
}
|
||||
|
||||
// Granular action Gates — checked inside individual Livewire methods and blade views.
|
||||
// Each resource lists only the verbs that make sense for it.
|
||||
$granular = [
|
||||
'posts' => ['view', 'create', 'update', 'publish', 'unpublish', 'delete'],
|
||||
// Polls: 'export' covers who voted (Results tab) and the CSV export
|
||||
'polls' => ['view', 'create', 'update', 'export', 'delete'],
|
||||
'tags' => ['view', 'create', 'update', 'delete'],
|
||||
'categories' => ['view', 'create', 'update', 'delete'],
|
||||
'calls' => ['view', 'create', 'update', 'publish', 'unpublish', 'delete'],
|
||||
'mailings' => ['view', 'create', 'update', 'send', 'delete'],
|
||||
'users' => ['view', 'create', 'update', 'delete'],
|
||||
'user_projects' => ['view', 'create', 'update', 'delete'],
|
||||
'organizations' => ['view', 'create', 'update', 'delete'],
|
||||
'banks' => ['view', 'create', 'update', 'delete'],
|
||||
'admins' => ['view', 'create', 'update', 'delete'],
|
||||
'accounts' => ['view', 'create', 'update', 'delete'],
|
||||
'profiles' => ['view', 'create', 'update', 'delete'],
|
||||
'permissions' => ['view', 'create', 'update', 'delete'],
|
||||
'roles' => ['view', 'create', 'update', 'delete'],
|
||||
'search-metrics' => ['view'],
|
||||
'application-log' => ['view'],
|
||||
'inactive-profiles-log' => ['view'],
|
||||
'search-reindex-log' => ['view'],
|
||||
'stats' => ['view'],
|
||||
];
|
||||
|
||||
// Org/bank capability gates — checked inside org-context Livewire components.
|
||||
foreach (['pay', 'transactions', 'reports', 'stats', 'contacts', 'calls', 'messenger', 'edit-profile', 'settings', 'change-password'] as $cap) {
|
||||
Gate::define("org {$cap}", $makeGate("org {$cap}"));
|
||||
}
|
||||
|
||||
// Admin capability gates — checked inside admin-context components.
|
||||
foreach (['reports', 'contacts', 'messenger', 'settings', 'change-password'] as $cap) {
|
||||
Gate::define("admin {$cap}", $makeGate("admin {$cap}"));
|
||||
}
|
||||
|
||||
foreach ($granular as $resource => $verbs) {
|
||||
foreach ($verbs as $verb) {
|
||||
Gate::define("{$verb} {$resource}", $makeGate("{$verb} {$resource}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Regular users can always manage their own calls — no role/permission needed.
|
||||
// Ownership enforcement is done at the component level (query scoped to own calls).
|
||||
$userCallVerbs = ['create', 'update', 'publish', 'unpublish', 'delete'];
|
||||
foreach ($userCallVerbs as $verb) {
|
||||
Gate::define("{$verb} calls", function ($user) use ($verb, $makeGate) {
|
||||
$activeType = session('activeProfileType');
|
||||
if ($activeType && class_basename($activeType) === 'User') {
|
||||
return true;
|
||||
}
|
||||
return $makeGate("{$verb} calls")($user);
|
||||
});
|
||||
}
|
||||
|
||||
// access {resource} — page-level gate used on routes.
|
||||
// 'view {resource}' grants read-only page entry; 'manage {resource}' grants full access.
|
||||
// Roles without any permission for a resource cannot reach the page.
|
||||
$accessResources = array_keys($granular);
|
||||
foreach ($accessResources as $resource) {
|
||||
Gate::define("access {$resource}", function ($user) use ($resource, $makeGate) {
|
||||
return $makeGate("manage {$resource}")($user) || $makeGate("view {$resource}")($user);
|
||||
});
|
||||
}
|
||||
|
||||
// Route-level gate for the scoped bank-manager "manage accounts" surface
|
||||
// (Profiles\Manage.php, non-central banks). Bank-scoped roles created via
|
||||
// Roles\Manage.php only ever carry 'org *' style capability permissions —
|
||||
// never the Admin-style 'manage accounts' permission the granular gates
|
||||
// above check for — so this checks bank-manager role membership directly,
|
||||
// the same convention getCanCreatePayments() already uses for banks.
|
||||
Gate::define('bank manage accounts', function ($user) {
|
||||
if (!($user instanceof \App\Models\User)) {
|
||||
$user = \Illuminate\Support\Facades\Auth::guard('web')->user();
|
||||
if (!($user instanceof \App\Models\User)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$activeType = session('activeProfileType');
|
||||
$activeId = session('activeProfileId');
|
||||
|
||||
if ($activeType !== \App\Models\Bank::class || !$activeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole("Bank\\{$activeId}\\bank-manager");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user