ApiTokenLoginHandler::getLanguage()   A
last analyzed

Complexity

Conditions 5
Paths 11

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 2
b 0
f 0
nc 11
nop 1
dl 0
loc 27
ccs 0
cts 21
cp 0
crap 30
rs 9.4888
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Handler;
6
7
use App\Exception\HttpException;
8
use App\Infra\Log\AccessLogger;
9
use App\Security\ContredanseProductAccess;
10
use App\Security\Exception\NoProductAccessException;
11
use App\Security\Exception\ProductAccessExpiredException;
12
use App\Security\Exception\ProductPaymentIssueException;
13
use App\Security\UserProviderInterface;
14
use App\Service\Auth\AuthManager;
15
use App\Service\Auth\Exception\AuthExceptionInterface;
16
use App\Service\Token\TokenManager;
17
use DateTime;
18
use Fig\Http\Message\StatusCodeInterface;
19
use Negotiation\AcceptLanguage;
20
use Negotiation\LanguageNegotiator;
21
use Psr\Http\Message\ResponseInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
use Psr\Http\Server\RequestHandlerInterface;
24
use Zend\Diactoros\Response\JsonResponse;
25
26
class ApiTokenLoginHandler implements RequestHandlerInterface
27
{
28
    /**
29
     * @var UserProviderInterface
30
     */
31
    private $userProvider;
32
33
    /**
34
     * @var TokenManager
35
     */
36
    private $tokenManager;
37
38
    /**
39
     * @var array<string, mixed>
40
     */
41
    private $authParams;
42
43
    /**
44
     * @var ContredanseProductAccess
45
     */
46
    private $productAccess;
47
48
    /**
49
     * @var AccessLogger|null
50
     */
51
    private $accessLogger;
52
53
    /**
54
     * @param array<string, mixed> $authParams
55
     */
56
    public function __construct(UserProviderInterface $userProvider, TokenManager $tokenManager, ContredanseProductAccess $productAccess, array $authParams, ?AccessLogger $accessLogger)
57
    {
58
        $this->userProvider  = $userProvider;
59
        $this->tokenManager  = $tokenManager;
60
        $this->authParams    = $authParams;
61
        $this->productAccess = $productAccess;
62
        $this->accessLogger  = $accessLogger;
63
    }
64
65
    public function handle(ServerRequestInterface $request): ResponseInterface
66
    {
67
        $authExpiry = $this->authParams['token_expiry'] ?? TokenManager::DEFAULT_EXPIRY;
68
69
        $method = $request->getMethod();
70
71
        if ($method !== 'POST') {
72
            throw new \RuntimeException('Unsupported http method');
73
        }
74
        // Authorization...
75
        //
76
        // Valid users are
77
        // - either admins
78
        // - or valid paying users
79
        //
80
81
        $body = $request->getParsedBody();
82
        if ($body === null) {
83
            throw new HttpException('Request body is empty');
84
        }
85
86
        /* @phpstan-ignore-next-line */
87
        $email = trim(array_key_exists('email', (array) $body) ? $body['email'] : '');
88
        /* @phpstan-ignore-next-line */
89
        $password = trim(array_key_exists('password', (array) $body) ? $body['password'] : '');
90
        /* @phpstan-ignore-next-line */
91
        $language = trim(array_key_exists('language', (array) $body) ? $body['language'] : '');
92
        if ($language === '') {
93
            $language = $this->getLanguage($request);
94
        }
95
96
        // @todo Must be removed when production
97
        if ($email === '[email protected]' && $password === 'demo') {
98
            // This is for demo only
99
            $this->logAccess($request, AccessLogger::TYPE_LOGIN_SUCCESS, $email, $language);
100
101
            return $this->getResponseWithAccessToken($email, $authExpiry);
102
        }
103
104
        $authenticationManager = new AuthManager($this->userProvider);
105
106
        try {
107
            // Authenticate, wil throw exception if failed
108
            $user = $authenticationManager->getAuthenticatedUser($email, $password);
109
110
            // Ensure authorization
111
            $this->productAccess->ensureAccess(ContredanseProductAccess::PAXTON_PRODUCT, $user);
112
            $this->logAccess($request, AccessLogger::TYPE_LOGIN_SUCCESS, $email, $language);
113
114
            return $this->getResponseWithAccessToken($user->getDetail('user_id'), $authExpiry);
115
        } catch (\Throwable $e) {
116
            $type = $this->getAccessLoggerTypeFromException($e);
117
            $this->logAccess($request, $type, $email, $language);
118
119
            $responseData = [
120
                'success'    => false,
121
                'reason'     => $e->getMessage(),
122
                'error_type' => $type,
123
            ];
124
125
            if ($e instanceof ProductAccessExpiredException) {
126
                $responseData['expired_date'] = $e->getExpiryDate()->format(DateTime::ATOM);
127
            }
128
129
            return (new JsonResponse($responseData))->withStatus(StatusCodeInterface::STATUS_UNAUTHORIZED);
130
        }
131
    }
132
133
    private function getAccessLoggerTypeFromException(\Throwable $e): string
134
    {
135
        switch (true) {
136
            case $e instanceof AuthExceptionInterface:
137
                return AccessLogger::TYPE_LOGIN_FAILURE_CREDENTIALS;
138
            case $e instanceof NoProductAccessException:
139
                return AccessLogger::TYPE_LOGIN_FAILURE_NO_ACCESS;
140
            case $e instanceof ProductPaymentIssueException:
141
                return AccessLogger::TYPE_LOGIN_FAILURE_PAYMENT_ISSUE;
142
            case $e instanceof ProductAccessExpiredException:
143
                return AccessLogger::TYPE_LOGIN_FAILURE_EXPIRY;
144
            default:
145
                return AccessLogger::TYPE_LOGIN_FAILURE;
146
        }
147
    }
148
149
    private function logAccess(ServerRequestInterface $request, string $type, string $email, ?string $language): void
150
    {
151
        if ($this->accessLogger !== null) {
152
            ['REMOTE_ADDR' => $ipAddress, 'HTTP_USER_AGENT' => $userAgent] = $request->getServerParams();
153
            try {
154
                $this->accessLogger->log(
155
                    $type,
156
                    $email,
157
                    $language,
158
                    $ipAddress,
159
                    $userAgent
160
                );
161
            } catch (\Throwable $e) {
162
                // Discard any error
163
                //var_dump($e->getMessage());
164
                //die();
165
                error_log('AuthLoggerMiddleware failure' . $e->getMessage());
166
            }
167
        }
168
    }
169
170
    private function getLanguage(ServerRequestInterface $request): ?string
171
    {
172
        try {
173
            $acceptLanguageHeader = trim($request->getHeaderLine('Accept-Language'));
174
175
            if (trim($acceptLanguageHeader) !== '') {
176
                $negotiator = new LanguageNegotiator();
177
                /**
178
                 * @var AcceptLanguage|null $acceptLanguage
179
                 */
180
                $acceptLanguage = $negotiator->getBest($acceptLanguageHeader, ['fr', 'en']);
181
                if ($acceptLanguage !== null) {
182
                    $parts = array_filter([
183
                        $acceptLanguage->getBasePart(), // lang
184
                        $acceptLanguage->getSubPart() // region
185
                    ]);
186
187
                    if (count($parts) > 0) {
188
                        return implode('_', $parts);
189
                    }
190
                }
191
            }
192
        } catch (\Throwable $e) {
193
            return null;
194
        }
195
196
        return null;
197
    }
198
199
    private function getResponseWithAccessToken(string $user_id, int $authExpiry): ResponseInterface
200
    {
201
        $token = $this->tokenManager->createNewToken([
202
            'user_id' => $user_id
203
            //'email'    => $email,
204
        ], $authExpiry);
205
206
        return (new JsonResponse([
207
            'access_token' => (string) $token,
208
            'token_type'   => 'api_auth',
209
            'success'      => true,
210
        ]))->withStatus(StatusCodeInterface::STATUS_OK);
211
    }
212
}
213