From b2745a99234d8b501d26ab279e43c0038bdaa6e4 Mon Sep 17 00:00:00 2001 From: Ivan Bochkarev Date: Sun, 16 Aug 2026 18:15:01 +0600 Subject: [PATCH 1/2] feat(web-api): customer auth contract for Nuxt SSR (me / refresh / Bearer bind) Add GET /customer/me introspection, real POST /customer/token/refresh rotation, and validate Bearer before binding guest cart on login/register. --- .../minishop3/config/routes/web.php | 8 + .../Api/Web/CustomerAuthController.php | 46 ++++- .../src/Middleware/TokenMiddleware.php | 48 +----- .../Customer/CustomerSessionService.php | 57 +++++++ .../minishop3/src/Services/TokenService.php | 159 +++++++++++++++++- .../tests/CustomerAuthRoutesTest.php | 8 + .../tests/CustomerSessionContractTest.php | 113 +++++++++++++ .../Customer/AuthManagerLifecycleTest.php | 72 ++++++++ .../tests/TokenRefreshRouteRemovedTest.php | 54 +++++- 9 files changed, 506 insertions(+), 59 deletions(-) create mode 100644 core/components/minishop3/src/Services/Customer/CustomerSessionService.php create mode 100644 core/components/minishop3/tests/CustomerSessionContractTest.php diff --git a/core/components/minishop3/config/routes/web.php b/core/components/minishop3/config/routes/web.php index c9ebce46..2c4368e2 100644 --- a/core/components/minishop3/config/routes/web.php +++ b/core/components/minishop3/config/routes/web.php @@ -149,6 +149,10 @@ return $customerAuth()->registerFromRequest(); }); + $router->get('/me', function ($params) use ($customerAuth) { + return $customerAuth()->me(); + }, [$tokenMiddleware]); + $router->post('/logout', function ($params) use ($customerAuth) { return $customerAuth()->logout(); }, [$tokenMiddleware]); @@ -182,6 +186,10 @@ } }); + $router->post('/token/refresh', function ($params) use ($customerAuth) { + return $customerAuth()->refreshToken(); + }, [$tokenMiddleware]); + $router->group('/addresses', function ($router) use ($modx) { $router->get('', function ($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\Web\CustomerAddressController($modx); diff --git a/core/components/minishop3/src/Controllers/Api/Web/CustomerAuthController.php b/core/components/minishop3/src/Controllers/Api/Web/CustomerAuthController.php index 79c7a483..463c6352 100644 --- a/core/components/minishop3/src/Controllers/Api/Web/CustomerAuthController.php +++ b/core/components/minishop3/src/Controllers/Api/Web/CustomerAuthController.php @@ -4,10 +4,12 @@ use MiniShop3\Router\HttpStatus; use MiniShop3\Router\Response; +use MiniShop3\Services\Customer\CustomerSessionService; +use MiniShop3\Services\TokenService; use MODX\Revolution\modX; /** - * CustomerAuthController — login, register, logout, password recovery (Web API). + * CustomerAuthController — login, register, logout, password recovery, session (Web API). * * Delegates to Processors\Api\Customer\* and maps processor failures to HTTP responses. */ @@ -49,6 +51,34 @@ public function resetPasswordFromRequest(): Response return $this->resetPassword($this->readJsonBody()); } + /** + * GET /api/v1/customer/me + */ + public function me(): Response + { + $payload = $this->sessionService()->buildMePayload($this->requestToken()); + if ($payload === null) { + return Response::error('ms3_err_token_invalid', HttpStatus::UNAUTHORIZED); + } + + return Response::success($payload); + } + + /** + * POST /api/v1/customer/token/refresh + */ + public function refreshToken(): Response + { + /** @var TokenService $tokenService */ + $tokenService = $this->modx->services->get('ms3_token_service'); + $rotated = $tokenService->rotateApiToken($this->requestToken()); + if ($rotated === null) { + return Response::error('ms3_err_token_invalid', HttpStatus::UNAUTHORIZED); + } + + return Response::success($rotated); + } + /** * @return array */ @@ -123,6 +153,20 @@ public function resetPassword(array $data): Response ]); } + private function requestToken(): string + { + return TokenService::resolveTokenFromRequest(); + } + + private function sessionService(): CustomerSessionService + { + /** @var TokenService $tokenService */ + $tokenService = $this->modx->services->get('ms3_token_service'); + $ms3 = $this->modx->services->get('ms3'); + + return new CustomerSessionService($this->modx, $tokenService, $ms3); + } + /** * @param array $properties */ diff --git a/core/components/minishop3/src/Middleware/TokenMiddleware.php b/core/components/minishop3/src/Middleware/TokenMiddleware.php index 17fb82da..00910bcf 100644 --- a/core/components/minishop3/src/Middleware/TokenMiddleware.php +++ b/core/components/minishop3/src/Middleware/TokenMiddleware.php @@ -87,24 +87,15 @@ public function handle(array $params) $_REQUEST['ms3_token'] = (string) $_SESSION['ms3']['customer_token']; } - // Resolve token from multiple sources - $token = $this->resolveToken(); + // Resolve token (middleware order; login bind uses getBindableTokenString) + $token = TokenService::resolveTokenFromRequest(); // If a token is present, always validate it (do not skip via session bypass). if (!empty($token)) { $resolved = $tokenService->resolveApiToken($token); if ($resolved['reason'] === 'ok') { - $tokenObj = $resolved['token']; - - if (!isset($_SESSION['ms3'])) { - $_SESSION['ms3'] = []; - } - $_SESSION['ms3']['customer_token'] = $token; - $_SESSION['ms3']['customer_id'] = $tokenObj->get('customer_id'); - $_SESSION['ms3']['customer_token_expires'] = strtotime($tokenObj->get('expires_at')); - - CookieHelper::setTokenCookie($this->modx, $token); + $tokenService->syncSessionFromToken($resolved['token']); $_REQUEST['ms3_token'] = $token; return null; @@ -162,39 +153,6 @@ private function clearClientTokenState(): void ); } - /** - * Resolve token from request sources - * - * @return string Token or empty string - */ - private function resolveToken(): string - { - // 1. Authorization: Bearer header (for mobile apps) - $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; - if (str_starts_with($authHeader, 'Bearer ')) { - $token = substr($authHeader, 7); - if (!empty($token)) { - return $token; - } - } - - // 2. HTTP_MS3TOKEN header (legacy) - $token = $_SERVER['HTTP_MS3TOKEN'] ?? ''; - if (!empty($token)) { - return $token; - } - - // 3. $_REQUEST (includes cookie via injection + legacy URL param) - $token = $_REQUEST['ms3_token'] ?? $_REQUEST['token'] ?? ''; - if (!empty($token)) { - return $token; - } - - // 4. Session cache (must still pass DB validation in handle()) - return $_SESSION['ms3']['customer_token'] ?? ''; - } - - /** * Check if route is public * diff --git a/core/components/minishop3/src/Services/Customer/CustomerSessionService.php b/core/components/minishop3/src/Services/Customer/CustomerSessionService.php new file mode 100644 index 00000000..fccc801e --- /dev/null +++ b/core/components/minishop3/src/Services/Customer/CustomerSessionService.php @@ -0,0 +1,57 @@ +|null, + * token: array{expires_at: string, customer_id: int} + * }|null null when token is missing/invalid/expired + */ + public function buildMePayload(string $tokenString): ?array + { + $resolved = $this->tokenService->resolveApiToken($tokenString); + if ($resolved['reason'] !== 'ok' || $resolved['token'] === null) { + return null; + } + + $tokenObj = $resolved['token']; + $customerId = (int) $tokenObj->get('customer_id'); + $customer = $customerId > 0 + ? $this->modx->getObject(msCustomer::class, $customerId) + : null; + + return [ + 'authenticated' => $customer instanceof msCustomer, + 'customer' => $customer instanceof msCustomer + ? CustomerPublicDto::fromCustomer($customer, $this->modx, $this->ms3) + : null, + 'token' => [ + 'expires_at' => (string) $tokenObj->get('expires_at'), + 'customer_id' => $customerId, + ], + ]; + } +} diff --git a/core/components/minishop3/src/Services/TokenService.php b/core/components/minishop3/src/Services/TokenService.php index ce4194d8..d5f13e41 100644 --- a/core/components/minishop3/src/Services/TokenService.php +++ b/core/components/minishop3/src/Services/TokenService.php @@ -506,18 +506,167 @@ public function ensureSessionActive(): void } /** - * Token currently bound to the browser (session first, then cookie), without minting. + * Resolve opaque API token from request globals (same order as TokenMiddleware). + * + * 1. Authorization: Bearer + * 2. HTTP_MS3TOKEN (legacy) + * 3. $_REQUEST['ms3_token'] / token (includes cookie inject) + * 4. PHP session cache + */ + public static function resolveTokenFromRequest(): string + { + $fromHeader = self::resolveBearerOrLegacyHeader(); + if ($fromHeader !== '') { + return $fromHeader; + } + + $fromRequest = $_REQUEST['ms3_token'] ?? $_REQUEST['token'] ?? ''; + if ($fromRequest !== '') { + return (string) $fromRequest; + } + + return (string) ($_SESSION['ms3']['customer_token'] ?? ''); + } + + /** + * Token to bind cart on login/register (no mint). + * + * Order: valid Bearer/MS3TOKEN → session → cookie → request params. + * Header only when resolveApiToken is ok (junk Bearer must not hide cookie cart). + * Session/cookie before $_REQUEST so stale cookie inject cannot mask the browser token. */ public function getBindableTokenString(): string { SessionHelper::ensureActive(); - $token = $this->getCustomerToken(); - if ($token !== null && $token !== '') { - return $token; + $fromHeader = self::resolveBearerOrLegacyHeader(); + if ($fromHeader !== '' && $this->resolveApiToken($fromHeader)['reason'] === 'ok') { + return $fromHeader; + } + + $sessionToken = $this->getCustomerToken(); + if ($sessionToken !== null) { + return $sessionToken; + } + + $cookieToken = CookieHelper::getTokenFromCookie(); + if ($cookieToken !== '') { + return $cookieToken; + } + + return (string) ($_REQUEST['ms3_token'] ?? $_REQUEST['token'] ?? ''); + } + + /** + * Bearer or legacy MS3TOKEN header value, empty when absent. + */ + private static function resolveBearerOrLegacyHeader(): string + { + foreach (['HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION'] as $serverKey) { + $authHeader = $_SERVER[$serverKey] ?? ''; + if (str_starts_with($authHeader, 'Bearer ')) { + $token = substr($authHeader, 7); + if ($token !== '') { + return $token; + } + } } - return CookieHelper::getTokenFromCookie(); + return (string) ($_SERVER['HTTP_MS3TOKEN'] ?? ''); + } + + /** + * Rotate a valid API token: mint new row, move draft, revoke old (headless TTL refresh). + * + * @return array{token: string, expires_at: string, customer_id: int}|null + */ + public function rotateApiToken(string $currentToken): ?array + { + $resolved = $this->resolveApiToken($currentToken); + if ($resolved['reason'] !== 'ok' || $resolved['token'] === null) { + return null; + } + + /** @var msCustomerToken $oldToken */ + $oldToken = $resolved['token']; + $customerId = (int) $oldToken->get('customer_id'); + + $newToken = $this->persistApiToken($customerId, null, null); + if (!$newToken) { + return null; + } + + $newTokenString = (string) $newToken->get('token'); + if ($newTokenString === '' || $newTokenString === $currentToken) { + return null; + } + + if (!$this->moveOrderDraftOnTokenRotation($currentToken, $newTokenString, $customerId)) { + $newToken->remove(); + $this->syncSessionFromToken($oldToken); + + return null; + } + + if (!$oldToken->remove()) { + $this->modx->log( + modX::LOG_LEVEL_ERROR, + '[TokenService] rotateApiToken: failed to revoke previous API token after mint' + ); + $newToken->remove(); + $this->syncSessionFromToken($oldToken); + + return null; + } + + return [ + 'token' => $newTokenString, + 'expires_at' => (string) $newToken->get('expires_at'), + 'customer_id' => $customerId, + ]; + } + + /** + * Re-link order draft after API token rotation (guest or authenticated). + * + * @return bool false when an existing draft could not be moved (caller must abort rotate) + */ + private function moveOrderDraftOnTokenRotation(string $oldToken, string $newToken, int $customerId): bool + { + if (!$this->modx->services->has('ms3_order_draft_manager')) { + return true; + } + + /** @var \MiniShop3\Services\Order\OrderDraftManager $draftManager */ + $draftManager = $this->modx->services->get('ms3_order_draft_manager'); + + if ($customerId > 0) { + if ($draftManager->transferDraftToToken($oldToken, $newToken, $customerId)) { + return true; + } + $this->modx->log( + modX::LOG_LEVEL_WARN, + '[TokenService] rotateApiToken: transferDraftToToken failed' + ); + + return false; + } + + $draft = $draftManager->getDraft($oldToken); + if (!$draft) { + return true; + } + + if ($draftManager->syncToken($draft, $newToken)) { + return true; + } + + $this->modx->log( + modX::LOG_LEVEL_ERROR, + '[TokenService] rotateApiToken: guest draft syncToken failed' + ); + + return false; } /** diff --git a/core/components/minishop3/tests/CustomerAuthRoutesTest.php b/core/components/minishop3/tests/CustomerAuthRoutesTest.php index ebcdef80..86c094d5 100644 --- a/core/components/minishop3/tests/CustomerAuthRoutesTest.php +++ b/core/components/minishop3/tests/CustomerAuthRoutesTest.php @@ -119,6 +119,10 @@ 'tokenMiddleware' => false, 'handlerMethod' => 'registerFromRequest', ], + 'GET /api/v1/customer/me' => [ + 'tokenMiddleware' => true, + 'handlerMethod' => 'me', + ], 'POST /api/v1/customer/logout' => [ 'tokenMiddleware' => true, 'handlerMethod' => 'logout', @@ -131,6 +135,10 @@ 'tokenMiddleware' => false, 'handlerMethod' => 'resetPasswordFromRequest', ], + 'POST /api/v1/customer/token/refresh' => [ + 'tokenMiddleware' => true, + 'handlerMethod' => 'refreshToken', + ], ]; $actual = []; diff --git a/core/components/minishop3/tests/CustomerSessionContractTest.php b/core/components/minishop3/tests/CustomerSessionContractTest.php new file mode 100644 index 00000000..30c047bf --- /dev/null +++ b/core/components/minishop3/tests/CustomerSessionContractTest.php @@ -0,0 +1,113 @@ + MS3TOKEN > REQUEST > session --- +$_SERVER = []; +$_REQUEST = []; +$_SESSION = []; + +$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer bearer-token-value'; +$_SERVER['HTTP_MS3TOKEN'] = 'legacy-header'; +$_REQUEST['ms3_token'] = 'request-token'; +$_SESSION['ms3']['customer_token'] = 'session-token'; +$assertSame('bearer-token-value', TokenService::resolveTokenFromRequest(), 'Bearer wins'); + +unset($_SERVER['HTTP_AUTHORIZATION']); +$assertSame('legacy-header', TokenService::resolveTokenFromRequest(), 'MS3TOKEN next'); + +unset($_SERVER['HTTP_MS3TOKEN']); +$assertSame('request-token', TokenService::resolveTokenFromRequest(), 'REQUEST next'); + +unset($_REQUEST['ms3_token'], $_REQUEST['token']); +$assertSame('session-token', TokenService::resolveTokenFromRequest(), 'session last among globals'); + +unset($_SESSION['ms3']['customer_token']); +$assertSame('', TokenService::resolveTokenFromRequest(), 'empty when nothing set'); + +// --- CustomerPublicDto must not expose secrets (me allowlist) --- +$leaky = [ + 'id' => 1, + 'email' => 'a@b.c', + 'password' => 'hash', + 'token' => 'internal', + 'user_id' => 9, + 'first_name' => 'Ann', +]; +$public = CustomerPublicDto::fromArray($leaky); +$assertSame(1, $public['id'] ?? null, 'id kept'); +$assertSame('a@b.c', $public['email'] ?? null, 'email kept'); +$assertSame('Ann', $public['first_name'] ?? null, 'first_name kept'); +$assertFalse(array_key_exists('password', $public), 'password stripped'); +$assertFalse(array_key_exists('token', $public), 'token stripped'); +$assertFalse(array_key_exists('user_id', $public), 'user_id stripped'); + +// --- me / refresh source contracts --- +$sessionService = file_get_contents(__DIR__ . '/../src/Services/Customer/CustomerSessionService.php'); +if ($sessionService === false) { + $fail('cannot read CustomerSessionService'); +} +$assertTrue(str_contains($sessionService, "'authenticated'"), 'me payload has authenticated'); +$assertTrue(str_contains($sessionService, 'CustomerPublicDto::fromCustomer'), 'me uses public DTO'); +$assertFalse(str_contains($sessionService, 'rotateApiToken'), 'session service is me-only (no refresh wrapper)'); + +$tokenServiceSrc = file_get_contents(__DIR__ . '/../src/Services/TokenService.php'); +if ($tokenServiceSrc === false) { + $fail('cannot read TokenService'); +} +$assertTrue(str_contains($tokenServiceSrc, 'resolveTokenFromRequest()'), 'bind uses shared resolve'); +$assertTrue( + str_contains($tokenServiceSrc, 'getBindableTokenString') + && str_contains($tokenServiceSrc, 'resolveTokenFromRequest'), + 'getBindableTokenString file includes resolveTokenFromRequest for Bearer bind' +); + +$middlewareSrc = file_get_contents(__DIR__ . '/../src/Middleware/TokenMiddleware.php'); +if ($middlewareSrc === false) { + $fail('cannot read TokenMiddleware'); +} +$assertTrue( + str_contains($middlewareSrc, 'TokenService::resolveTokenFromRequest()'), + 'TokenMiddleware uses shared resolve' +); +$assertFalse( + str_contains($middlewareSrc, 'private function resolveToken'), + 'TokenMiddleware must not keep a private resolveToken duplicate' +); + +fwrite(STDOUT, "OK CustomerSessionContractTest\n"); +exit(0); diff --git a/core/components/minishop3/tests/Integration/Customer/AuthManagerLifecycleTest.php b/core/components/minishop3/tests/Integration/Customer/AuthManagerLifecycleTest.php index f02e4fa7..77079d27 100644 --- a/core/components/minishop3/tests/Integration/Customer/AuthManagerLifecycleTest.php +++ b/core/components/minishop3/tests/Integration/Customer/AuthManagerLifecycleTest.php @@ -39,6 +39,12 @@ protected function setUp(): void } $_SESSION = []; $_COOKIE = []; + unset( + $_REQUEST['ms3_token'], + $_REQUEST['token'], + $_SERVER['HTTP_AUTHORIZATION'], + $_SERVER['HTTP_MS3TOKEN'] + ); $this->store = $this->createStore(); $this->store->reset(); @@ -49,6 +55,12 @@ protected function tearDown(): void { $_SESSION = []; $_COOKIE = []; + unset( + $_REQUEST['ms3_token'], + $_REQUEST['token'], + $_SERVER['HTTP_AUTHORIZATION'], + $_SERVER['HTTP_MS3TOKEN'] + ); } protected function createStore(): CustomerAuthPdoStore @@ -169,6 +181,66 @@ public function testForeignTokenDoesNotTransferCartButIsRevoked(): void ); } + public function testRotateApiTokenRevokesOldAndKeepsCustomer(): void + { + $customer = $this->seedCustomer([ + 'email' => 'buyer@example.com', + 'is_active' => 1, + 'is_blocked' => 0, + ]); + + $modx = $this->makeModx(); + $tokenService = new TokenService($modx); + $current = $tokenService->persistApiToken((int) $customer->id, null, 3600); + self::assertNotNull($current); + $oldToken = (string) $current->get('token'); + + $rotated = $tokenService->rotateApiToken($oldToken); + self::assertNotNull($rotated); + self::assertNotSame($oldToken, $rotated['token']); + self::assertSame((int) $customer->id, $rotated['customer_id']); + self::assertNull($this->store->findToken(['token' => $oldToken, 'type' => msCustomerToken::TYPE_API])); + self::assertNotNull($this->store->findToken(['token' => $rotated['token'], 'type' => msCustomerToken::TYPE_API])); + self::assertContains('transfer:' . $oldToken . '=>' . $rotated['token'], $this->draftCalls); + + self::assertNull($tokenService->rotateApiToken($oldToken)); + } + + public function testGetBindableTokenStringPrefersBearerOverSession(): void + { + $modx = $this->makeModx(); + $tokenService = new TokenService($modx); + $bearer = $tokenService->persistApiToken(0, null, 3600); + self::assertNotNull($bearer); + $bearerToken = (string) $bearer->get('token'); + + $_SESSION['ms3']['customer_token'] = 'session-only-token'; + $_SESSION['ms3']['customer_token_expires'] = time() + 3600; + $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $bearerToken; + unset($_REQUEST['ms3_token'], $_REQUEST['token'], $_COOKIE['ms3_token']); + + self::assertSame($bearerToken, $tokenService->getBindableTokenString()); + + unset($_SERVER['HTTP_AUTHORIZATION']); + } + + public function testGetBindableTokenStringIgnoresInvalidBearerAndUsesSession(): void + { + $modx = $this->makeModx(); + $tokenService = new TokenService($modx); + $guest = $tokenService->persistApiToken(0, null, 3600); + self::assertNotNull($guest); + $guestToken = (string) $guest->get('token'); + + $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer dead-or-revoked-token'; + unset($_REQUEST['ms3_token'], $_REQUEST['token'], $_COOKIE['ms3_token']); + + self::assertSame($guestToken, $tokenService->getBindableTokenString()); + self::assertSame($guestToken, $_SESSION['ms3']['customer_token'] ?? null); + + unset($_SERVER['HTTP_AUTHORIZATION']); + } + public function testValidateTokenAndRevokeTokens(): void { $customer = $this->seedCustomer([ diff --git a/core/components/minishop3/tests/TokenRefreshRouteRemovedTest.php b/core/components/minishop3/tests/TokenRefreshRouteRemovedTest.php index 70fd3ee8..57519d5f 100644 --- a/core/components/minishop3/tests/TokenRefreshRouteRemovedTest.php +++ b/core/components/minishop3/tests/TokenRefreshRouteRemovedTest.php @@ -1,10 +1,10 @@ Date: Sun, 16 Aug 2026 18:26:18 +0600 Subject: [PATCH 2/2] fix(web-api): drop unused \$params in customer route closures --- .../minishop3/config/routes/web.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/components/minishop3/config/routes/web.php b/core/components/minishop3/config/routes/web.php index 2c4368e2..888548b6 100644 --- a/core/components/minishop3/config/routes/web.php +++ b/core/components/minishop3/config/routes/web.php @@ -141,31 +141,31 @@ $customerAuth = static fn (): \MiniShop3\Controllers\Api\Web\CustomerAuthController => new \MiniShop3\Controllers\Api\Web\CustomerAuthController($modx); - $router->post('/login', function ($params) use ($customerAuth) { + $router->post('/login', function () use ($customerAuth) { return $customerAuth()->loginFromRequest(); }); - $router->post('/register', function ($params) use ($customerAuth) { + $router->post('/register', function () use ($customerAuth) { return $customerAuth()->registerFromRequest(); }); - $router->get('/me', function ($params) use ($customerAuth) { + $router->get('/me', function () use ($customerAuth) { return $customerAuth()->me(); }, [$tokenMiddleware]); - $router->post('/logout', function ($params) use ($customerAuth) { + $router->post('/logout', function () use ($customerAuth) { return $customerAuth()->logout(); }, [$tokenMiddleware]); - $router->post('/forgot-password', function ($params) use ($customerAuth) { + $router->post('/forgot-password', function () use ($customerAuth) { return $customerAuth()->forgotPasswordFromRequest(); }); - $router->post('/reset-password', function ($params) use ($customerAuth) { + $router->post('/reset-password', function () use ($customerAuth) { return $customerAuth()->resetPasswordFromRequest(); }); - $router->post('/add', function ($params) use ($modx) { + $router->post('/add', function () use ($modx) { $ms3 = $modx->services->get('ms3'); $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -174,7 +174,7 @@ return $controller->updateField($data); }, [$tokenMiddleware]); - $router->get('/token/get', function ($params) use ($modx) { + $router->get('/token/get', function () use ($modx) { $ms3 = $modx->services->get('ms3'); $ms3->initialize($modx->context->key ?? 'web'); $response = $ms3->customer->generateToken(); @@ -186,7 +186,7 @@ } }); - $router->post('/token/refresh', function ($params) use ($customerAuth) { + $router->post('/token/refresh', function () use ($customerAuth) { return $customerAuth()->refreshToken(); }, [$tokenMiddleware]); @@ -200,7 +200,7 @@ return $controller->get($params); }); - $router->post('', function ($params) use ($modx) { + $router->post('', function () use ($modx) { $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -227,7 +227,7 @@ }); }, [$tokenMiddleware]); - $router->put('/profile', function ($params) use ($modx) { + $router->put('/profile', function () use ($modx) { $ms3 = $modx->services->get('ms3'); $input = file_get_contents('php://input'); $data = json_decode($input, true) ?: []; @@ -241,7 +241,7 @@ return $controller->changeCustomerAddress($params); }, [$tokenMiddleware]); - $router->post('/email/resend-verification', function ($params) use ($modx) { + $router->post('/email/resend-verification', function () use ($modx) { $ms3 = $modx->services->get('ms3'); $controller = new \MiniShop3\Controllers\Api\Web\CustomerEmailController($modx, $ms3); return $controller->resendVerification();