diff --git a/wa-apps/apiexplorer/client/README.md b/wa-apps/apiexplorer/client/README.md index ab578b534..860cbde40 100644 --- a/wa-apps/apiexplorer/client/README.md +++ b/wa-apps/apiexplorer/client/README.md @@ -5,6 +5,12 @@ npm install ``` +### Environment variables +Create `.env.development.local` (see `.env.development.local.example`) +Where +- `VUE_APP_WA_DEV`: URL to the Webasyst framework root +- `VUE_APP_BASE_URL`: Path to API Explorer backend from domain root + ### Compiles and hot-reloads for development ``` npm run serve diff --git a/wa-apps/apiexplorer/client/package.json b/wa-apps/apiexplorer/client/package.json index 867ace2f0..2d43710f6 100644 --- a/wa-apps/apiexplorer/client/package.json +++ b/wa-apps/apiexplorer/client/package.json @@ -21,8 +21,8 @@ "swagger-client": "^3.18.1", "v-calendar": "^3.0.0-alpha.6", "vue": "^3.2.27", - "vue-i18n": "^9.2.0-beta.28", - "vue-router": "^4.0.12", + "vue-i18n": "^9.2.2", + "vue-router": "^4.1.5", "vuex": "^4.0.2" }, "devDependencies": { diff --git a/wa-apps/apiexplorer/client/public/index.html b/wa-apps/apiexplorer/client/public/index.html index f3d107057..9ad5adeda 100644 --- a/wa-apps/apiexplorer/client/public/index.html +++ b/wa-apps/apiexplorer/client/public/index.html @@ -19,7 +19,8 @@ "rootUrl": "/", "user_id": "1", "development_mode": "1", - "locale": "<%= VUE_APP_I18N_LOCALE %>" + "locale": "<%= VUE_APP_I18N_LOCALE %>", + "centralUrl": "<%= VUE_APP_WA_CENTRAL %>" }; <% } %> diff --git a/wa-apps/apiexplorer/client/src/components/CodeBlock.vue b/wa-apps/apiexplorer/client/src/components/CodeBlock.vue new file mode 100644 index 000000000..1313b894d --- /dev/null +++ b/wa-apps/apiexplorer/client/src/components/CodeBlock.vue @@ -0,0 +1,50 @@ + + + diff --git a/wa-apps/apiexplorer/client/src/components/SchemaInput.vue b/wa-apps/apiexplorer/client/src/components/SchemaInput.vue index 55e656f47..48edbd321 100644 --- a/wa-apps/apiexplorer/client/src/components/SchemaInput.vue +++ b/wa-apps/apiexplorer/client/src/components/SchemaInput.vue @@ -20,6 +20,16 @@ +
+
+ +
+
@@ -132,7 +142,8 @@ export default { }, data() { return { - l_value: {} + l_value: {}, + filename: null }; }, created() { @@ -167,6 +178,19 @@ export default { this.$emit('update:modelValue', val); this.$emit('updated'); }, + readFile: function (event) { + if (event.target.files.length == 0) { + return; + } + const file = event.target.files[0]; + if (Object.prototype.toString.call(file) !== '[object File]') { + return; + } + const reader = new FileReader(); + reader.onload = e => this.updateValue(e.target.result); + reader.readAsBinaryString(file); + this.filename = file.name; + }, updateArrValue: function (val, index) { this.l_value[index] = val; this.$emit('update:modelValue', Object.values(this.l_value)); diff --git a/wa-apps/apiexplorer/client/src/i18n.js b/wa-apps/apiexplorer/client/src/i18n.js index 39bacc341..995b62891 100644 --- a/wa-apps/apiexplorer/client/src/i18n.js +++ b/wa-apps/apiexplorer/client/src/i18n.js @@ -1,5 +1,5 @@ //import Vue from "vue"; -import { createI18n } from "vue-i18n"; +import { createI18n } from "vue-i18n/index"; //Vue.use(VueI18n); diff --git a/wa-apps/apiexplorer/client/src/locales/en.json b/wa-apps/apiexplorer/client/src/locales/en.json index 330b30002..ab39dc10c 100644 --- a/wa-apps/apiexplorer/client/src/locales/en.json +++ b/wa-apps/apiexplorer/client/src/locales/en.json @@ -4,6 +4,7 @@ "no-api-hint": "If you are a developer of this app, please refer to this guide on how to create an API-enabled Webasyst app.", "empty": "empty", "Request": "Request", + "Response": "Response", "Schema": "Schema", "Headers": "Headers", "Data": "Data", @@ -38,5 +39,11 @@ "Date & time": "Date & time", "Run API": "Run API", "Body": "Body", - "OpenAPI Document": "OpenAPI Document" + "OpenAPI Document": "OpenAPI Document", + "Query string parameters": "Query string parameters", + "Request body parameters": "Request body parameters", + "Select file": "Select file", + "Response data format": "Response data format", + "show": "show", + "hide": "hide" } diff --git a/wa-apps/apiexplorer/client/src/locales/ru.json b/wa-apps/apiexplorer/client/src/locales/ru.json index e8f6c447d..ea302e165 100644 --- a/wa-apps/apiexplorer/client/src/locales/ru.json +++ b/wa-apps/apiexplorer/client/src/locales/ru.json @@ -28,6 +28,7 @@ "Headers": "Заголовки", "Schema": "Схема", "Request": "Запрос", + "Response": "Ответ", "no-api-message": "Приложение «{0}» не предоставляет публичного API", "No API": "Нет API", "no-api-hint": "Если вы являетесь разработчиком данного приложения, ознакомьтесь с инструкцией по созданию публичного API в приложениях Webasyst.", @@ -38,5 +39,11 @@ "URL parameters": "Параметры строки запроса", "Run API": "Запустить API", "Body": "Содержимое", - "OpenAPI Document": "Описание OpenAPI" + "OpenAPI Document": "Описание OpenAPI", + "Query string parameters": "Параметры строки запроса", + "Request body parameters": "Параметры тела запроса", + "Select file": "Выберите файл", + "Response data format": "Формат ответа", + "show": "показать", + "hide": "скрыть" } diff --git a/wa-apps/apiexplorer/client/src/views/About.vue b/wa-apps/apiexplorer/client/src/views/About.vue index 60930cad2..12ec1d042 100644 --- a/wa-apps/apiexplorer/client/src/views/About.vue +++ b/wa-apps/apiexplorer/client/src/views/About.vue @@ -27,6 +27,7 @@ export default { const resp = await this.axios.get('?module=pages&action=about'); this.page = resp.data; this.state.loading = false; + document.title = 'About — API Explorer'; } } diff --git a/wa-apps/apiexplorer/client/src/views/Application.vue b/wa-apps/apiexplorer/client/src/views/Application.vue index 15d85f25f..052ca9185 100644 --- a/wa-apps/apiexplorer/client/src/views/Application.vue +++ b/wa-apps/apiexplorer/client/src/views/Application.vue @@ -37,6 +37,7 @@ export default { mounted() { this.app_id = this.$route.params.name; this.app = this.$store.getters.getApp(this.app_id); + document.title = this.app.name + ' — API Explorer'; } } diff --git a/wa-apps/apiexplorer/client/src/views/Method.vue b/wa-apps/apiexplorer/client/src/views/Method.vue index 8e4d4e3f4..493d79cd8 100644 --- a/wa-apps/apiexplorer/client/src/views/Method.vue +++ b/wa-apps/apiexplorer/client/src/views/Method.vue @@ -26,6 +26,17 @@
+ +
+
+ {{ $t('Response data format') }} + + + + +
+
+

@@ -58,8 +69,9 @@
-
-
+
+
{{ $t('Query string parameters') }}
+
-
+
+
{{ $t('Request body parameters') }}
+
+
    +
  • + {{ param.name }} + + + + * — + {{ param.description }} +
  • +
+
@@ -151,6 +176,19 @@
+ +
+
    +
  • + {{ param.name }} + + + + * — + {{ param.description }} +
  • +
+
@@ -185,59 +223,73 @@
-
+ {$event_html} + diff --git a/wa-system/design/templates/Theme.html b/wa-system/design/templates/Theme.html index 5029697ad..22998761f 100644 --- a/wa-system/design/templates/Theme.html +++ b/wa-system/design/templates/Theme.html @@ -716,7 +716,8 @@
[s`About this theme`]
theme_routes: {$theme_routes|json_encode}, design_url: {$design_url|json_encode}, locale: {$_locale|json_encode}, - is_wa2: true + is_wa2: true, + wa_url: {$wa_url|json_encode} }); })(jQuery); diff --git a/wa-system/file/waNet.class.php b/wa-system/file/waNet.class.php index cdb9ad584..661a79cd0 100644 --- a/wa-system/file/waNet.class.php +++ b/wa-system/file/waNet.class.php @@ -33,6 +33,7 @@ class waNet const METHOD_GET = 'GET'; const METHOD_POST = 'POST'; const METHOD_PUT = 'PUT'; + const METHOD_PATCH = 'PATCH'; // since 2.7.0 const METHOD_DELETE = 'DELETE'; const FORMAT_JSON = 'json'; @@ -260,6 +261,7 @@ protected function buildRequest(&$url, &$content, &$method) switch ($method) { case self::METHOD_POST: case self::METHOD_PUT: + case self::METHOD_PATCH: case self::METHOD_DELETE: $content = $this->encodeRequest($content); break; @@ -737,14 +739,9 @@ private function getCurl($url, $content, $method, $curl_options = array()) $curl_options[CURLOPT_POSTFIELDS] = $content; } break; - case self::METHOD_PUT: - $curl_options[CURLOPT_CUSTOMREQUEST] = $method; - if ($content) { - $curl_options[CURLOPT_POST] = 0; - $curl_options[CURLOPT_POSTFIELDS] = $content; - } - break; case self::METHOD_DELETE: + case self::METHOD_PATCH: + case self::METHOD_PUT: $curl_options[CURLOPT_CUSTOMREQUEST] = $method; if ($content) { $curl_options[CURLOPT_POST] = 0; @@ -854,7 +851,7 @@ private function getStreamContext($content, $method) $context_params['header'] = implode("\r\n", $headers); //5.2.10 array support - if (in_array($method, array(self::METHOD_POST, self::METHOD_PUT))) { + if (in_array($method, array(self::METHOD_POST, self::METHOD_PUT, self::METHOD_PATCH))) { $context_params += array( 'method' => $method, 'content' => $content, @@ -865,6 +862,8 @@ private function getStreamContext($content, $method) 'max_redirects' => 5, ); + $context_params = ['http' => $context_params]; + //SSL if (!empty($this->options['verify'])) { $context_params['ssl']['verify_peer'] = true; @@ -889,7 +888,7 @@ private function getStreamContext($content, $method) $context_params['ssl']['verify_peer_name'] = false; } - return stream_context_create(array('http' => $context_params,)); + return stream_context_create($context_params); } /** diff --git a/wa-system/file/waTheme.class.php b/wa-system/file/waTheme.class.php index ddb80576b..60a39c36c 100644 --- a/wa-system/file/waTheme.class.php +++ b/wa-system/file/waTheme.class.php @@ -2864,6 +2864,7 @@ public function extractSettings($archive_path) foreach ($files as $file) { $file_name = ifempty($file['filename']); $file_ext = pathinfo($file_name, PATHINFO_EXTENSION); + $file_ext = mb_strtolower($file_ext); foreach ($missed_images as $i => $image) { $pattern = "@(/|^)".wa_make_pattern($image, '@')."$@"; diff --git a/wa-system/image/waImage.class.php b/wa-system/image/waImage.class.php index 5b97ab295..3d0201812 100644 --- a/wa-system/image/waImage.class.php +++ b/wa-system/image/waImage.class.php @@ -55,7 +55,9 @@ public function __construct($file) { try { $file = realpath($file); - $image_info = @getimagesize($file); + if ($file) { + $image_info = @getimagesize($file); + } } catch (Exception $e){} if (empty($file) OR empty($image_info)) { diff --git a/wa-system/licensing/waLicensing.class.php b/wa-system/licensing/waLicensing.class.php new file mode 100644 index 000000000..c6daae5d0 --- /dev/null +++ b/wa-system/licensing/waLicensing.class.php @@ -0,0 +1,235 @@ +isPremium() + * @since 2.7.0 + */ +class waLicensing +{ + /** + * Can be used to create subclasses limited to a certain product slug + * @example + * class appLicensing { + * protected static $static_slug = 'app'; + * } + * appLicensing::check()->isPremium() + * + * @var string + */ + protected static $static_slug = null; + + /** @var string */ + protected $slug; + protected $slug_parse = null; + + protected $license = null; + + protected static $cache = []; + + /** @param string */ + public function __construct($slug) + { + $this->slug = $slug; + if (!isset(self::$cache[$slug])) { + self::$cache[$slug] = $this; + } + } + + /** + * @param string + * @return waLicensing + * @throws waException + */ + public static function check($slug='') + { + $slug = ifempty($slug, static::$static_slug); + if (empty($slug)) { + throw new waException('$slug is required'); + } + if (isset(self::$cache[$slug])) { + return self::$cache[$slug]; + } + return new static($slug); + } + + /** @return string */ + public function getSlug() + { + return $this->slug; + } + + /** + * Whether product is in Standard (non-premium) mode. + * + * @return bool + */ + public function isStandard() + { + return !self::isPremium(); + } + + /** + * Whether product is in Premium mode. In this mode, more features are available to user. + * + * Premium mode is enabled if license allows it, or if app at any point in past + * had a license with premium features enabled, or if any of premium features + * are currently turned on. + * + * Installer occasionally checks licensing for all installed products independently + * and will take measures in case of violation. It is not this class' job to force + * any action or even try to outsmart a clever hacker. + * + * @return bool + */ + public function isPremium() + { + // Used to have Premium license in the past? + $is_premium = $this->getSetting('license_premium'); + if ($is_premium) { + return true; + } + + // If any premium feature is enabled, force Premium license mode. + // Installer checks this occasionally and enforces licensing penalties. + if (self::isAnyPremiumFeatureEnabled()) { + return true; + } + + // Ask Installer if we have a proper license. + if (self::hasPremiumLicense()) { + $this->setSetting('license_premium', date('Y-m-d H:i:s')); + return true; + } + + return false; + } + + /** + * @return bool whether Shop app has any premium feature turned on + */ + public function isAnyPremiumFeatureEnabled() + { + try { + list($app_id, $ext_id, $type) = $this->parseSlug(); + switch ($type) { + case 'app': + return wa($app_id)->getConfig()->isAnyPremiumFeatureEnabled(); + case 'plugin': + return wa($app_id)->getPlugin($ext_id)->isAnyPremiumFeatureEnabled(); + case 'widget': + case 'payment': + case 'shipping': + case 'theme': + case 'sms': + return false; // not supported + } + } catch (waException $e) { + } + return false; + } + + /** + * @return bool whether installation has a proper license (basic or premium) bound to it + */ + public function hasLicense() + { + $license = self::getLicense(); + return !empty($license['status']); + } + + /** + * @return bool whether installation has a license bound to it with Premium features enabled + */ + public function hasPremiumLicense() + { + $license = self::getLicense(); + return ifempty($license, 'options', 'edition', null) === 'PREMIUM'; + } + + /** + * @return bool whether installation has a basic (non-premium) license bound to it + */ + public function hasStandardLicense() + { + return $this->hasLicense() && !$this->hasPremiumLicense(); + } + + /** + * @return null|array + */ + protected function getLicense() + { + if ($this->license === null) { + $this->license = false; + try { + if (wa()->appExists('installer')) { + wa('installer'); + $this->license = installerHelper::checkLicense($this->slug); + } + } catch (waException $e) { + } + } + + return ifempty($this->license); + } + + protected function getSettingsProductKey() + { + list($app_id, $ext_id, $type) = $this->parseSlug(); + switch ($type) { + case 'app': + return $app_id; + case 'plugin': + return $app_id.'.'.$ext_id; + case 'widget': + case 'theme': + case 'payment': + case 'shipping': + case 'sms': + return "{$app_id}.{$type}.{$ext_id}"; + } + } + + public function getSetting($name, $default=null) + { + $product_key = $this->getSettingsProductKey(); + if (!$product_key) { + return; // not supported + } + return $this->getAppSettingModel()->get($product_key, $name, $default); + } + + public function setSetting($name, $value) + { + $product_key = $this->getSettingsProductKey(); + if (!$product_key) { + return; // not supported + } + if ($value !== null) { + $this->getAppSettingModel()->set($product_key, $name, $value); + } else { + $this->getAppSettingModel()->del($product_key, $name); + } + } + + protected function getAppSettingModel() + { + static $app_settings_model = null; + if ($app_settings_model === null) { + $app_settings_model = new waAppSettingsModel(); + } + return $app_settings_model; + } + + protected function parseSlug() + { + if ($this->slug_parse === null) { + if (wa()->appExists('installer')) { + wa('installer'); + $this->slug_parse = installerHelper::parseSlug($this->slug); + } else { + $this->slug_parse = [null, null, 'unsupported']; + } + } + return $this->slug_parse; + } +} diff --git a/wa-system/plugin/waPlugin.class.php b/wa-system/plugin/waPlugin.class.php index fa376d11b..98640d1cd 100644 --- a/wa-system/plugin/waPlugin.class.php +++ b/wa-system/plugin/waPlugin.class.php @@ -547,4 +547,12 @@ protected function buildFullTemplatePath($scope, $template_path) return $templates_dir . '/' . $template_path; } + /** + * @return bool + * @see waLicensing + */ + public function isAnyPremiumFeatureEnabled() + { + return false; + } } diff --git a/wa-system/waid/waWebasystIDAccessTokenManager.class.php b/wa-system/waid/waWebasystIDAccessTokenManager.class.php index a9db9606f..0e2b6a675 100644 --- a/wa-system/waid/waWebasystIDAccessTokenManager.class.php +++ b/wa-system/waid/waWebasystIDAccessTokenManager.class.php @@ -83,6 +83,18 @@ public function releaseToken(array $params, $secret) 'email' => isset($params['email']) && is_string($params['email']) ? $params['email'] : '', ]; + if (isset($params['phone']) && is_string($params['phone'])) { + $payload['phone'] = $params['phone']; + } + + if (isset($params['two_fa_mode']) && is_string($params['two_fa_mode'])) { + $payload['two_fa_mode'] = $params['two_fa_mode']; + } + + if (isset($params['two_fa_time']) && is_string($params['two_fa_time'])) { + $payload['two_fa_time'] = $params['two_fa_time']; + } + if (!empty(ifempty($params['aux_info']))) { $payload['aux_info'] = json_encode($params['aux_info']); } @@ -164,6 +176,18 @@ public function extractTokenInfo($token) $info['email'] = $payload['email']; } + if (isset($payload['phone']) && is_string($payload['phone'])) { + $info['phone'] = $payload['phone']; + } + + if (isset($payload['two_fa_mode']) && is_string($payload['two_fa_mode'])) { + $info['two_fa_mode'] = $payload['two_fa_mode']; + } + + if (isset($payload['two_fa_time']) && is_string($payload['two_fa_time'])) { + $info['two_fa_time'] = $payload['two_fa_time']; + } + if (isset($payload['aux_info']) && is_string($payload['aux_info'])) { $aux_info = json_decode($payload['aux_info'], true); if (is_array($aux_info)) { diff --git a/wa-system/waid/waWebasystIDAuthAdapter.class.php b/wa-system/waid/waWebasystIDAuthAdapter.class.php index 28289596b..456ec1074 100644 --- a/wa-system/waid/waWebasystIDAuthAdapter.class.php +++ b/wa-system/waid/waWebasystIDAuthAdapter.class.php @@ -197,7 +197,7 @@ protected function processAuthResponse() public function getRedirectUri() { $auth_url = $this->getAuthCodeUrl(); - if (wa()->getUser()->getId() > 0) { + if (!empty($auth_url) && wa()->getUser()->getId() > 0) { $user_info = $this->getUserInfo(); $user_info_str = waUtils::urlSafeBase64Encode(json_encode($user_info)); $auth_url .= '&info=' . $user_info_str; diff --git a/wa-system/waid/waWebasystIDWAAuth.class.php b/wa-system/waid/waWebasystIDWAAuth.class.php index 378652bc6..10b0481f5 100644 --- a/wa-system/waid/waWebasystIDWAAuth.class.php +++ b/wa-system/waid/waWebasystIDWAAuth.class.php @@ -96,10 +96,10 @@ public function getCallbackUrl($absolute = true) $ignore = array_flip($ignore); foreach (waRequest::get() as $key => $value) { if (!isset($ignore[$key]) && $value) { - $callback_url .= '&' . $key . '=' . $value; + $callback_url .= '&' . $key . '=' . urlencode($value); } } - + return $callback_url; } @@ -154,7 +154,7 @@ protected function getAuthUrl(array $params = []) { $referrer_url = isset($params['referrer_url']) ? $params['referrer_url'] : null; - if (!$referrer_url) { + if (!$referrer_url && !waRequest::request('background_process')) { $url = wa()->getConfig()->getRequestUrl(true, false); $url = ltrim($url, '/'); $domain = wa()->getConfig()->getDomain(); @@ -166,9 +166,10 @@ protected function getAuthUrl(array $params = []) } } - $referrer_url = waUtils::urlSafeBase64Encode($referrer_url); - - $params['referrer_url'] = $referrer_url; + if (!empty($referrer_url)) { + $referrer_url = waUtils::urlSafeBase64Encode($referrer_url); + $params['referrer_url'] = $referrer_url; + } if ($this->getClientManager()->isBackendAuthForced()) { $params['mode'] = 'forced'; @@ -263,7 +264,32 @@ protected function getAuthCodeUrl() if (!$this->isClientConnected()) { return ''; } - return parent::getAuthCodeUrl(); + + $auth_url = parent::getAuthCodeUrl(); + if (empty($auth_url)) { + return $auth_url; + } + + $phone = waContactPhoneField::cleanPhoneNumber(waRequest::get('phone', '', waRequest::TYPE_STRING_TRIM)); + if (empty($phone) || !(new waPhoneNumberValidator)->isValid($phone) || !wa()->getUser()->isAuth()) { + return $auth_url; + } + + $this->savePhone($phone); + return $auth_url . '&auth_type=onetime_password&2fa_phone=' . urlencode($phone); + } + + private function savePhone($phone) + { + $user = wa()->getUser(); + if (!$user->isAuth()) { + return; + } + $user_phones = $user->get('phone'); + if (!in_array($phone, array_column($user_phones, 'value'))) { + $user->add('phone', $phone); + $user->save(); + } } /** diff --git a/wa-system/waid/waWebasystIDWAAuthController.class.php b/wa-system/waid/waWebasystIDWAAuthController.class.php index 0d01a90ec..572c8be81 100644 --- a/wa-system/waid/waWebasystIDWAAuthController.class.php +++ b/wa-system/waid/waWebasystIDWAAuthController.class.php @@ -493,8 +493,12 @@ private function authBackendUser($params) $_params = [ 'type' => 'backend', - 'user-agent' => wa()->getRequest()->getUserAgent() + 'user-agent' => wa()->getRequest()->getUserAgent(), + 'two_fa_mode' => ifempty($token_info['two_fa_mode'], false), ]; + if (ifempty($token_info['two_fa_time'], false)) { + $_params['two_fa_time'] = $token_info['two_fa_time']; + } $this->logAction('waid_auth', json_encode($_params)); /** diff --git a/wa-system/webasyst/lib/actions/backend/webasystBackend.actions.php b/wa-system/webasyst/lib/actions/backend/webasystBackend.actions.php index ef2dcd26e..0b8079f8a 100755 --- a/wa-system/webasyst/lib/actions/backend/webasystBackend.actions.php +++ b/wa-system/webasyst/lib/actions/backend/webasystBackend.actions.php @@ -133,6 +133,10 @@ public function dashboardAction() 'show_tutorial' => !wa()->getUser()->getSettings('webasyst', 'widget_tutorial_closed'), 'public_dashboards' => $this->getPublicDashboards(), 'no_today_activity' => $no_today_activity, + 'webasyst_id_settings_url' => $this->getWebasystIDSettingsUrl(), + 'webasyst_id_auth_banner' => $this->getWebasystIDAuthBanner(), + 'show_connection_banner' => $this->showConnectionBanner(), + 'current_domain' => $this->getCurrentDomain(), ]); } @@ -234,5 +238,9 @@ protected function getPublicDashboards() return $public_dashboards; } -} + protected function getCurrentDomain() { + $domain = wa()->getConfig()->getDomain(); + return waIdna::dec($domain); + } +} diff --git a/wa-system/webasyst/lib/actions/backend/webasystBackendHeader.action.php b/wa-system/webasyst/lib/actions/backend/webasystBackendHeader.action.php index c3b894bfd..02cc6f849 100644 --- a/wa-system/webasyst/lib/actions/backend/webasystBackendHeader.action.php +++ b/wa-system/webasyst/lib/actions/backend/webasystBackendHeader.action.php @@ -173,64 +173,4 @@ protected function getCurrentDomain() return waIdna::dec($domain); } - - protected function getWebasystIDSettingsUrl() - { - return wa()->getConfig()->getBackendUrl(true) . 'webasyst/settings/waid/'; - } - - /** - * Is installation connected to webasyst ID - * @return bool - * @throws waDbException - * @throws waException - */ - protected function isConnectedToWebasystID() - { - // client (installation) not connected - $auth = new waWebasystIDWAAuth(); - return $auth->isClientConnected(); - } - - protected function showConnectionBanner() - { - $is_connected = $this->isConnectedToWebasystID(); - if ($is_connected) { - return false; - } - - $is_closed = wa()->getUser()->getSettings('webasyst', 'webasyst_id_announcement_close'); - if ($is_closed) { - return false; - } - - return wa()->getUser()->isAdmin('webasyst'); - } - - protected function getWebasystIDAuthBanner() - { - $is_closed = wa()->getUser()->getSettings('webasyst', 'webasyst_id_announcement_close'); - if ($is_closed) { - return null; - } - - $is_connected = $this->isConnectedToWebasystID(); - if (!$is_connected) { - return null; - } - - // user is bound with webasyst contact id already - $user = $this->getUser(); - $webasyst_contact_id = $user->getWebasystContactId(); - if ($webasyst_contact_id) { - return null; - } - - $auth = new waWebasystIDWAAuth(); - $auth_url = $auth->getUrl(); - - return [ - 'url' => $auth_url - ]; - } } diff --git a/wa-system/webasyst/lib/actions/login/webasystLogin.controller.php b/wa-system/webasyst/lib/actions/login/webasystLogin.controller.php index b51e5f4e7..2aa9f861c 100644 --- a/wa-system/webasyst/lib/actions/login/webasystLogin.controller.php +++ b/wa-system/webasyst/lib/actions/login/webasystLogin.controller.php @@ -23,12 +23,15 @@ public function execute() return; } - $cm = new waWebasystIDClientManager(); - - try { - $webasyst_id_forced_auth = $cm->isBackendAuthForced() && !wa()->getRequest()->get('force_login_form'); - } catch (waException $e) { - $webasyst_id_forced_auth = false; + $webasyst_id_forced_auth = false; + // Check for Webasyst ID oauth only for non AJAX requests & not forced stamdard login form + if (!waRequest::isXMLHttpRequest() && empty(waRequest::request('wa_json_mode')) && empty(waRequest::get('force_login_form'))) { + $cm = new waWebasystIDClientManager(); + try { + $webasyst_id_forced_auth = $cm->isBackendAuthForced(); + } catch (waException $e) { + // do nothing + } } // Webasyst ID oauth not forced - standard backend auth login diff --git a/wa-system/webasyst/lib/actions/settings/waid/webasystSettingsWaID.action.php b/wa-system/webasyst/lib/actions/settings/waid/webasystSettingsWaID.action.php index 8b69a2877..b88fd7842 100644 --- a/wa-system/webasyst/lib/actions/settings/waid/webasystSettingsWaID.action.php +++ b/wa-system/webasyst/lib/actions/settings/waid/webasystSettingsWaID.action.php @@ -63,6 +63,8 @@ protected function getConnectedUsers() if (!empty($info['email'])) { $user['webasyst_id'] = $info['email']; } + $user['two_fa_mode'] = ifempty($info['two_fa_mode'], false); + $user['two_fa_time'] = intval(ifempty($info['two_fa_time'])) ? date('Y-m-d H:i:s', intval($info['two_fa_time'])) : false; } unset($user); diff --git a/wa-system/webasyst/lib/classes/webasystHeader.trait.php b/wa-system/webasyst/lib/classes/webasystHeader.trait.php index 14bb4c555..e4a6147c4 100644 --- a/wa-system/webasyst/lib/classes/webasystHeader.trait.php +++ b/wa-system/webasyst/lib/classes/webasystHeader.trait.php @@ -144,4 +144,64 @@ public function getAnnouncements(array $options = []) } return $announcements; } + + protected function getWebasystIDSettingsUrl() + { + return wa()->getConfig()->getBackendUrl(true) . 'webasyst/settings/waid/'; + } + + /** + * Is installation connected to webasyst ID + * @return bool + * @throws waDbException + * @throws waException + */ + protected function isConnectedToWebasystID() + { + // client (installation) not connected + $auth = new waWebasystIDWAAuth(); + return $auth->isClientConnected(); + } + + protected function showConnectionBanner() + { + $is_connected = $this->isConnectedToWebasystID(); + if ($is_connected) { + return false; + } + + $is_closed = wa()->getUser()->getSettings('webasyst', 'webasyst_id_announcement_close'); + if ($is_closed) { + return false; + } + + return wa()->getUser()->isAdmin('webasyst'); + } + + protected function getWebasystIDAuthBanner() + { + $is_closed = wa()->getUser()->getSettings('webasyst', 'webasyst_id_announcement_close'); + if ($is_closed) { + return null; + } + + $is_connected = $this->isConnectedToWebasystID(); + if (!$is_connected) { + return null; + } + + // user is bound with webasyst contact id already + $user = $this->getUser(); + $webasyst_contact_id = $user->getWebasystContactId(); + if ($webasyst_contact_id) { + return null; + } + + $auth_url = (new waWebasystIDWAAuth)->getUrl(); + $phone_formatted = (new waContactPhoneField('phone', ''))->format(wa()->getUser()->get('phone', 'default'), 'value'); + return [ + 'url' => $auth_url, + 'phone' => $phone_formatted, + ]; + } } diff --git a/wa-system/webasyst/lib/config/app.php b/wa-system/webasyst/lib/config/app.php index c9c79af9a..f5ec12cb0 100644 --- a/wa-system/webasyst/lib/config/app.php +++ b/wa-system/webasyst/lib/config/app.php @@ -3,8 +3,8 @@ return array( 'name' => 'Webasyst', 'prefix' => 'webasyst', - 'version' => '2.6.2', - 'critical' => '2.6.2', + 'version' => '2.7.0', + 'critical' => '2.7.0', 'vendor' => 'webasyst', 'csrf' => true, 'header_items' => array( diff --git a/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.mo b/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.mo index ede981d6f..0f8ac4f20 100644 Binary files a/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.mo and b/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.mo differ diff --git a/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.po b/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.po index 44c0043bd..a0f43dbfc 100755 --- a/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.po +++ b/wa-system/webasyst/locale/ru_RU/LC_MESSAGES/webasyst.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: WebAsyst\n" "POT-Creation-Date: \n" -"PO-Revision-Date: 2022-06-20 12:56+0300\n" +"PO-Revision-Date: 2022-10-03 15:13+0300\n" "Last-Translator: \n" "Language-Team: \n" "Language: ru_RU\n" @@ -6585,3 +6585,15 @@ msgstr "Имена не должны содержать URL-адреса." msgid "No supported network transports are available." msgstr "Отсутствуют поддерживаемые сетевые инструменты передачи данных." + +msgid "Enable secure sign-in with 2-factor authentication (2FA). Your account will be connected to Webasyst ID, and any sign-in attempts from new devices will be protected with an SMS confirmation code." +msgstr "Подключите безопасный вход с двухфакторной аутентификацией (2FA). Ваш аккаунт будет подключен к Webasyst ID, и все попытки входа с новых устройств будут защищены подтверждением по SMS." + +msgid "+1" +msgstr "+7" + +msgid "Sign-in with 2FA" +msgstr "Вход с 2FA" + +msgid "Last sign-in with 2-factor authentication" +msgstr "Последний вход с двухфакторной авторизацией" diff --git a/wa-system/webasyst/templates/actions/backend/BackendDashboard.html b/wa-system/webasyst/templates/actions/backend/BackendDashboard.html index 3c3aead46..e44c6f5ac 100644 --- a/wa-system/webasyst/templates/actions/backend/BackendDashboard.html +++ b/wa-system/webasyst/templates/actions/backend/BackendDashboard.html @@ -2,6 +2,104 @@ {* APPS *}
{include file="../dashboard/DashboardAppsBlock.html"} + {if !empty($show_connection_banner)} +
+
+
+

