Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions assets/components/minishop3/js/web/core/ApiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ class ApiClient {
return tokenErrors.includes(result.message)
}

/**
* Web API returns payload in `data`; legacy processors used `object`.
*
* @param {Object|null|undefined} result - API response
* @returns {Object|null}
*/
static getPayload (result) {
if (!result) {
return null
}
return result.data ?? result.object ?? null
}

/**
* GET request
*
Expand Down
7 changes: 4 additions & 3 deletions assets/components/minishop3/js/web/ui/AuthUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class AuthUI {
if (result.success) {
this.showMessage('login-messages', this.t('ms3_customer_login_success'), 'success')
setTimeout(() => {
this.handleRedirect(result.object)
this.handleRedirect(ApiClient.getPayload(result))
}, 1000)
} else {
this.showMessage('login-messages', result.message || this.t('ms3_err_unknown'), 'danger')
Expand Down Expand Up @@ -177,9 +177,10 @@ class AuthUI {
'success'
)

if (result.object && result.object.token) {
const payload = ApiClient.getPayload(result)
if (payload && payload.token) {
setTimeout(() => {
this.handleRedirect(result.object)
this.handleRedirect(payload)
}, 1500)
} else {
setTimeout(() => {
Expand Down
12 changes: 12 additions & 0 deletions core/components/minishop3/config/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@

return Response::success($response->getObject(), $response->getMessage());
});
$router->post('/logout', function($params) use ($modx) {
$response = $modx->runProcessor(
'MiniShop3\Processors\Api\Customer\Logout',
[]
);

if ($response->isError()) {
return Response::error($response->getMessage(), HttpStatus::BAD_REQUEST);
}

return Response::success($response->getObject() ?: [], $response->getMessage());
}, [$tokenMiddleware]);
$router->post('/register', function($params) use ($modx) {
$input = file_get_contents('php://input');
$data = json_decode($input, true) ?: [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
{'ms3_customer_email' | lexicon} <span class="text-danger">*</span>
</label>
<input type="email" class="form-control" id="login-email" name="email"
autocomplete="username"
placeholder="{'ms3_customer_email_placeholder' | lexicon}" required>
</div>

Expand All @@ -43,16 +44,10 @@
{'ms3_customer_password' | lexicon} <span class="text-danger">*</span>
</label>
<input type="password" class="form-control" id="login-password" name="password"
autocomplete="current-password"
placeholder="{'ms3_customer_password_placeholder' | lexicon}" required>
</div>

<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="login-remember" name="remember">
<label class="form-check-label" for="login-remember">
{'ms3_customer_remember_me' | lexicon}
</label>
</div>

<button type="submit" class="btn btn-primary w-100" id="login-submit-btn">
{'ms3_customer_login' | lexicon}
</button>
Expand All @@ -79,6 +74,7 @@
{'ms3_customer_email' | lexicon} <span class="text-danger">*</span>
</label>
<input type="email" class="form-control" id="register-email" name="email"
autocomplete="email"
placeholder="{'ms3_customer_email_placeholder' | lexicon}" required>
</div>

Expand Down Expand Up @@ -112,6 +108,7 @@
{'ms3_customer_password' | lexicon} <span class="text-danger">*</span>
</label>
<input type="password" class="form-control" id="register-password" name="password"
autocomplete="new-password"
placeholder="{'ms3_customer_password_placeholder' | lexicon}" required>
<small class="form-text text-muted">
{'ms3_customer_password_hint' | lexicon}
Expand All @@ -123,6 +120,7 @@
{'ms3_customer_password_confirm' | lexicon} <span class="text-danger">*</span>
</label>
<input type="password" class="form-control" id="register-password-confirm" name="password_confirm"
autocomplete="new-password"
placeholder="{'ms3_customer_password_confirm_placeholder' | lexicon}" required>
</div>

Expand Down Expand Up @@ -151,4 +149,5 @@ window.ms3Lexicon.ms3_customer_err_password_mismatch = '{'ms3_customer_err_passw
window.ms3Lexicon.ms3_customer_err_privacy_required = '{'ms3_customer_err_privacy_required' | lexicon}';
window.ms3Lexicon.ms3_customer_register_success = '{'ms3_customer_register_success' | lexicon}';
window.ms3Lexicon.ms3_err_unknown = '{'ms3_err_unknown' | lexicon}';
window.ms3Lexicon.ms3_customer_password_recovery_not_available = '{'ms3_customer_password_recovery_not_available' | lexicon}';
</script>
22 changes: 11 additions & 11 deletions core/components/minishop3/elements/snippets/ms3_customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,20 @@
$modx->lexicon->load('minishop3:cart'); // For order details template

if (isset($_GET['action']) && $_GET['action'] === 'logout') {
if (!empty($_SESSION['ms3']['customer_token'])) {
$token = $_SESSION['ms3']['customer_token'];
$tokenObj = $modx->getObject(\MiniShop3\Model\msCustomerToken::class, ['token' => $token]);
if ($tokenObj) {
$tokenObj->remove();
/** @var \MiniShop3\Services\Customer\AuthManager $authManager */
$authManager = $modx->services->get('ms3_auth_manager');
if (!$authManager->logoutCurrentCustomer()) {
$modx->log(
\MODX\Revolution\modX::LOG_LEVEL_ERROR,
'[ms3_customer] logoutCurrentCustomer failed; forcing guest token mint'
);
if ($modx->services->has('ms3_token_service')) {
/** @var \MiniShop3\Services\TokenService $tokenService */
$tokenService = $modx->services->get('ms3_token_service');
$tokenService->persistApiToken(0);
}
}

