Passed
Push — master ( 49b4e9...c9293f )
by Darko
11:23
created

ManageRoleStacking::listPendingRoles()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 24
c 1
b 0
f 0
dl 0
loc 36
rs 9.2248
cc 5
nc 6
nop 0
1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Models\User;
6
use App\Models\UserRoleHistory;
7
use Carbon\Carbon;
8
use Illuminate\Console\Command;
9
use Spatie\Permission\Models\Role;
10
11
class ManageRoleStacking extends Command
12
{
13
    /**
14
     * The name and signature of the console command.
15
     *
16
     * @var string
17
     */
18
    protected $signature = 'role:manage
19
                          {action : Action to perform (list-pending, cancel-pending, history, activate-pending)}
20
                          {user? : User ID or username}
21
                          {--all : Apply to all users}';
22
23
    /**
24
     * The console command description.
25
     *
26
     * @var string
27
     */
28
    protected $description = 'Manage role stacking for users';
29
30
    /**
31
     * Execute the console command.
32
     */
33
    public function handle(): int
34
    {
35
        $action = $this->argument('action');
36
37
        return match($action) {
38
            'list-pending' => $this->listPendingRoles(),
39
            'cancel-pending' => $this->cancelPendingRole(),
40
            'history' => $this->showHistory(),
41
            'activate-pending' => $this->activatePendingRoles(),
42
            default => $this->handleUnknownAction($action),
43
        };
44
    }
45
46
    /**
47
     * Handle unknown action
48
     */
49
    protected function handleUnknownAction(string $action): int
50
    {
51
        $this->error("Unknown action: {$action}");
52
        return 1;
53
    }
54
55
    /**
56
     * List all users with pending roles
57
     */
58
    protected function listPendingRoles(): int
59
    {
60
        $users = User::whereNotNull('pending_roles_id')
61
            ->whereNotNull('pending_role_start_date')
62
            ->get();
63
64
        if ($users->isEmpty()) {
65
            $this->info('No users with pending roles found.');
66
            return 0;
67
        }
68
69
        $this->info('Users with Pending Roles:');
70
        $this->newLine();
71
72
        $headers = ['User ID', 'Username', 'Current Role', 'Pending Role', 'Activation Date', 'Time Until'];
73
        $rows = [];
74
75
        foreach ($users as $user) {
76
            $currentRole = $user->roles->first();
77
            $pendingRole = $user->getPendingRole();
78
            $activationDate = Carbon::parse($user->pending_role_start_date);
79
80
            $rows[] = [
81
                $user->id,
82
                $user->username,
83
                $currentRole ? $currentRole->name : 'None',
84
                $pendingRole ? $pendingRole->name : 'Unknown',
85
                $activationDate->format('Y-m-d H:i'),
86
                $activationDate->diffForHumans(),
87
            ];
88
        }
89
90
        $this->table($headers, $rows);
91
        $this->info(sprintf('Total: %d users', count($rows)));
92
93
        return 0;
94
    }
95
96
    /**
97
     * Cancel pending role for user(s)
98
     */
99
    protected function cancelPendingRole(): int
100
    {
101
        if ($this->option('all')) {
102
            if (!$this->confirm('Are you sure you want to cancel ALL pending roles?')) {
103
                $this->info('Operation cancelled.');
104
                return 0;
105
            }
106
107
            $users = User::whereNotNull('pending_roles_id')->get();
108
            $count = 0;
109
110
            foreach ($users as $user) {
111
                if ($user->cancelPendingRole()) {
112
                    $count++;
113
                    $this->info("Cancelled pending role for user: {$user->username}");
114
                }
115
            }
116
117
            $this->info("Cancelled pending roles for {$count} users.");
118
            return 0;
119
        }
120
121
        $userId = $this->argument('user');
122
        if (!$userId) {
123
            $this->error('User ID or username required. Use --all to cancel for all users.');
124
            return 1;
125
        }
126
127
        $user = $this->findUser($userId);
128
        if (!$user) {
129
            return 1;
130
        }
131
132
        if (!$user->hasPendingRole()) {
133
            $this->info("User {$user->username} has no pending role.");
134
            return 0;
135
        }
136
137
        $pendingRole = $user->getPendingRole();
138
        if (!$this->confirm("Cancel pending role '{$pendingRole->name}' for user '{$user->username}'?")) {
139
            $this->info('Operation cancelled.');
140
            return 0;
141
        }
142
143
        if ($user->cancelPendingRole()) {
144
            $this->info("Successfully cancelled pending role for user: {$user->username}");
145
            return 0;
146
        }
147
148
        $this->error("Failed to cancel pending role.");
149
        return 1;
150
    }
151
152
    /**
153
     * Show role change history for user(s)
154
     */
155
    protected function showHistory(): int
156
    {
157
        $userId = $this->argument('user');
158
        if (!$userId) {
159
            $this->error('User ID or username required.');
160
            return 1;
161
        }
162
163
        $user = $this->findUser($userId);
164
        if (!$user) {
165
            return 1;
166
        }
167
168
        $history = UserRoleHistory::getUserHistory($user->id);
169
170
        if ($history->isEmpty()) {
171
            $this->info("No role change history found for user: {$user->username}");
172
            return 0;
173
        }
174
175
        $this->info("Role Change History for: {$user->username} (ID: {$user->id})");
176
        $this->newLine();
177
178
        $headers = ['Date', 'From Role', 'To Role', 'Type', 'Reason', 'Changed By'];
179
        $rows = [];
180
181
        foreach ($history as $change) {
182
            $oldRole = $change->old_role_id ? Role::find($change->old_role_id) : null;
183
            $newRole = Role::find($change->new_role_id);
184
            $changedBy = $change->changed_by ? User::find($change->changed_by) : null;
185
186
            $rows[] = [
187
                $change->effective_date->format('Y-m-d H:i'),
188
                $oldRole ? $oldRole->name : 'None',
189
                $newRole ? $newRole->name : 'Unknown',
190
                $change->is_stacked ? 'Stacked' : 'Immediate',
191
                $change->change_reason ?? 'N/A',
192
                $changedBy ? $changedBy->username : 'System',
193
            ];
194
        }
195
196
        $this->table($headers, $rows);
197
        $this->info(sprintf('Total changes: %d', count($rows)));
198
199
        // Show stacked changes summary
200
        $stackedCount = $history->where('is_stacked', true)->count();
201
        if ($stackedCount > 0) {
202
            $this->newLine();
203
            $this->info("Stacked role changes: {$stackedCount}");
204
        }
205
206
        return 0;
207
    }
208
209
    /**
210
     * Manually activate pending roles (normally done automatically)
211
     */
212
    protected function activatePendingRoles(): int
213
    {
214
        $this->warn('This command manually activates pending roles that are scheduled for activation.');
215
        $this->warn('Normally, this is done automatically by the scheduled task.');
216
        $this->newLine();
217
218
        if (!$this->confirm('Do you want to continue?')) {
219
            $this->info('Operation cancelled.');
220
            return 0;
221
        }
222
223
        $now = Carbon::now();
224
        $users = User::whereNotNull('pending_roles_id')
225
            ->whereNotNull('pending_role_start_date')
226
            ->where('pending_role_start_date', '<=', $now)
227
            ->get();
228
229
        if ($users->isEmpty()) {
230
            $this->info('No pending roles ready for activation.');
231
            return 0;
232
        }
233
234
        $this->info(sprintf('Found %d pending roles ready for activation:', $users->count()));
235
        $this->newLine();
236
237
        $activated = 0;
238
        $failed = 0;
239
240
        foreach ($users as $user) {
241
            $pendingRole = $user->getPendingRole();
242
            $oldRole = $user->roles->first();
243
244
            try {
245
                // Activate the pending role
246
                $user->update([
247
                    'roles_id' => $user->pending_roles_id,
248
                    'pending_roles_id' => null,
249
                    'pending_role_start_date' => null,
250
                ]);
251
252
                if ($pendingRole) {
253
                    $user->syncRoles([$pendingRole->name]);
254
                }
255
256
                // Record in history
257
                UserRoleHistory::recordRoleChange(
258
                    userId: $user->id,
259
                    oldRoleId: $oldRole ? $oldRole->id : null,
260
                    newRoleId: $pendingRole ? $pendingRole->id : $user->roles_id,
261
                    oldExpiryDate: null,
262
                    newExpiryDate: null,
263
                    effectiveDate: $now,
264
                    isStacked: true,
265
                    changeReason: 'manual_activation',
266
                    changedBy: null
267
                );
268
269
                $this->info(sprintf(
270
                    '✓ Activated %s -> %s for user: %s',
271
                    $oldRole ? $oldRole->name : 'None',
272
                    $pendingRole ? $pendingRole->name : 'Unknown',
273
                    $user->username
274
                ));
275
276
                $activated++;
277
            } catch (\Exception $e) {
278
                $this->error(sprintf(
279
                    '✗ Failed to activate role for user %s: %s',
280
                    $user->username,
281
                    $e->getMessage()
282
                ));
283
                $failed++;
284
            }
285
        }
286
287
        $this->newLine();
288
        $this->info("Activation complete:");
289
        $this->info("  Successful: {$activated}");
290
        if ($failed > 0) {
291
            $this->error("  Failed: {$failed}");
292
        }
293
294
        return $failed > 0 ? 1 : 0;
295
    }
296
297
    /**
298
     * Find user by ID or username
299
     */
300
    protected function findUser(string|int $identifier): ?User
301
    {
302
        $user = is_numeric($identifier)
303
            ? User::find($identifier)
304
            : User::where('username', $identifier)->first();
305
306
        if (!$user) {
307
            $this->error("User not found: {$identifier}");
308
            return null;
309
        }
310
311
        return $user;
312
    }
313
}
314
315