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,985 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
class DatabaseUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'database:update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Apply database updates for schema changes and data migrations';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting database updates...');
|
||||
|
||||
// Apply all update methods
|
||||
$this->renameAssociationToPlatformOrganization();
|
||||
$this->addLocationNotSpecifiedCountry();
|
||||
$this->addManageCallsPermission();
|
||||
$this->createAdminRolesTable();
|
||||
$this->createOrganizationRolesTable();
|
||||
$this->addGranularPermissions();
|
||||
$this->seedAdminRolePresets();
|
||||
$this->updateAdministratorRolePermissions();
|
||||
$this->migrateFlatAdminRoleToScoped();
|
||||
$this->replaceLegacyAdminRoles();
|
||||
$this->migrateHardcodedOrgRolesToOrganizationRoles();
|
||||
$this->fixCoordinatorSystemRoles();
|
||||
$this->addPasswordChangeLockColumns();
|
||||
$this->addChangePasswordToManagerRoles();
|
||||
$this->addAdminCapabilityPermissions();
|
||||
$this->fixReversalTransactionTypeIcon();
|
||||
$this->addPollCategory();
|
||||
$this->addPollPermissions();
|
||||
|
||||
$this->info('Database updates completed successfully!');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename 'Association' to 'Timebank Organization' across the database
|
||||
* Updates categories table from Association/PlatformOrganization to TimebankOrganization
|
||||
* Updates category_translations table with proper translations
|
||||
*/
|
||||
private function renameAssociationToPlatformOrganization()
|
||||
{
|
||||
$this->info('Renaming Association to Timebank Organization...');
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Update categories table: change type from Association to TimebankOrganization
|
||||
$categoriesUpdated = DB::table('categories')
|
||||
->where('type', 'SiteContents\\Static\\Association')
|
||||
->update(['type' => 'SiteContents\\Static\\TimebankOrganization']);
|
||||
|
||||
if ($categoriesUpdated > 0) {
|
||||
$this->info(" ✓ Updated {$categoriesUpdated} category record(s) from Association");
|
||||
}
|
||||
|
||||
// Also update any PlatformOrganization entries to TimebankOrganization
|
||||
$platformUpdated = DB::table('categories')
|
||||
->where('type', 'SiteContents\\Static\\PlatformOrganization')
|
||||
->update(['type' => 'SiteContents\\Static\\TimebankOrganization']);
|
||||
|
||||
if ($platformUpdated > 0) {
|
||||
$this->info(" ✓ Updated {$platformUpdated} category record(s) from PlatformOrganization");
|
||||
}
|
||||
|
||||
if ($categoriesUpdated === 0 && $platformUpdated === 0) {
|
||||
$this->warn(' ℹ No categories found to update');
|
||||
}
|
||||
|
||||
// Update category_translations table
|
||||
// Get the category ID for TimebankOrganization
|
||||
$category = DB::table('categories')
|
||||
->where('type', 'SiteContents\\Static\\TimebankOrganization')
|
||||
->first();
|
||||
|
||||
if ($category) {
|
||||
$translations = [
|
||||
'en' => [
|
||||
'name' => 'Timebank organization page',
|
||||
'slug' => 'timebank-organization-page',
|
||||
],
|
||||
'nl' => [
|
||||
'name' => 'Timebank organisatie pagina',
|
||||
'slug' => 'timebank-organisatie-pagina',
|
||||
],
|
||||
'de' => [
|
||||
'name' => 'Timebank-Organisation Seite',
|
||||
'slug' => 'timebank-organisation-seite',
|
||||
],
|
||||
'es' => [
|
||||
'name' => 'Organización de Timebank página',
|
||||
'slug' => 'organizacion-de-timebank-pagina',
|
||||
],
|
||||
'fr' => [
|
||||
'name' => 'Organisation de Timebank page',
|
||||
'slug' => 'organisation-de-timebank-page',
|
||||
],
|
||||
];
|
||||
|
||||
$translationsUpdated = 0;
|
||||
foreach ($translations as $locale => $data) {
|
||||
$updated = DB::table('category_translations')
|
||||
->where('category_id', $category->id)
|
||||
->where('locale', $locale)
|
||||
->update([
|
||||
'name' => $data['name'],
|
||||
'slug' => $data['slug'],
|
||||
]);
|
||||
|
||||
$translationsUpdated += $updated;
|
||||
}
|
||||
|
||||
if ($translationsUpdated > 0) {
|
||||
$this->info(" ✓ Updated {$translationsUpdated} category translation(s) (name and slug)");
|
||||
} else {
|
||||
$this->warn(' ℹ No category translations found to update');
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->info(' ✓ Renamed to Timebank Organization successfully');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
$this->error(' ✗ Failed to rename: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the "Location not specified" placeholder country (id=10, code=XX) with translations.
|
||||
* Used for profiles whose Cyclos country was unmapped (code 863).
|
||||
*/
|
||||
private function addLocationNotSpecifiedCountry()
|
||||
{
|
||||
$this->info('Adding "Location not specified" placeholder country...');
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
DB::table('countries')->upsert(
|
||||
[['id' => 10, 'code' => 'XX', 'flag' => '🌐', 'phonecode' => '']],
|
||||
['id']
|
||||
);
|
||||
|
||||
$locales = [
|
||||
['id' => 37, 'locale' => 'en', 'name' => '~ Location not specified'],
|
||||
['id' => 38, 'locale' => 'nl', 'name' => '~ Locatie niet opgegeven'],
|
||||
['id' => 39, 'locale' => 'fr', 'name' => '~ Emplacement non précisé'],
|
||||
['id' => 40, 'locale' => 'es', 'name' => '~ Ubicación no especificada'],
|
||||
['id' => 41, 'locale' => 'de', 'name' => '~ Standort nicht angegeben'],
|
||||
];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
DB::table('country_locales')->upsert(
|
||||
[['id' => $locale['id'], 'country_id' => 10, 'name' => $locale['name'], 'alias' => null, 'locale' => $locale['locale']]],
|
||||
['id']
|
||||
);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->info(' ✓ "Location not specified" country added/updated');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
$this->error(' ✗ Failed to add location not specified country: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function addManageCallsPermission()
|
||||
{
|
||||
$this->info('Adding "manage calls" permission...');
|
||||
|
||||
try {
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
$permission = Permission::firstOrCreate(['name' => 'manage calls', 'guard_name' => 'web']);
|
||||
|
||||
foreach (['create calls', 'update calls', 'delete calls'] as $name) {
|
||||
Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
|
||||
}
|
||||
|
||||
// Give permission to existing site-editor and admin roles
|
||||
foreach (['site-editor', 'admin'] as $roleName) {
|
||||
$role = Role::findByName($roleName, 'web');
|
||||
if ($role && !$role->hasPermissionTo('manage calls')) {
|
||||
$role->givePermissionTo($permission);
|
||||
$this->info(" ✓ Gave 'manage calls' to role '{$roleName}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Give permission to all scoped Admin\{id}\site-editor roles
|
||||
$scopedRoles = Role::where('name', 'like', 'Admin\\%\\site-editor')->get();
|
||||
foreach ($scopedRoles as $scopedRole) {
|
||||
if (!$scopedRole->hasPermissionTo('manage calls')) {
|
||||
$scopedRole->givePermissionTo($permission);
|
||||
$this->info(" ✓ Gave 'manage calls' to scoped role '{$scopedRole->name}'");
|
||||
}
|
||||
}
|
||||
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
$this->info(' ✓ "manage calls" permission added/updated');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to add manage calls permission: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function addGranularPermissions()
|
||||
{
|
||||
$this->info('Adding granular permissions (view/publish/unpublish/send)...');
|
||||
|
||||
try {
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
$newPermissions = [
|
||||
// view permissions for all resources
|
||||
'view posts', 'view tags', 'view categories', 'view calls',
|
||||
'view mailings', 'view users', 'view user_projects',
|
||||
'view organizations', 'view banks', 'view admins', 'view accounts',
|
||||
'view profiles', 'view permissions', 'view roles',
|
||||
// publish/unpublish calls
|
||||
'publish calls', 'unpublish calls',
|
||||
// publish/unpublish posts
|
||||
'publish posts', 'unpublish posts',
|
||||
// send mailings (irreversible dispatch)
|
||||
'send mailings',
|
||||
// additional granular actions already in seeder but possibly missing
|
||||
'create mailings', 'update mailings', 'delete mailings',
|
||||
'create calls', 'update calls', 'delete calls',
|
||||
'create posts', 'update posts', 'delete posts',
|
||||
'create tags', 'update tags', 'delete tags',
|
||||
'create categories', 'update categories', 'delete categories',
|
||||
'create users', 'update users', 'delete users',
|
||||
'create user_projects', 'update user_projects', 'delete user_projects',
|
||||
'create organizations', 'update organizations', 'delete organizations',
|
||||
'create banks', 'update banks', 'delete banks',
|
||||
'create admins', 'update admins', 'delete admins',
|
||||
'create accounts', 'update accounts', 'delete accounts',
|
||||
'create profiles', 'update profiles', 'delete profiles',
|
||||
'create permissions', 'update permissions', 'delete permissions',
|
||||
'create roles', 'update roles', 'delete roles',
|
||||
'manage maintenance',
|
||||
'view search-metrics', 'view application-log', 'view inactive-profiles-log', 'view search-reindex-log',
|
||||
'view stats',
|
||||
// Org/bank capability permissions
|
||||
'org pay', 'org transactions', 'org reports', 'org stats', 'org contacts',
|
||||
'org calls', 'org messenger', 'org edit-profile', 'org settings', 'org change-password',
|
||||
// Admin profile capability permissions
|
||||
'admin reports', 'admin contacts', 'admin messenger', 'admin settings', 'admin change-password',
|
||||
];
|
||||
|
||||
$created = 0;
|
||||
foreach ($newPermissions as $name) {
|
||||
$permission = Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
|
||||
if ($permission->wasRecentlyCreated) {
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
// Give all permissions to the flat admin and super-admin Spatie roles
|
||||
foreach (['admin', 'super-admin'] as $roleName) {
|
||||
$role = Role::where('name', $roleName)->where('guard_name', 'web')->first();
|
||||
if ($role) {
|
||||
$role->givePermissionTo(Permission::where('guard_name', 'web')->get());
|
||||
}
|
||||
}
|
||||
|
||||
// Give view+create+update posts/tags/categories/calls to flat site-editor role
|
||||
$siteEditorRole = Role::where('name', 'site-editor')->where('guard_name', 'web')->first();
|
||||
if ($siteEditorRole) {
|
||||
$siteEditorPerms = [
|
||||
'view posts', 'create posts', 'update posts', 'publish posts', 'unpublish posts', 'delete posts', 'manage posts',
|
||||
'view tags', 'create tags', 'update tags', 'delete tags', 'manage tags',
|
||||
'view categories', 'create categories', 'update categories', 'delete categories', 'manage categories',
|
||||
'view calls', 'create calls', 'update calls', 'publish calls', 'unpublish calls', 'delete calls', 'manage calls',
|
||||
];
|
||||
foreach ($siteEditorPerms as $permName) {
|
||||
$perm = Permission::where('name', $permName)->where('guard_name', 'web')->first();
|
||||
if ($perm && !$siteEditorRole->hasPermissionTo($permName)) {
|
||||
$siteEditorRole->givePermissionTo($perm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync all scoped Admin\{id}\{role} Spatie roles from AdminRole definitions
|
||||
if (\Schema::hasTable('admin_roles')) {
|
||||
$adminRoles = \App\Models\AdminRole::all();
|
||||
$allSpatieRoles = Role::where('guard_name', 'web')->get();
|
||||
foreach ($adminRoles as $adminRole) {
|
||||
$suffix = '\\' . $adminRole->name;
|
||||
$spatieRoles = $allSpatieRoles->filter(fn($r) => str_ends_with($r->name, $suffix));
|
||||
foreach ($spatieRoles as $spatieRole) {
|
||||
$spatieRole->syncPermissions($adminRole->permissions ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
$this->info(" ✓ Added {$created} new permission(s), synced scoped roles");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to add granular permissions: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function updateAdministratorRolePermissions()
|
||||
{
|
||||
$this->info('Updating administrator role with full granular permissions...');
|
||||
|
||||
if (!\Schema::hasTable('admin_roles')) {
|
||||
$this->warn(' ℹ admin_roles table not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have UPDATE privilege on admin_roles
|
||||
try {
|
||||
DB::statement('UPDATE admin_roles SET updated_at = updated_at WHERE id = 0');
|
||||
} catch (\Exception $e) {
|
||||
$this->warn(' ℹ Insufficient privileges to update admin_roles. Run manually:');
|
||||
$this->warn(' sudo mysql -e "GRANT UPDATE, DELETE ON ' . config('database.connections.mysql.database') . '.admin_roles TO \''. config('database.connections.mysql.username') . '\'@\'localhost\'; FLUSH PRIVILEGES;"');
|
||||
$this->warn(' Then re-run: php artisan database:update');
|
||||
return;
|
||||
}
|
||||
|
||||
$allPermissions = [
|
||||
// Posts
|
||||
'view posts', 'create posts', 'update posts', 'publish posts', 'unpublish posts', 'delete posts', 'manage posts',
|
||||
// Tags
|
||||
'view tags', 'create tags', 'update tags', 'delete tags', 'manage tags',
|
||||
// Categories
|
||||
'view categories', 'create categories', 'update categories', 'delete categories', 'manage categories',
|
||||
// Calls
|
||||
'view calls', 'create calls', 'update calls', 'publish calls', 'unpublish calls', 'delete calls', 'manage calls',
|
||||
// Mailings
|
||||
'view mailings', 'create mailings', 'update mailings', 'send mailings', 'delete mailings', 'manage mailings',
|
||||
// Users
|
||||
'view users', 'create users', 'update users', 'delete users', 'manage users',
|
||||
// User projects
|
||||
'view user_projects', 'create user_projects', 'update user_projects', 'delete user_projects', 'manage user_projects',
|
||||
// Organizations
|
||||
'view organizations', 'create organizations', 'update organizations', 'delete organizations', 'manage organizations',
|
||||
// Banks
|
||||
'view banks', 'create banks', 'update banks', 'delete banks', 'manage banks',
|
||||
// Admins
|
||||
'view admins', 'create admins', 'update admins', 'delete admins', 'manage admins',
|
||||
// Accounts
|
||||
'view accounts', 'create accounts', 'update accounts', 'delete accounts', 'manage accounts',
|
||||
// Profiles
|
||||
'view profiles', 'create profiles', 'update profiles', 'delete profiles', 'manage profiles',
|
||||
// Permissions
|
||||
'view permissions', 'create permissions', 'update permissions', 'delete permissions', 'manage permissions',
|
||||
// Roles
|
||||
'view roles', 'create roles', 'update roles', 'delete roles', 'manage roles',
|
||||
// Maintenance & admin tools
|
||||
'manage maintenance',
|
||||
'view search-metrics', 'view application-log', 'view inactive-profiles-log', 'view search-reindex-log',
|
||||
'view stats',
|
||||
// Admin profile capabilities
|
||||
'admin reports', 'admin contacts', 'admin messenger', 'admin settings', 'admin change-password',
|
||||
];
|
||||
|
||||
try {
|
||||
$adminRole = \App\Models\AdminRole::where('name', 'administrator')->first();
|
||||
if (!$adminRole) {
|
||||
$this->warn(' ℹ administrator AdminRole not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$adminRole->permissions = $allPermissions;
|
||||
$adminRole->save();
|
||||
|
||||
// Sync all scoped Admin\{id}\administrator Spatie roles
|
||||
$spatieRoles = Role::where('guard_name', 'web')->get()
|
||||
->filter(fn($r) => str_ends_with($r->name, '\\administrator'));
|
||||
foreach ($spatieRoles as $spatieRole) {
|
||||
$spatieRole->syncPermissions($allPermissions);
|
||||
$this->info(" ✓ Synced permissions for scoped role '{$spatieRole->name}'");
|
||||
}
|
||||
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
$this->info(' ✓ administrator AdminRole permissions updated (' . count($allPermissions) . ' permissions)');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to update administrator role: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the legacy roles Admin\{id}\admin by the default role from admin_roles (see admin-roles:migrate).
|
||||
* Runs after migrateFlatAdminRoleToScoped(), so its administrator roles already exist.
|
||||
*/
|
||||
private function replaceLegacyAdminRoles()
|
||||
{
|
||||
$this->info('Replacing legacy Admin\\{id}\\admin roles...');
|
||||
|
||||
try {
|
||||
$this->call('admin-roles:migrate');
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(' ℹ Skipped: ' . $e->getMessage() . ' (run php artisan admin-roles:migrate --dry-run)');
|
||||
}
|
||||
}
|
||||
|
||||
private function migrateFlatAdminRoleToScoped()
|
||||
{
|
||||
$this->info('Migrating flat admin role to scoped roles...');
|
||||
|
||||
try {
|
||||
$flatAdminRole = Role::where('name', 'admin')->where('guard_name', 'web')->first();
|
||||
if (!$flatAdminRole) {
|
||||
$this->info(' ℹ No flat admin role found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find all users with the flat admin role
|
||||
$userIds = DB::table('model_has_roles')
|
||||
->where('role_id', $flatAdminRole->id)
|
||||
->where('model_type', 'App\Models\User')
|
||||
->pluck('model_id');
|
||||
|
||||
if ($userIds->isEmpty()) {
|
||||
$this->info(' ℹ No users with flat admin role, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$administratorAdminRole = \App\Models\AdminRole::where('name', 'administrator')->first();
|
||||
if (!$administratorAdminRole) {
|
||||
$this->warn(' ℹ administrator AdminRole not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$migrated = 0;
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
$user = \App\Models\User::find($userId);
|
||||
if (!$user) continue;
|
||||
|
||||
// Find all admin profiles linked to this user
|
||||
$adminProfiles = \App\Models\Admin::whereHas('users', fn($q) => $q->where('users.id', $userId))->get();
|
||||
|
||||
// Without a link nothing is granted: guessing would give administrator on every admin profile
|
||||
if ($adminProfiles->isEmpty()) {
|
||||
$this->warn(" ℹ User {$user->name} (id:{$userId}) has the flat admin role but no admin profile link: no role assigned");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($adminProfiles as $admin) {
|
||||
$scopedRoleName = 'Admin\\' . $admin->id . '\\administrator';
|
||||
|
||||
// Get or create the scoped Spatie role
|
||||
$scopedRole = Role::firstOrCreate(
|
||||
['name' => $scopedRoleName, 'guard_name' => 'web'],
|
||||
);
|
||||
|
||||
// Sync permissions from AdminRole definition
|
||||
$scopedRole->syncPermissions($administratorAdminRole->permissions ?? []);
|
||||
|
||||
// Assign to user if not already assigned
|
||||
$exists = DB::table('model_has_roles')
|
||||
->where('role_id', $scopedRole->id)
|
||||
->where('model_id', $userId)
|
||||
->where('model_type', 'App\Models\User')
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
DB::table('model_has_roles')->insert([
|
||||
'role_id' => $scopedRole->id,
|
||||
'model_id' => $userId,
|
||||
'model_type' => 'App\Models\User',
|
||||
]);
|
||||
$this->info(" ✓ Assigned {$scopedRoleName} to user {$user->name} (id:{$userId})");
|
||||
$migrated++;
|
||||
} else {
|
||||
$this->info(" ℹ User {$user->name} already has {$scopedRoleName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
$this->info(" ✓ Migrated {$migrated} scoped role assignment(s)");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to migrate flat admin role: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function seedAdminRolePresets()
|
||||
{
|
||||
$this->info('Seeding admin role presets...');
|
||||
|
||||
if (!\Schema::hasTable('admin_roles')) {
|
||||
$this->warn(' ℹ admin_roles table not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$presets = [
|
||||
[
|
||||
'name' => 'content-reviewer',
|
||||
'label' => 'Content Reviewer',
|
||||
'permissions' => ['view posts', 'view tags', 'view categories', 'view calls'],
|
||||
'is_system' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'content-editor',
|
||||
'label' => 'Content Editor',
|
||||
'permissions' => [
|
||||
'view posts', 'create posts', 'update posts',
|
||||
'view tags', 'create tags', 'update tags',
|
||||
'view categories', 'create categories', 'update categories',
|
||||
'view calls', 'create calls', 'update calls',
|
||||
],
|
||||
'is_system' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'content-publisher',
|
||||
'label' => 'Content Publisher',
|
||||
'permissions' => [
|
||||
'view posts', 'create posts', 'update posts', 'publish posts', 'unpublish posts',
|
||||
'view tags', 'create tags', 'update tags',
|
||||
'view categories', 'create categories', 'update categories',
|
||||
'view calls', 'create calls', 'update calls', 'publish calls', 'unpublish calls',
|
||||
],
|
||||
'is_system' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'mailing-editor',
|
||||
'label' => 'Mailing Editor',
|
||||
'permissions' => ['view mailings', 'create mailings', 'update mailings'],
|
||||
'is_system' => false,
|
||||
],
|
||||
];
|
||||
|
||||
$seeded = 0;
|
||||
foreach ($presets as $preset) {
|
||||
$exists = DB::table('admin_roles')->where('name', $preset['name'])->exists();
|
||||
if (!$exists) {
|
||||
DB::table('admin_roles')->insert([
|
||||
'name' => $preset['name'],
|
||||
'label' => $preset['label'],
|
||||
'permissions' => json_encode($preset['permissions']),
|
||||
'is_system' => $preset['is_system'],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$seeded++;
|
||||
$this->info(" ✓ Seeded role preset '{$preset['name']}'");
|
||||
}
|
||||
}
|
||||
|
||||
if ($seeded === 0) {
|
||||
$this->warn(' ℹ All role presets already exist, skipping');
|
||||
}
|
||||
}
|
||||
|
||||
private function createOrganizationRolesTable()
|
||||
{
|
||||
$this->info('Creating organization_roles table...');
|
||||
|
||||
try {
|
||||
if (\Schema::hasTable('organization_roles')) {
|
||||
$this->warn(' ℹ organization_roles table already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
\Schema::create('organization_roles', function (\Illuminate\Database\Schema\Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('label');
|
||||
$table->json('permissions');
|
||||
$table->boolean('is_system')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
$managerPermissions = ['org pay', 'org transactions', 'org reports', 'org contacts', 'org calls', 'org messenger', 'org edit-profile', 'org settings', 'org change-password'];
|
||||
$coordinatorPermissions = ['org transactions', 'org reports', 'org contacts', 'org calls', 'org messenger', 'org edit-profile', 'org settings'];
|
||||
|
||||
DB::table('organization_roles')->insert([
|
||||
[
|
||||
'name' => 'organization-manager',
|
||||
'label' => 'Organization Manager',
|
||||
'permissions' => json_encode($managerPermissions),
|
||||
'is_system' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'organization-coordinator',
|
||||
'label' => 'Organization Coordinator',
|
||||
'permissions' => json_encode($coordinatorPermissions),
|
||||
'is_system' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'bank-manager',
|
||||
'label' => 'Bank Manager',
|
||||
'permissions' => json_encode($managerPermissions),
|
||||
'is_system' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'bank-coordinator',
|
||||
'label' => 'Bank Coordinator',
|
||||
'permissions' => json_encode($coordinatorPermissions),
|
||||
'is_system' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->info(' ✓ organization_roles table created and seeded');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to create organization_roles table: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function addChangePasswordToManagerRoles()
|
||||
{
|
||||
$this->info('Adding org change-password to manager roles...');
|
||||
|
||||
try {
|
||||
if (!\Schema::hasTable('organization_roles')) {
|
||||
$this->warn(' ℹ organization_roles table not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$managers = \App\Models\OrganizationRole::whereIn('name', ['organization-manager', 'bank-manager'])->get();
|
||||
$updated = 0;
|
||||
|
||||
foreach ($managers as $role) {
|
||||
$permissions = $role->permissions ?? [];
|
||||
if (!in_array('org change-password', $permissions)) {
|
||||
$permissions[] = 'org change-password';
|
||||
$role->permissions = $permissions;
|
||||
$role->save();
|
||||
$this->info(" ✓ Added org change-password to '{$role->name}'");
|
||||
$updated++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($updated === 0) {
|
||||
$this->warn(' ℹ Manager roles already have org change-password, skipping');
|
||||
}
|
||||
|
||||
// Re-sync scoped Spatie roles so the permission takes effect
|
||||
if ($updated > 0) {
|
||||
$allSpatieRoles = \Spatie\Permission\Models\Role::where('guard_name', 'web')->get();
|
||||
foreach ($managers->fresh() as $orgRole) {
|
||||
$suffix = '\\' . $orgRole->name;
|
||||
$spatieRoles = $allSpatieRoles->filter(fn($r) => str_ends_with($r->name, $suffix));
|
||||
foreach ($spatieRoles as $spatieRole) {
|
||||
$spatieRole->syncPermissions($orgRole->permissions ?? []);
|
||||
$this->info(" ✓ Synced '{$spatieRole->name}'");
|
||||
}
|
||||
}
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to add org change-password to manager roles: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function fixCoordinatorSystemRoles()
|
||||
{
|
||||
$this->info('Fixing coordinator roles is_system flag...');
|
||||
|
||||
try {
|
||||
if (!\Schema::hasTable('organization_roles')) {
|
||||
$this->warn(' ℹ organization_roles table not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$updated = DB::table('organization_roles')
|
||||
->whereIn('name', ['organization-coordinator', 'bank-coordinator'])
|
||||
->where('is_system', true)
|
||||
->update(['is_system' => false]);
|
||||
|
||||
if ($updated > 0) {
|
||||
$this->info(" ✓ Set {$updated} coordinator role(s) to is_system=false");
|
||||
} else {
|
||||
$this->warn(' ℹ Coordinator roles already correct, skipping');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to fix coordinator roles: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function addPasswordChangeLockColumns()
|
||||
{
|
||||
$this->info('Adding password_change_disabled column...');
|
||||
|
||||
try {
|
||||
foreach (['users', 'admins', 'banks'] as $tableName) {
|
||||
if (\Schema::hasColumn($tableName, 'password_change_disabled')) {
|
||||
$this->warn(" ℹ {$tableName}.password_change_disabled already exists, skipping");
|
||||
continue;
|
||||
}
|
||||
\Schema::table($tableName, function (\Illuminate\Database\Schema\Blueprint $table) {
|
||||
$table->boolean('password_change_disabled')->default(false)->after('password');
|
||||
});
|
||||
$this->info(" ✓ Added {$tableName}.password_change_disabled");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to add password_change_disabled: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function migrateHardcodedOrgRolesToOrganizationRoles()
|
||||
{
|
||||
$this->info('Syncing scoped org/bank Spatie roles from OrganizationRole definitions...');
|
||||
|
||||
if (!\Schema::hasTable('organization_roles')) {
|
||||
$this->warn(' ℹ organization_roles table not found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$orgRoles = \App\Models\OrganizationRole::all();
|
||||
$allSpatieRoles = Role::where('guard_name', 'web')->get();
|
||||
$synced = 0;
|
||||
|
||||
foreach ($orgRoles as $orgRole) {
|
||||
$suffix = '\\' . $orgRole->name;
|
||||
$spatieRoles = $allSpatieRoles->filter(fn($r) => str_ends_with($r->name, $suffix));
|
||||
foreach ($spatieRoles as $spatieRole) {
|
||||
$spatieRole->syncPermissions($orgRole->permissions ?? []);
|
||||
$this->info(" ✓ Synced '{$spatieRole->name}'");
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
$this->info(" ✓ Synced {$synced} scoped org/bank role(s)");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to sync org/bank roles: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function createAdminRolesTable()
|
||||
{
|
||||
$this->info('Creating admin_roles table...');
|
||||
|
||||
try {
|
||||
if (\Schema::hasTable('admin_roles')) {
|
||||
$this->warn(' ℹ admin_roles table already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
\Schema::create('admin_roles', function (\Illuminate\Database\Schema\Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('label');
|
||||
$table->json('permissions');
|
||||
$table->boolean('is_system')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
DB::table('admin_roles')->insert([
|
||||
[
|
||||
'name' => 'administrator',
|
||||
'label' => 'Administrator',
|
||||
'permissions' => json_encode([
|
||||
'manage posts', 'manage tags', 'manage categories',
|
||||
'manage mailings', 'manage calls', 'manage users',
|
||||
'manage user_projects', 'manage organizations', 'manage banks',
|
||||
'manage admins', 'manage accounts',
|
||||
'view profiles', 'create profiles', 'update profiles', 'delete profiles', 'manage profiles',
|
||||
'view permissions', 'create permissions', 'update permissions', 'delete permissions', 'manage permissions',
|
||||
'view roles', 'create roles', 'update roles', 'delete roles', 'manage roles',
|
||||
'manage maintenance',
|
||||
'view search-metrics', 'view application-log', 'view inactive-profiles-log', 'view search-reindex-log',
|
||||
'view stats',
|
||||
]),
|
||||
'is_system' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'name' => 'site-editor',
|
||||
'label' => 'Site Editor',
|
||||
'permissions' => json_encode([
|
||||
'manage posts', 'manage tags', 'manage categories', 'manage calls',
|
||||
]),
|
||||
'is_system' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->info(' ✓ admin_roles table created and seeded');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to create admin_roles table: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll permissions (view / create / update / export / delete / manage polls), separate from posts.
|
||||
* 'manage polls' goes to every admin role type (admin_roles) and to every existing scoped
|
||||
* admin role, including older ones without an admin_roles row. Safe to run again.
|
||||
*/
|
||||
private function addPollPermissions()
|
||||
{
|
||||
$this->info('Adding poll permissions...');
|
||||
|
||||
$permissions = ['view polls', 'create polls', 'update polls', 'export polls', 'delete polls', 'manage polls'];
|
||||
|
||||
try {
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
$created = 0;
|
||||
foreach ($permissions as $name) {
|
||||
if (Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web'])->wasRecentlyCreated) {
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
$types = 0;
|
||||
foreach (\App\Models\AdminRole::all() as $adminRole) {
|
||||
$current = $adminRole->permissions ?? [];
|
||||
if (!in_array('manage polls', $current, true)) {
|
||||
$adminRole->permissions = array_values(array_merge($current, ['manage polls']));
|
||||
$adminRole->save();
|
||||
$types++;
|
||||
}
|
||||
}
|
||||
|
||||
// Added to the scoped roles rather than re-synced, so their other permissions stay as they are
|
||||
$roles = 0;
|
||||
Role::where('guard_name', 'web')->where('name', 'like', 'Admin\\\\%')->get()
|
||||
->each(function (Role $role) use (&$roles) {
|
||||
if (!$role->hasPermissionTo('manage polls')) {
|
||||
$role->givePermissionTo('manage polls');
|
||||
$roles++;
|
||||
}
|
||||
});
|
||||
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
$this->info(" ✓ Added {$created} poll permission(s); 'manage polls' given to {$types} admin role type(s) and {$roles} admin role(s)");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to add poll permissions: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function addAdminCapabilityPermissions()
|
||||
{
|
||||
$this->info('Adding admin profile capability permissions...');
|
||||
|
||||
$caps = ['admin reports', 'admin contacts', 'admin messenger', 'admin settings', 'admin change-password'];
|
||||
|
||||
try {
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
$created = 0;
|
||||
foreach ($caps as $name) {
|
||||
$permission = Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
|
||||
if ($permission->wasRecentlyCreated) {
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
// Grant all admin caps to the administrator AdminRole and sync its scoped Spatie roles
|
||||
$adminRole = \App\Models\AdminRole::where('name', 'administrator')->first();
|
||||
if ($adminRole) {
|
||||
$current = $adminRole->permissions ?? [];
|
||||
$merged = array_values(array_unique(array_merge($current, $caps)));
|
||||
$adminRole->permissions = $merged;
|
||||
$adminRole->save();
|
||||
|
||||
$suffix = '\\administrator';
|
||||
Role::where('guard_name', 'web')->get()
|
||||
->filter(fn($r) => str_ends_with($r->name, $suffix))
|
||||
->each(fn($r) => $r->syncPermissions($adminRole->permissions));
|
||||
|
||||
$this->info(' ✓ Synced admin capabilities to administrator role');
|
||||
}
|
||||
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
$this->info(" ✓ Added {$created} new admin capability permission(s)");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to add admin capability permissions: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the Reversal transaction type's icon.
|
||||
*
|
||||
* The type was originally seeded with icon='receipt-refund'; it was
|
||||
* later changed to icon='arrow-path' (rendered mirrored via a CSS
|
||||
* transform at the two blade render sites) but transaction_types is
|
||||
* immutable for the app's restricted DB user, so any environment that
|
||||
* already inserted the row via the earlier migration needs this
|
||||
* corrected via an elevated-privilege command like this one.
|
||||
*/
|
||||
private function fixReversalTransactionTypeIcon()
|
||||
{
|
||||
$this->info('Fixing Reversal transaction type icon...');
|
||||
|
||||
try {
|
||||
$updated = DB::table('transaction_types')
|
||||
->where('id', 8)
|
||||
->where('name', 'Reversal')
|
||||
->where('icon', '!=', 'arrow-path')
|
||||
->update(['icon' => 'arrow-path']);
|
||||
|
||||
if ($updated > 0) {
|
||||
$this->info(' ✓ Updated Reversal transaction type icon to arrow-path');
|
||||
} else {
|
||||
$this->warn(' ℹ Reversal transaction type icon already correct (or type not found), skipping');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ✗ Failed to fix Reversal transaction type icon: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the "Poll" post category (type App\Models\Poll, not bound to a location:
|
||||
* a poll's own targeting decides who sees it).
|
||||
*/
|
||||
private function addPollCategory()
|
||||
{
|
||||
$this->info('Adding "Poll" post category...');
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$category = \App\Models\Category::firstOrCreate(
|
||||
['type' => \App\Models\Poll::class, 'categoryable_id' => null, 'categoryable_type' => null]
|
||||
);
|
||||
|
||||
$translations = [
|
||||
'en' => ['name' => 'Poll', 'slug' => 'poll'],
|
||||
'nl' => ['name' => 'Peiling', 'slug' => 'peiling'],
|
||||
'de' => ['name' => 'Umfrage', 'slug' => 'umfrage'],
|
||||
'es' => ['name' => 'Encuesta', 'slug' => 'encuesta'],
|
||||
'fr' => ['name' => 'Sondage', 'slug' => 'sondage'],
|
||||
];
|
||||
|
||||
foreach ($translations as $locale => $translation) {
|
||||
\App\Models\CategoryTranslation::updateOrCreate(
|
||||
['category_id' => $category->id, 'locale' => $locale],
|
||||
$translation
|
||||
);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->info(" ✓ \"Poll\" category added/updated (id {$category->id})");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
$this->error(' ✗ Failed to add poll category: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user