if (isset($_SESSION['ms3'])) {
unset($_SESSION['ms3']['customer_id']);
unset($_SESSION['ms3']['customer_token']);
unset($_SESSION['ms3']['customer_token_expires']);
}

$loginPageId = $modx->getOption('ms3_customer_login_page_id', null, 1);
$modx->sendRedirect($modx->makeUrl($loginPageId));
exit;
Expand Down
4 changes: 4 additions & 0 deletions core/components/minishop3/lexicon/en/customer.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@
$_lang['ms3_customer_password'] = 'Password';
$_lang['ms3_customer_password_confirm'] = 'Confirm Password';
$_lang['ms3_customer_register_success'] = 'Registration successful';
$_lang['ms3_customer_register_success_login_required'] = 'Registration successful. Please sign in with your email and password.';
$_lang['ms3_customer_login_success'] = 'You have successfully logged in';
$_lang['ms3_customer_password_recovery_not_available'] = 'Password recovery is not available yet';
$_lang['ms3_customer_logout'] = 'Logout';
$_lang['ms3_customer_logout_success'] = 'You have been logged out';
$_lang['ms3_customer_logout_confirm'] = 'Are you sure you want to logout?';

// Errors - Authentication
$_lang['ms3_customer_err_login_required'] = 'Please provide email and password';
$_lang['ms3_customer_err_login_invalid'] = 'Invalid email or password';
$_lang['ms3_customer_err_login_blocked'] = 'This account is temporarily blocked. Try again later.';
$_lang['ms3_customer_err_login_inactive'] = 'This account is inactive. Contact the store administrator.';
$_lang['ms3_customer_err_login_rate_limit'] = 'Too many login attempts ({attempts}/{max}). Try again in {minutes} minutes.';
$_lang['ms3_customer_err_email_required'] = 'Email is required';
$_lang['ms3_customer_err_email_invalid'] = 'Invalid email format';
Expand Down
4 changes: 4 additions & 0 deletions core/components/minishop3/lexicon/ru/customer.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@
$_lang['ms3_customer_password'] = 'Пароль';
$_lang['ms3_customer_password_confirm'] = 'Подтверждение пароля';
$_lang['ms3_customer_register_success'] = 'Регистрация прошла успешно';
$_lang['ms3_customer_register_success_login_required'] = 'Регистрация прошла успешно. Войдите с email и паролем.';
$_lang['ms3_customer_login_success'] = 'Вы успешно вошли в систему';
$_lang['ms3_customer_password_recovery_not_available'] = 'Восстановление пароля пока недоступно';
$_lang['ms3_customer_logout'] = 'Выход';
$_lang['ms3_customer_logout_success'] = 'Вы вышли из системы';
$_lang['ms3_customer_logout_confirm'] = 'Вы действительно хотите выйти?';

