313 lines
13 KiB
PHP
313 lines
13 KiB
PHP
<?php
|
|
|
|
namespace KupShop\UserOauthBundle\Security;
|
|
|
|
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
|
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
|
|
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
|
|
use KupShop\GraphQLBundle\EventListener\JsShopRefreshListener;
|
|
use KupShop\KupShopBundle\Context\CurrencyContext;
|
|
use KupShop\KupShopBundle\Context\LanguageContext;
|
|
use KupShop\KupShopBundle\Util\Contexts;
|
|
use KupShop\UserBundle\Event\UserRegisteredEvent;
|
|
use KupShop\UserOauthBundle\ResourceOwner\ICustomResourceOwner;
|
|
use Query\Operator;
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
|
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
|
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
|
use Symfony\Component\Security\Core\User\UserInterface;
|
|
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
|
|
|
class OauthUserProvider implements UserProviderInterface, OAuthAwareUserProviderInterface
|
|
{
|
|
use \DatabaseCommunication;
|
|
|
|
/** @var EventDispatcherInterface */
|
|
protected $eventDispatcher;
|
|
|
|
public function __construct(
|
|
protected TokenStorageInterface $tokenStorage,
|
|
protected SessionInterface $session,
|
|
protected RequestStack $requestStack,
|
|
) {
|
|
}
|
|
|
|
public function loadUserByUsername($username)
|
|
{
|
|
return $this->loadUserByIdentifier($username);
|
|
}
|
|
|
|
public function loadUserByIdentifier(string $identifier): UserInterface
|
|
{
|
|
throw new UserNotFoundException();
|
|
}
|
|
|
|
/**
|
|
* Refreshes the user for the account interface.
|
|
*
|
|
* It is up to the implementation to decide if the user data should be
|
|
* totally reloaded (e.g. from the database), or if the UserInterface
|
|
* object can just be merged into some internal array of users / identity
|
|
* map.
|
|
*
|
|
* @throws UnsupportedUserException if the account is not supported
|
|
*/
|
|
public function refreshUser(UserInterface $user): UserInterface
|
|
{
|
|
if ($user instanceof OauthUser) {
|
|
$userObject = $this->getUserByClientID($user->getProvider(), $user->getClientID());
|
|
if ($userObject) {
|
|
$userObject->activateUser();
|
|
|
|
return new OauthUser(
|
|
$userObject->id,
|
|
$userObject->email,
|
|
$user->getProvider(),
|
|
$user->getClientID(),
|
|
['ROLE_USER'],
|
|
$userObject
|
|
);
|
|
} else {
|
|
// this usually happens when user unbinds his fb/google account and is currently logged in with it
|
|
$this->tokenStorage->setToken(null);
|
|
// hack to preserve userMessages
|
|
$userMessages = $this->session->get('userMessages');
|
|
$this->session->invalidate();
|
|
$this->session->set('userMessages', $userMessages);
|
|
if (findModule(\Modules::JS_SHOP)) {
|
|
// set `jsShopReload: true` session to notify js-cart
|
|
$this->session->set(JsShopRefreshListener::SESSION_NAME, true);
|
|
}
|
|
addUserMessage(translate('oauth', 'login')['invalid_login']);
|
|
redirection(path('kupshop_user_login_login'));
|
|
}
|
|
}
|
|
|
|
throw new UnsupportedUserException();
|
|
}
|
|
|
|
/**
|
|
* Whether this provider supports the given user class.
|
|
*
|
|
* @param string $class
|
|
*/
|
|
public function supportsClass($class): bool
|
|
{
|
|
return $class === OauthUser::class;
|
|
}
|
|
|
|
private function getUserByClientID(string $provider, string $clientID)
|
|
{
|
|
$providerRow = $this->selectSQL('users_provider_ids', [
|
|
'provider' => $provider,
|
|
'client_id' => $clientID,
|
|
])->fetch();
|
|
if ($providerRow) {
|
|
$user = \User::createFromId($providerRow['id_user']);
|
|
}
|
|
|
|
return $user ?? false;
|
|
}
|
|
|
|
/**
|
|
* Loads the user by a given UserResponseInterface object.
|
|
*
|
|
* @return UserInterface
|
|
*
|
|
* @throws UserNotFoundException if the user is not found
|
|
*/
|
|
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
|
|
{
|
|
$email = $response->getEmail();
|
|
$resourceOwner = $response->getResourceOwner();
|
|
$provider = $resourceOwner->getName();
|
|
$clientID = $response->getUsername();
|
|
$user = $this->getUserByClientID($provider, $clientID ?? '');
|
|
$token = $this->tokenStorage->getToken();
|
|
|
|
if ($user && !$user->isActive()) {
|
|
throw new UserNotFoundException();
|
|
}
|
|
|
|
if ($user && $token?->getUser()) {
|
|
if ($this->session->get('bind_external_account') === $provider) {
|
|
// external account is already linked so refuse bind
|
|
$this->session->remove('bind_external_account');
|
|
throw new OauthBindRefusedException('Account already binded.');
|
|
} else {
|
|
// unintended bind (eg: when user is already logged in, but goes to /connect/google without proper session)
|
|
throw new ShouldLogOutException('Unintended bind.');
|
|
}
|
|
}
|
|
|
|
// bind account to current user
|
|
// to restrict binding for non logged users add condition: $token && $token->isAuthenticated()
|
|
if (!$user) {
|
|
$tokenUsername = $token?->getUser() ? $token->getUserIdentifier() : null;
|
|
// unintended bind check
|
|
if ($tokenUsername && $this->session->get('bind_external_account') !== $provider) {
|
|
throw new OauthBindRefusedException('Unintended bind.');
|
|
}
|
|
$this->session->remove('bind_external_account');
|
|
|
|
$user = \User::createFromLogin($tokenUsername ?? $email);
|
|
if ($user) {
|
|
try {
|
|
$user = sqlGetConnection()->transactional(function () use ($user, $provider, $clientID, $email) {
|
|
$this->insertSQL(
|
|
'users_provider_ids',
|
|
[
|
|
'id_user' => $user->id,
|
|
'provider' => $provider,
|
|
'client_id' => $clientID,
|
|
'email' => $email,
|
|
]
|
|
);
|
|
|
|
return $this->getUserByClientID($provider, $clientID);
|
|
});
|
|
|
|
if ($token?->getUser()) {
|
|
if ($user) {
|
|
addUserMessage(
|
|
sprintf(
|
|
translate('oauth', 'login')['bind_success'],
|
|
ucfirst($provider),
|
|
$email
|
|
),
|
|
'success'
|
|
);
|
|
if ($resourceOwner instanceof ICustomResourceOwner) {
|
|
$resourceOwner->updateUserData($user, $response);
|
|
}
|
|
throw new OauthBindSuccessException();
|
|
} else {
|
|
throw new OauthBindFailedException('User not found.');
|
|
}
|
|
}
|
|
} catch (UniqueConstraintViolationException $e) {
|
|
throw new OauthBindFailedException('OAuth already binded.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// add new user
|
|
if (!$user && !empty($email) && !empty($response->getFirstName()) && !empty($response->getLastName())
|
|
&& !empty($clientID)
|
|
) {
|
|
try {
|
|
$user = sqlGetConnection()->transactional(function () use ($email, $response, $provider, $clientID) {
|
|
$insert = [
|
|
'email' => $email,
|
|
'name' => $response->getFirstName(),
|
|
'surname' => $response->getLastName(),
|
|
'date_reg' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
if (findModule(\Modules::CURRENCIES)) {
|
|
$currencyContext = Contexts::get(CurrencyContext::class);
|
|
$insert['currency'] = $currencyContext->getActiveId();
|
|
}
|
|
|
|
if (findModule(\Modules::TRANSLATIONS)) {
|
|
$languageContext = Contexts::get(LanguageContext::class);
|
|
$insert['id_language'] = $languageContext->getActiveId();
|
|
}
|
|
|
|
try {
|
|
$this->insertSQL('users', $insert);
|
|
$userId = sqlInsertId();
|
|
} catch (UniqueConstraintViolationException) {
|
|
if (!($userId = $this->checkNewsletterUser($insert['email'], $insert))) {
|
|
// pokud uzivatel existuje a je neaktivni z jineho duvodu, nez newsletter, tak ho nedovolim sparovat/prihlasit
|
|
throw new UserNotFoundException();
|
|
}
|
|
}
|
|
|
|
$this->insertSQL(
|
|
'users_provider_ids',
|
|
[
|
|
'id_user' => $userId,
|
|
'provider' => $provider,
|
|
'client_id' => $clientID,
|
|
'email' => $email,
|
|
]
|
|
);
|
|
|
|
$this->eventDispatcher->dispatch(
|
|
new UserRegisteredEvent(\User::createFromId($userId), $response->getResourceOwner()->getName())
|
|
);
|
|
|
|
return $this->getUserByClientID($provider, $clientID);
|
|
});
|
|
// redirect to user account
|
|
addUserMessage(translate('oauth', 'login')['registration_success'], 'success');
|
|
$this->requestStack->getMainRequest()->attributes->set('gtm_registration', [
|
|
'email' => $email ?? 'no-email',
|
|
'firstname' => $response->getFirstName() ?? 'no-firstname',
|
|
]);
|
|
// force redirection to user settings
|
|
$this->session->set('_security.main.target_path', path('settings'));
|
|
} catch (UniqueConstraintViolationException $e) {
|
|
}
|
|
}
|
|
|
|
// user was not found by clientID, but we already have his email, so we can't register him
|
|
// LoginView catches this error and displays translatable message:
|
|
// translate('oauth', 'login')['unknown_clientid_with_known_email']
|
|
if (!$user) {
|
|
throw new ClientIDNotFoundException($email ?? '');
|
|
}
|
|
|
|
if ($resourceOwner instanceof ICustomResourceOwner) {
|
|
$resourceOwner->updateUserData($user, $response);
|
|
}
|
|
|
|
if ($this->session->get('oauth_register_redirect')) {
|
|
$this->session->set('_security.main.target_path', $this->session->get('oauth_register_redirect'));
|
|
$this->session->remove('oauth_register_redirect');
|
|
}
|
|
|
|
// user found or successfully created
|
|
return new OauthUser($user->id, $user->email, $provider, $clientID, ['ROLE_USER'], $user);
|
|
}
|
|
|
|
private function checkNewsletterUser(string $email, array $update = []): ?int
|
|
{
|
|
$user = sqlQueryBuilder()
|
|
->select('id, figure, passw, get_news, date_subscribe, date_unsubscribe')
|
|
->from('users')
|
|
->where(Operator::equals(['email' => $email]))
|
|
->execute()->fetchAssociative();
|
|
|
|
if (!$user) {
|
|
return null;
|
|
}
|
|
|
|
// pokud je to neaktivni uzivatel, kterej nejspis vznikl pres newsletter, tak ho rovnou zaktivuju at to prihlaseni pres socky vubec funguje
|
|
if ($user['figure'] === 'N' && empty($user['passw']) && ($user['get_news'] === 'Y' || !empty($user['date_subscribe']) || !empty($user['date_unsubscribe']))) {
|
|
sqlQueryBuilder()
|
|
->update('users')
|
|
->directValues(array_merge($update, ['figure' => 'Y']))
|
|
->where(Operator::equals(['id' => $user['id']]))
|
|
->execute();
|
|
} else {
|
|
// pokud to neni newsletter uzivatel, tak vracim null
|
|
return null;
|
|
}
|
|
|
|
return $user['id'];
|
|
}
|
|
|
|
/**
|
|
* @required
|
|
*/
|
|
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
|
{
|
|
$this->eventDispatcher = $eventDispatcher;
|
|
}
|
|
}
|