Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.16% covered (success)
92.16%
94 / 102
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RegisterAction
92.16% covered (success)
92.16%
94 / 102
75.00% covered (warning)
75.00%
6 / 8
20.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isCompleteRegistration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 handleCompleteRegistration
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
2
 handleSimpleRegistration
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
 validateSimpleRegistrationInput
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 validateCompleteRegistrationInput
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
3.04
 getClientIp
70.59% covered (warning)
70.59%
12 / 17
0.00% covered (danger)
0.00%
0 / 1
6.92
1<?php
2
3declare(strict_types=1);
4
5namespace App\Action\Auth;
6
7use App\Domain\Auth\Service\AuthService;
8use App\Domain\Auth\Service\RegistrationService;
9use App\Domain\Exception\BadRequestException;
10use App\Support\Row;
11use Psr\Http\Message\ResponseInterface;
12use Psr\Http\Message\ServerRequestInterface;
13
14use function array_key_exists;
15use function json_encode;
16use function sprintf;
17
18/**
19 * Register new user.
20 *
21 * POST /api/auth/register
22 *
23 * Supports two modes:
24 * 1. Simple registration (username, email, password) - creates user only
25 * 2. Complete registration (includes investor data) - creates investor, user, and account atomically
26 */
27final readonly class RegisterAction
28{
29    public function __construct(
30        private readonly AuthService $authService,
31        private readonly RegistrationService $registrationService,
32    ) {}
33
34    public function __invoke(
35        ServerRequestInterface $request,
36        ResponseInterface $response,
37    ): ResponseInterface {
38        // Get request data
39        $data = (array)$request->getParsedBody();
40
41        // Determine registration mode based on presence of investor fields
42        $isCompleteRegistration = $this->isCompleteRegistration($data);
43
44        if ($isCompleteRegistration) {
45            return $this->handleCompleteRegistration($request, $response, $data);
46        }
47
48        return $this->handleSimpleRegistration($response, $data);
49    }
50
51    /**
52     * Check if this is a complete registration (with investor data).
53     *
54     * @param array<mixed> $data
55     */
56    private function isCompleteRegistration(array $data): bool
57    {
58        // If firstName is present, assume complete registration
59        return array_key_exists('firstName', $data) && !empty($data['firstName']);
60    }
61
62    /**
63     * Handle complete registration with investor and account creation.
64     *
65     * @param array<mixed> $data
66     * @param ServerRequestInterface $request
67     * @param ResponseInterface $response
68     */
69    private function handleCompleteRegistration(
70        ServerRequestInterface $request,
71        ResponseInterface $response,
72        array $data,
73    ): ResponseInterface {
74        // Validate required fields for complete registration
75        $this->validateCompleteRegistrationInput($data);
76
77        // Get client info for session tracking
78        $ipAddress = $this->getClientIp($request);
79        $userAgent = $request->getHeaderLine('User-Agent') ?: null;
80
81        // Perform atomic registration
82        $result = $this->registrationService->registerComplete(
83            data: $data,
84            ipAddress: $ipAddress,
85            userAgent: $userAgent,
86        );
87
88        $responseData = [
89            'success' => true,
90            'message' => 'Registration completed successfully',
91            'data' => [
92                'user' => [
93                    'userId' => $result['user']->userId,
94                    'username' => $result['user']->username,
95                    'email' => $result['user']->email,
96                    'role' => $result['user']->role,
97                ],
98                'investorId' => $result['investorId'],
99                'accountId' => $result['accountId'],
100                'accountNumber' => $result['accountNumber'],
101                'accessToken' => $result['tokens']->accessToken,
102                'refreshToken' => $result['tokens']->refreshToken,
103                'tokenType' => $result['tokens']->tokenType,
104                'expiresIn' => $result['tokens']->expiresIn,
105            ],
106        ];
107
108        $response->getBody()->write((string)json_encode($responseData));
109
110        return $response
111            ->withHeader('Content-Type', 'application/json')
112            ->withStatus(201);
113    }
114
115    /**
116     * Handle simple registration (user only, for admin/special cases).
117     *
118     * @param array<mixed> $data
119     * @param ResponseInterface $response
120     */
121    private function handleSimpleRegistration(
122        ResponseInterface $response,
123        array $data,
124    ): ResponseInterface {
125        $this->validateSimpleRegistrationInput($data);
126        $user = $this->authService->register(
127            username: Row::string($data, 'username'),
128            email: Row::string($data, 'email'),
129            password: Row::string($data, 'password'),
130            investorId: Row::nullableInt($data, 'investorId'),
131            role: Row::nullableString($data, 'role') ?? 'investor',
132        );
133
134        // Return success response (don't expose the user password hash)
135        $responseData = [
136            'success' => true,
137            'message' => 'User registered successfully',
138            'data' => [
139                'userId' => $user->userId,
140                'username' => $user->username,
141                'email' => $user->email,
142                'role' => $user->role,
143            ],
144        ];
145
146        $response->getBody()->write((string)json_encode($responseData));
147
148        return $response
149            ->withHeader('Content-Type', 'application/json')
150            ->withStatus(201);
151    }
152
153    /**
154     * Validate simple registration input.
155     *
156     * @param array<mixed> $data
157     *
158     * @throws BadRequestException
159     */
160    private function validateSimpleRegistrationInput(array $data): void
161    {
162        $requiredFields = ['username', 'email', 'password'];
163
164        foreach ($requiredFields as $field) {
165            if (empty($data[$field])) {
166                throw new BadRequestException(
167                    sprintf('Missing required field: %s', $field),
168                );
169            }
170        }
171    }
172
173    /**
174     * Validate complete registration input (basic presence check).
175     * Detailed validation is done in RegistrationService.
176     *
177     * @param array<mixed> $data
178     *
179     * @throws BadRequestException
180     */
181    private function validateCompleteRegistrationInput(array $data): void
182    {
183        $requiredFields = [
184            'username',
185            'email',
186            'password',
187            'firstName',
188            'lastName',
189            'dateOfBirth',
190            'phone',
191            'addressLine1',
192            'city',
193            'state',
194            'zipCode',
195            'country',
196        ];
197
198        foreach ($requiredFields as $field) {
199            if (empty($data[$field])) {
200                throw new BadRequestException(
201                    sprintf('Missing required field: %s', $field),
202                );
203            }
204        }
205    }
206    /**
207     * Get client IP address.
208     * @param ServerRequestInterface $request
209     */
210    private function getClientIp(ServerRequestInterface $request): ?string
211    {
212        $serverParams = $request->getServerParams();
213
214        $headers = [
215            'HTTP_CF_CONNECTING_IP',
216            'HTTP_X_REAL_IP',
217            'HTTP_X_FORWARDED_FOR',
218            'REMOTE_ADDR',
219        ];
220
221        foreach ($headers as $header) {
222            $ip = Row::nullableString($serverParams, $header);
223            if ($ip === null || $ip === '') {
224                continue;
225            }
226
227            if ($header === 'HTTP_X_FORWARDED_FOR') {
228                $ips = explode(',', $ip);
229                $ip = trim($ips[0]);
230            }
231
232            if (filter_var($ip, FILTER_VALIDATE_IP)) {
233                return $ip;
234            }
235        }
236
237        return null;
238    }
239}