+ + [s`Enable sign-in with Webasyst ID, a universal authorization option that unites Webasyst Customer Center and backend sign-in on your custom domains. Webasyst ID allows you to use all Webasyst apps, sites, and services, and to avoid the use of different passwords on various domains.`]  + {sprintf('[s`Enable Webasyst ID on %s — it’s free and secure!`]', $current_domain|escape|mb_strtoupper)}  + [s`How does it work?`] +

+
+ + + +
+ +
+ {elseif !empty($webasyst_id_auth_banner)} + + {/if}
diff --git a/wa-system/webasyst/templates/actions/backend/BackendDefaultMobile.html b/wa-system/webasyst/templates/actions/backend/BackendDefaultMobile.html index dbf53297e..7127c65c7 100644 --- a/wa-system/webasyst/templates/actions/backend/BackendDefaultMobile.html +++ b/wa-system/webasyst/templates/actions/backend/BackendDefaultMobile.html @@ -104,6 +104,106 @@
+ {if !empty($show_connection_banner)} +
+
+
+

+ + [s`Enable sign-in with Webasyst ID, a universal authorization option that unites Webasyst Customer Center and backend sign-in on your custom domains. Webasyst ID allows you to use all Webasyst apps, sites, and services, and to avoid the use of different passwords on various domains.`]  + {sprintf('[s`Enable Webasyst ID on %s — it’s free and secure!`]', $current_domain|escape|mb_strtoupper)} +