// Errors - Authentication
$_lang['ms3_customer_err_login_required'] = 'Укажите email и пароль';
$_lang['ms3_customer_err_login_invalid'] = 'Неверный email или пароль';
$_lang['ms3_customer_err_login_blocked'] = 'Аккаунт временно заблокирован. Попробуйте позже.';
$_lang['ms3_customer_err_login_inactive'] = 'Аккаунт неактивен. Обратитесь к администратору магазина.';
$_lang['ms3_customer_err_login_rate_limit'] = 'Превышен лимит попыток входа ({attempts}/{max}). Попробуйте через {minutes} минут.';
$_lang['ms3_customer_err_email_required'] = 'Email обязателен для заполнения';
$_lang['ms3_customer_err_email_invalid'] = 'Указан некорректный email';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

namespace MiniShop3\Controllers\Api\Manager;

use MiniShop3\Controllers\Auth\PasswordAuthProvider;
use MiniShop3\Model\msCustomer;
use MiniShop3\Router\HttpStatus;
use MiniShop3\Router\Response;
use MiniShop3\Services\Customer\AuthManager;
use MODX\Revolution\modX;

/**
Expand Down Expand Up @@ -177,13 +179,19 @@ public function update(array $data = []): array

foreach ($allowedFields as $field) {
if (isset($data[$field])) {
$customer->set($field, $data[$field]);
$value = $data[$field];
if ($field === 'email' && is_string($value)) {
$value = AuthManager::normalizeEmail($value);
}
$customer->set($field, $value);
}
}

if (!empty($data['password'])) {
$hashedPassword = password_hash($data['password'], PASSWORD_DEFAULT);
$customer->set('password', $hashedPassword);
if (isset($data['password'])) {
$password = trim((string)$data['password']);
if ($password !== '') {
$customer->set('password', PasswordAuthProvider::hashPassword($password));
}
}

if (!$customer->save()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace MiniShop3\Controllers\Auth;

use MiniShop3\Model\msCustomer;
use MiniShop3\Services\Customer\AuthManager;
use MODX\Revolution\modX;

/**
Expand Down Expand Up @@ -40,6 +41,14 @@ public function __construct(modX $modx)
$this->modx = $modx;
}

/**
* Normalize email for lookup and storage (delegates to AuthManager).
*/
public static function normalizeEmail(string $email): string
{
return AuthManager::normalizeEmail($email);
}

/**
* Authenticate by email and password
*
Expand All @@ -48,7 +57,7 @@ public function __construct(modX $modx)
*/
public function authenticate(array $credentials): ?msCustomer
{
$email = trim($credentials['email'] ?? '');
$email = self::normalizeEmail($credentials['email'] ?? '');
$password = $credentials['password'] ?? '';

if (empty($email) || empty($password)) {
Expand All @@ -59,11 +68,19 @@ public function authenticate(array $credentials): ?msCustomer
return null;
}

// Find customer by email
/** @var msCustomer $customer */
// Normalized lookup, then legacy mixed-case exact match (utf8mb4_bin / old rows).
/** @var msCustomer|null $customer */
$customer = $this->modx->getObject(msCustomer::class, [
'email' => $email,
]);
if (!$customer) {
$raw = trim((string)($credentials['email'] ?? ''));
if ($raw !== '' && $raw !== $email) {
$customer = $this->modx->getObject(msCustomer::class, [
'email' => $raw,
]);
}
}

if (!$customer) {
$this->modx->log(
Expand Down Expand Up @@ -92,16 +109,32 @@ public function authenticate(array $credentials): ?msCustomer
return null;
}

// Soft-migrate legacy mixed-case emails to canonical lowercase.
if ((string)$customer->get('email') !== $email) {
$customer->set('email', $email);
if (!$customer->save()) {
$this->modx->log(
modX::LOG_LEVEL_WARN,
"[PasswordAuthProvider] Failed to normalize email for customer #{$customer->id}"
);
}
}

// Check if password hash needs to be updated (if bcrypt settings changed)
if (password_needs_rehash($hashedPassword, PASSWORD_BCRYPT)) {
$newHash = password_hash($password, PASSWORD_BCRYPT);
$customer->set('password', $newHash);
$customer->save();

$this->modx->log(
modX::LOG_LEVEL_INFO,
"[PasswordAuthProvider] Password rehashed for customer #{$customer->id}"
);
if ($customer->save()) {
$this->modx->log(
modX::LOG_LEVEL_INFO,
"[PasswordAuthProvider] Password rehashed for customer #{$customer->id}"
);
} else {
$this->modx->log(
modX::LOG_LEVEL_WARN,
"[PasswordAuthProvider] Failed to persist rehashed password for customer #{$customer->id}"
);
}
}

$this->modx->log(
Expand Down
Loading