Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.18% covered (success)
98.18%
54 / 55
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImpersonateUserAction
98.18% covered (success)
98.18%
54 / 55
66.67% covered (warning)
66.67%
2 / 3
10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
7
 logImpersonation
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Admin;
6
7use App\Domain\Auth\Service\AuthService;
8use App\Domain\Exception\ForbiddenException;
9use App\Domain\Exception\NotFoundException;
10use App\Domain\Exception\ValidationException;
11use App\Renderer\JsonRenderer;
12use App\Support\Row;
13use PDO;
14use PDOException;
15use Psr\Http\Message\ResponseInterface;
16use Psr\Http\Message\ServerRequestInterface;
17
18/**
19 * Allows admin to impersonate a user by generating tokens for them.
20 *
21 * POST /api/admin/impersonate/{userId}
22 *
23 * Security: This action should only be accessible to admins.
24 * The original admin's info is stored in the token for audit purposes.
25 */
26final class ImpersonateUserAction
27{
28    private PDO $pdo;
29
30    private AuthService $authService;
31
32    private JsonRenderer $renderer;
33
34    public function __construct(
35        PDO $pdo,
36        AuthService $authService,
37        JsonRenderer $renderer,
38    ) {
39        $this->pdo = $pdo;
40        $this->authService = $authService;
41        $this->renderer = $renderer;
42    }
43
44    /**
45     * @param array<string, string> $args
46     * @param ServerRequestInterface $request
47     * @param ResponseInterface $response
48     */
49    public function __invoke(
50        ServerRequestInterface $request,
51        ResponseInterface $response,
52        array $args,
53    ): ResponseInterface {
54        $targetUserId = (int)$args['userId'];
55
56        if ($targetUserId <= 0) {
57            throw new ValidationException('Invalid user ID');
58        }
59
60        // Get the admin user from the request (set by JWT middleware)
61        $adminUserId = Row::int($request->getAttributes(), 'userId');
62        $adminRole = $request->getAttribute('userRole');
63
64        // Verify admin role
65        if ($adminRole !== 'admin' && $adminRole !== 'super_admin') {
66            throw new ForbiddenException('Only admins can impersonate users');
67        }
68
69        // Get target user info
70        $sql = 'SELECT user_id, username, email, role FROM users WHERE user_id = :user_id';
71        $stmt = $this->pdo->prepare($sql);
72        $stmt->execute(['user_id' => $targetUserId]);
73        $targetUser = $stmt->fetch(PDO::FETCH_ASSOC);
74
75        if (!is_array($targetUser)) {
76            throw new NotFoundException('User not found');
77        }
78
79        $targetRole = Row::string($targetUser, 'role');
80        $targetUserIdActual = Row::int($targetUser, 'user_id');
81        $targetUsername = Row::string($targetUser, 'username');
82        $targetEmail = Row::string($targetUser, 'email');
83
84        // Prevent impersonating other admins (security measure)
85        if ($targetRole === 'admin' || $targetRole === 'super_admin') {
86            throw new ForbiddenException('Cannot impersonate admin users');
87        }
88
89        // Generate tokens for the target user
90        // Note: In a production system, you might want to add an "impersonated_by" claim
91        // to the JWT for audit purposes
92        $tokens = $this->authService->generateTokens([
93            'user_id' => $targetUserIdActual,
94            'username' => $targetUsername,
95            'email' => $targetEmail,
96            'role' => $targetRole,
97        ]);
98
99        // Log the impersonation for audit
100        $this->logImpersonation($adminUserId, $targetUserId);
101
102        return $this->renderer->json($response, [
103            'success' => true,
104            'message' => 'Impersonation successful',
105            'data' => [
106                'user' => [
107                    'userId' => $targetUserIdActual,
108                    'username' => $targetUsername,
109                    'email' => $targetEmail,
110                    'role' => $targetRole,
111                ],
112                'accessToken' => $tokens['accessToken'],
113                'refreshToken' => $tokens['refreshToken'],
114                'expiresIn' => $tokens['expiresIn'],
115                'tokenType' => 'Bearer',
116                'impersonatedBy' => $adminUserId,
117            ],
118        ]);
119    }
120
121    /**
122     * Log impersonation event for audit trail.
123     *
124     * @param int $adminUserId
125     * @param int $targetUserId
126     */
127    private function logImpersonation(int $adminUserId, int $targetUserId): void
128    {
129        // Insert into audit_log table if it exists
130        $sql = "
131            INSERT INTO audit_log (table_name, record_id, action, old_values, new_values, changed_by)
132            VALUES ('users', :target_user_id, 'impersonate', NULL, :details, :admin_user_id)
133        ";
134
135        try {
136            $stmt = $this->pdo->prepare($sql);
137            $stmt->execute([
138                'target_user_id' => $targetUserId,
139                'details' => json_encode(['impersonated_by' => $adminUserId, 'timestamp' => date('c')]),
140                'admin_user_id' => $adminUserId,
141            ]);
142        } catch (PDOException $e) {
143            // Silently fail if audit_log doesn't have the right structure
144            // In production, you'd want proper error handling here
145        }
146    }
147}