+ +
+ + + +
+ +
+ {elseif !empty($webasyst_id_auth_banner)} + + {/if}
    {foreach $header_items as $app_id => $app} diff --git a/wa-system/webasyst/templates/actions/backend/BackendHeader13.html b/wa-system/webasyst/templates/actions/backend/BackendHeader13.html index cdb504e9c..4ee9e7aa7 100644 --- a/wa-system/webasyst/templates/actions/backend/BackendHeader13.html +++ b/wa-system/webasyst/templates/actions/backend/BackendHeader13.html @@ -67,10 +67,27 @@

    - + {sprintf('[s`Upgrade your user account (%s) to Webasyst ID, a unified authorization across all Webasyst apps, sites, and services. It’s free, secure, and takes just a minute.`]', $user.login|escape|strtoupper)}  - {sprintf('[s`Enable Webasyst ID for %s now!`]', $user.login|escape|strtoupper)}  + [s`Enable secure sign-in with 2-factor authentication (2FA). Your account will be connected to Webasyst ID, and any sign-in attempts from new devices will be protected with an SMS confirmation code.`] +
    + + [s`Connect`] [s`How does it work?`] +

    {/if} diff --git a/wa-system/webasyst/templates/actions/backend/BackendWebasystIDHelp.html b/wa-system/webasyst/templates/actions/backend/BackendWebasystIDHelp.html index 612d5b6de..1859505f4 100644 --- a/wa-system/webasyst/templates/actions/backend/BackendWebasystIDHelp.html +++ b/wa-system/webasyst/templates/actions/backend/BackendWebasystIDHelp.html @@ -19,35 +19,39 @@