diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -181,6 +181,7 @@ $result = [ 'skus' => $skus, + 'enableBeta' => in_array('beta', $skus), // TODO: This will change when we enable all users to create domains 'enableDomains' => $isController && $hasCustomDomain, // TODO: Make 'enableDistlists' working for wallet controllers that aren't account owners diff --git a/src/app/IP4Net.php b/src/app/IP4Net.php --- a/src/app/IP4Net.php +++ b/src/app/IP4Net.php @@ -3,7 +3,6 @@ namespace App; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\DB; class IP4Net extends Model { @@ -21,10 +20,18 @@ 'updated_at' ]; + /** + * Get IP network by IP address + * + * @param string $ip IPv4 address + * + * @return ?\App\IP4Net IPv4 network record, Null if not found + */ public static function getNet($ip) { $where = 'INET_ATON(net_number) <= INET_ATON(?) and INET_ATON(net_broadcast) >= INET_ATON(?)'; - return IP4Net::whereRaw($where, [$ip, $ip]) + + return self::whereRaw($where, [$ip, $ip]) ->orderByRaw('INET_ATON(net_number), net_mask DESC') ->first(); } diff --git a/src/app/IP6Net.php b/src/app/IP6Net.php --- a/src/app/IP6Net.php +++ b/src/app/IP6Net.php @@ -3,7 +3,6 @@ namespace App; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\DB; class IP6Net extends Model { @@ -21,9 +20,17 @@ 'updated_at' ]; + /** + * Get IP network by IP address + * + * @param string $ip IPv6 address + * + * @return ?\App\IP6Net IPv6 network record, Null if not found + */ public static function getNet($ip) { $where = 'INET6_ATON(net_number) <= INET6_ATON(?) and INET6_ATON(net_broadcast) >= INET6_ATON(?)'; + return IP6Net::whereRaw($where, [$ip, $ip]) ->orderByRaw('INET6_ATON(net_number), net_mask DESC') ->first(); diff --git a/src/app/Traits/UserConfigTrait.php b/src/app/Traits/UserConfigTrait.php --- a/src/app/Traits/UserConfigTrait.php +++ b/src/app/Traits/UserConfigTrait.php @@ -11,10 +11,11 @@ */ public function getConfig(): array { - $settings = $this->getSettings(['greylist_enabled', 'password_policy', 'max_password_age']); + $settings = $this->getSettings(['greylist_enabled', 'password_policy', 'max_password_age', 'limit_geo']); $config = [ 'greylist_enabled' => $settings['greylist_enabled'] !== 'false', + 'limit_geo' => $settings['limit_geo'] ? json_decode($settings['limit_geo'], true) : [], 'max_password_age' => $settings['max_password_age'], 'password_policy' => $settings['password_policy'], ]; @@ -36,6 +37,26 @@ foreach ($config as $key => $value) { if ($key == 'greylist_enabled') { $this->setSetting($key, $value ? 'true' : 'false'); + } elseif ($key == 'limit_geo') { + if (!is_array($value)) { + $errors[$key] = \trans('validation.invalid-limit-geo'); + continue; + } + + foreach ($value as $idx => $country) { + if (!preg_match('/^[a-zA-Z]{2}$/', $country)) { + $errors[$key] = \trans('validation.invalid-limit-geo'); + continue 2; + } + + $value[$idx] = \strtoupper($country); + } + + if (count($value) > 250) { + $errors[$key] = \trans('validation.invalid-limit-geo'); + } + + $this->setSetting($key, !empty($value) ? json_encode($value) : null); } elseif ($key == 'max_password_age') { $this->setSetting($key, intval($value) > 0 ? (int) $value : null); } elseif ($key == 'password_policy') { diff --git a/src/database/seeds/local/UserSeeder.php b/src/database/seeds/local/UserSeeder.php --- a/src/database/seeds/local/UserSeeder.php +++ b/src/database/seeds/local/UserSeeder.php @@ -132,7 +132,6 @@ 'last_name' => 'Flanders', 'currency' => 'USD', 'country' => 'US', - // 'limit_geo' => json_encode(["CH"]), 'guam_enabled' => false, ] ); diff --git a/src/package-lock.json b/src/package-lock.json --- a/src/package-lock.json +++ b/src/package-lock.json @@ -19,6 +19,7 @@ "eslint": "^7.26.0", "eslint-plugin-vue": "^7.20.0", "frappe-charts": "^1.5.8", + "html-loader": "^3.1.0", "laravel-mix": "^6.0.43", "linkify-string": "^4.0.0-beta", "mediasoup-client": "^3.6.51", @@ -5878,119 +5879,55 @@ "dev": true }, "node_modules/html-loader": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-1.3.2.tgz", - "integrity": "sha512-DEkUwSd0sijK5PF3kRWspYi56XP7bTNkyg5YWSzBdjaSDmvCufep5c4Vpb3PBf6lUL0YPtLwBfy9fL0t5hBAGA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-3.1.0.tgz", + "integrity": "sha512-ycMYFRiCF7YANcLDNP72kh3Po5pTcH+bROzdDwh00iVOAY/BwvpuZ1BKPziQ35Dk9D+UD84VGX1Lu/H4HpO4fw==", "dev": true, "dependencies": { - "html-minifier-terser": "^5.1.1", - "htmlparser2": "^4.1.0", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "html-minifier-terser": "^6.0.2", + "parse5": "^6.0.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/html-loader/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "webpack": "^5.0.0" } }, "node_modules/html-minifier-terser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, "dependencies": { - "camel-case": "^4.1.1", - "clean-css": "^4.2.3", - "commander": "^4.1.1", + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", "he": "^1.2.0", - "param-case": "^3.0.3", + "param-case": "^3.0.4", "relateurl": "^0.2.7", - "terser": "^4.6.3" + "terser": "^5.10.0" }, "bin": { "html-minifier-terser": "cli.js" }, "engines": { - "node": ">=6" - } - }, - "node_modules/html-minifier-terser/node_modules/clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 4.0" + "node": ">=12" } }, "node_modules/html-minifier-terser/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/html-minifier-terser/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 12" } }, - "node_modules/html-minifier-terser/node_modules/terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, "node_modules/html-tags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", @@ -6866,6 +6803,93 @@ "node": ">=8" } }, + "node_modules/laravel-mix/node_modules/html-loader": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-1.3.2.tgz", + "integrity": "sha512-DEkUwSd0sijK5PF3kRWspYi56XP7bTNkyg5YWSzBdjaSDmvCufep5c4Vpb3PBf6lUL0YPtLwBfy9fL0t5hBAGA==", + "dev": true, + "dependencies": { + "html-minifier-terser": "^5.1.1", + "htmlparser2": "^4.1.0", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/laravel-mix/node_modules/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/laravel-mix/node_modules/html-minifier-terser/node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/laravel-mix/node_modules/html-minifier-terser/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/laravel-mix/node_modules/html-minifier-terser/node_modules/terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/laravel-mix/node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/laravel-mix/node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -6899,6 +6923,15 @@ "node": ">=10" } }, + "node_modules/laravel-mix/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/laravel-mix/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8081,6 +8114,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, "node_modules/parseqs": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", @@ -9565,7 +9604,7 @@ "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true, "engines": { "node": ">= 0.10" @@ -11179,9 +11218,9 @@ } }, "node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "dev": true }, "node_modules/tty-browserify": { @@ -16932,84 +16971,35 @@ "dev": true }, "html-loader": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-1.3.2.tgz", - "integrity": "sha512-DEkUwSd0sijK5PF3kRWspYi56XP7bTNkyg5YWSzBdjaSDmvCufep5c4Vpb3PBf6lUL0YPtLwBfy9fL0t5hBAGA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-3.1.0.tgz", + "integrity": "sha512-ycMYFRiCF7YANcLDNP72kh3Po5pTcH+bROzdDwh00iVOAY/BwvpuZ1BKPziQ35Dk9D+UD84VGX1Lu/H4HpO4fw==", "dev": true, "requires": { - "html-minifier-terser": "^5.1.1", - "htmlparser2": "^4.1.0", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } + "html-minifier-terser": "^6.0.2", + "parse5": "^6.0.1" } }, "html-minifier-terser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, "requires": { - "camel-case": "^4.1.1", - "clean-css": "^4.2.3", - "commander": "^4.1.1", + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", "he": "^1.2.0", - "param-case": "^3.0.3", + "param-case": "^3.0.4", "relateurl": "^0.2.7", - "terser": "^4.6.3" + "terser": "^5.10.0" }, "dependencies": { - "clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - } - }, "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true - }, - "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - } - } } } }, @@ -17639,6 +17629,69 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "html-loader": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-1.3.2.tgz", + "integrity": "sha512-DEkUwSd0sijK5PF3kRWspYi56XP7bTNkyg5YWSzBdjaSDmvCufep5c4Vpb3PBf6lUL0YPtLwBfy9fL0t5hBAGA==", + "dev": true, + "requires": { + "html-minifier-terser": "^5.1.1", + "htmlparser2": "^4.1.0", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } + }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "dependencies": { + "clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + } + } + }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -17659,6 +17712,12 @@ "lru-cache": "^6.0.0" } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18566,6 +18625,12 @@ "lines-and-columns": "^1.1.6" } }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, "parseqs": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", @@ -19643,7 +19708,7 @@ "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true }, "remark": { @@ -20854,9 +20919,9 @@ "dev": true }, "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "dev": true }, "tty-browserify": { diff --git a/src/package.json b/src/package.json --- a/src/package.json +++ b/src/package.json @@ -25,6 +25,7 @@ "eslint": "^7.26.0", "eslint-plugin-vue": "^7.20.0", "frappe-charts": "^1.5.8", + "html-loader": "^3.1.0", "laravel-mix": "^6.0.43", "linkify-string": "^4.0.0-beta", "mediasoup-client": "^3.6.51", diff --git a/src/resources/images/world_map.svg b/src/resources/images/world_map.svg new file mode 100644 --- /dev/null +++ b/src/resources/images/world_map.svg @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -77,6 +77,24 @@ }, methods: { clearFormValidation, + countriesText(list) { + if (list && list.length) { + let result = [] + + list.forEach(code => { + let country = window.config.countries[code] + if (country) { + result.push(country[1]) + } else { + console.warn(`Unknown country code: ${code}`) + } + }) + + return result.join(', ') + } + + return this.$t('form.norestrictions') + }, hasPermission(type) { const key = 'enable' + type.charAt(0).toUpperCase() + type.slice(1) return !!(this.authInfo && this.authInfo.statusInfo[key]) diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -165,10 +165,12 @@ 'name' => "Name", 'months' => "months", 'none' => "none", + 'norestrictions' => "No restrictions", 'or' => "or", 'password' => "Password", 'password-confirm' => "Confirm Password", 'phone' => "Phone", + 'selectcountries' => "Select countries", 'settings' => "Settings", 'shared-folder' => "Shared Folder", 'size' => "Size", @@ -456,6 +458,8 @@ 'ext-email' => "External Email", 'email-aliases' => "Email Aliases", 'finances' => "Finances", + 'geolimit' => "Geo-lockin", + 'geolimit-text' => "Defines a list of locations that are allowed for logon. You will not be able to login from a country that is not listed here.", 'greylisting' => "Greylisting", 'greylisting-text' => "Greylisting is a method of defending users against spam. Any incoming mail from an unrecognized sender " . "is temporarily rejected. The originating server should try again after a delay. " diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php --- a/src/resources/lang/en/validation.php +++ b/src/resources/lang/en/validation.php @@ -163,6 +163,7 @@ 'nameexists' => 'The specified name is not available.', 'nameinvalid' => 'The specified name is invalid.', 'password-policy-error' => 'Specified password does not comply with the policy.', + 'invalid-limit-geo' => 'Specified configuration is invalid. Expected a list of two-letter country codes.', 'invalid-password-policy' => 'Specified password policy is invalid.', 'password-policy-min-len-error' => 'Minimum password length cannot be less than :min.', 'password-policy-max-len-error' => 'Maximum password length cannot be more than :max.', diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss --- a/src/resources/themes/app.scss +++ b/src/resources/themes/app.scss @@ -286,6 +286,24 @@ .modal-body { overflow: auto !important; } + + &.fullscreen { + .modal-dialog { + height: 100%; + width: 100%; + max-width: calc(100vw - 1.5rem); + } + + .modal-content { + height: 100%; + max-height: 100% !important; + } + + .modal-body { + padding: 0; + margin: 1em; + } + } } #status-box { diff --git a/src/resources/themes/forms.scss b/src/resources/themes/forms.scss --- a/src/resources/themes/forms.scss +++ b/src/resources/themes/forms.scss @@ -198,3 +198,34 @@ color: $main-color; } } + +.world-map { + height: 100%; + text-align: center; + + svg { + height: 100%; + + .bg { + fill: $gray-600; + } + + [cc] { + fill: white; + stroke: $gray-600; + stroke-width: 0.25px; + cursor: pointer; + } + + [cc]:focus, [cc]:hover { + fill: lighten($main-color, 20%); + stroke: $main-color; + stroke-width: 0.5px; + } + + [cc][aria-selected="true"] { + fill: $main-color; + stroke: $main-color; + } + } +} diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue --- a/src/resources/vue/Admin/User.vue +++ b/src/resources/vue/Admin/User.vue @@ -205,6 +205,14 @@ +
+ +
+ + {{ $root.countriesText(user.config.limit_geo) }} + +
+
diff --git a/src/resources/vue/Logout.vue b/src/resources/vue/Logout.vue --- a/src/resources/vue/Logout.vue +++ b/src/resources/vue/Logout.vue @@ -7,6 +7,7 @@ .then(response => { this.$toast.success(response.data.message) }) + .finally(() => {}) this.$root.logoutUser() } diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue --- a/src/resources/vue/User/Info.vue +++ b/src/resources/vue/User/Info.vue @@ -93,6 +93,18 @@ +
+ +
+ + + {{ $t('user.geolimit-text') }} + +
+
{{ $t('btn.submit') }} @@ -109,6 +121,7 @@ diff --git a/src/resources/vue/Widgets/ModalDialog.vue b/src/resources/vue/Widgets/ModalDialog.vue --- a/src/resources/vue/Widgets/ModalDialog.vue +++ b/src/resources/vue/Widgets/ModalDialog.vue @@ -42,6 +42,11 @@ label: 'btn.submit', icon: 'check' }, + save: { + className: 'btn-primary modal-action', + label: 'btn.save', + icon: 'check' + } } export default { @@ -65,7 +70,7 @@ }, methods: { btnProperty(button, property) { - const isString = typeof button == 'string' + const isString = typeof button == 'string' if (!isString && property in button) { return button[property] } diff --git a/src/tests/Browser.php b/src/tests/Browser.php --- a/src/tests/Browser.php +++ b/src/tests/Browser.php @@ -173,6 +173,15 @@ return $this->waitFor($selector . ':not([disabled])')->click($selector); } + /** + * Execute javascript code ignoring it's result + */ + public function execScript($script) + { + $this->script($script); + return $this; + } + /** * Check if in Phone mode */ diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php --- a/src/tests/Browser/Admin/UserTest.php +++ b/src/tests/Browser/Admin/UserTest.php @@ -86,6 +86,8 @@ { $this->browse(function (Browser $browser) { $jack = $this->getTestUser('jack@kolab.org'); + $jack->setSetting('limit_geo', null); + $page = new UserPage($jack->id); $browser->visit(new Home()) @@ -189,9 +191,22 @@ $browser->assertSeeIn('@nav #tab-settings', 'Settings') ->click('@nav #tab-settings') ->whenAvailable('@user-settings form', function (Browser $browser) { - $browser->assertElementsCount('.row', 1) + $browser->assertElementsCount('.row', 2) ->assertSeeIn('.row:first-child label', 'Greylisting') - ->assertSeeIn('.row:first-child .text-success', 'enabled'); + ->assertSeeIn('.row:first-child .text-success', 'enabled') + ->assertSeeIn('.row:nth-child(2) label', 'Geo-lockin') + ->assertSeeIn('.row:nth-child(2) #limit_geo', 'No restrictions'); + }); + + $jack->setSetting('limit_geo', '["PL","DE"]'); + + // Assert Settings tab + $browser->refresh() + ->on($page) + ->click('@nav #tab-settings') + ->whenAvailable('@user-settings form', function (Browser $browser) { + $browser->assertSeeIn('.row:nth-child(2) label', 'Geo-lockin') + ->assertSeeIn('.row:nth-child(2) #limit_geo', 'Poland, Germany'); }); }); } @@ -460,7 +475,7 @@ $browser->assertSeeIn('@nav #tab-settings', 'Settings') ->click('@nav #tab-settings') ->whenAvailable('@user-settings form', function (Browser $browser) { - $browser->assertElementsCount('.row', 1) + $browser->assertElementsCount('.row', 2) ->assertSeeIn('.row:first-child label', 'Greylisting') ->assertSeeIn('.row:first-child .text-danger', 'disabled'); }); diff --git a/src/tests/Browser/Components/CountrySelect.php b/src/tests/Browser/Components/CountrySelect.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/Components/CountrySelect.php @@ -0,0 +1,118 @@ +selector = $selector; + $this->countries = include resource_path('countries.php'); + } + + /** + * Get the root selector for the component. + * + * @return string + */ + public function selector() + { + return $this->selector; + } + + /** + * Assert that the browser page contains the component. + * + * @param \Laravel\Dusk\Browser $browser + * + * @return void + */ + public function assert($browser) + { + $browser->assertVisible($this->selector) + ->assertMissing("{$this->selector} @dialog"); + } + + /** + * Get the element shortcuts for the component. + * + * @return array + */ + public function elements() + { + return [ + '@link' => 'a', + '@dialog' => '.modal', + ]; + } + + /** + * Assert selected countries on the map and in the link content + */ + public function assertCountries($browser, array $list) + { + if (empty($list)) { + $browser->assertSeeIn('@link', 'No restrictions') + ->click('@link') + ->with(new Dialog('@dialog'), function ($browser) { + $browser->assertVisible('.world-map') + ->assertElementsCount('.world-map [aria-selected="true"]', 0) + ->click('@button-cancel'); + }); + + return; + } + + $browser->assertSeeIn('@link', $this->countriesText($list)) + ->click('@link') + ->with(new Dialog('@dialog'), function ($browser) use ($list) { + $browser->assertVisible('.world-map') + ->assertElementsCount('.world-map [aria-selected="true"]', count($list)); + + foreach ($list as $code) { + $code = strtolower($code); + $browser->assertVisible('.world-map [cc="' . $code . '"]'); + } + + $browser->click('@button-cancel'); + }); + } + + /** + * Update selected countries + */ + public function setCountries($browser, array $list) + { + $browser->click('@link') + ->with(new Dialog('@dialog'), function ($browser) use ($list) { + $browser->execScript("\$('.world-map [cc]').attr('aria-selected', '')"); + + foreach ($list as $code) { + $code = strtolower($code); + $browser->click('.world-map [cc="' . $code . '"]'); + } + + $browser->click('@button-action'); + }); + } + + /** + * Get textual list of country names + */ + protected function countriesText(array $list): string + { + $names = []; + foreach ($list as $code) { + $names[] = $this->countries[$code][1]; + } + + return implode(', ', $names); + } +} diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php --- a/src/tests/Browser/UsersTest.php +++ b/src/tests/Browser/UsersTest.php @@ -8,6 +8,7 @@ use App\User; use App\UserAlias; use Tests\Browser; +use Tests\Browser\Components\CountrySelect; use Tests\Browser\Components\Dialog; use Tests\Browser\Components\ListInput; use Tests\Browser\Components\QuotaInput; @@ -376,6 +377,7 @@ { $john = $this->getTestUser('john@kolab.org'); $john->setSetting('greylist_enabled', null); + $john->setSetting('limit_geo', null); $this->browse(function (Browser $browser) use ($john) { $browser->visit('/user/' . $john->id) @@ -386,13 +388,45 @@ ->click('@nav #tab-settings') ->with('#settings form', function (Browser $browser) { $browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting') + ->assertMissing('div.row:nth-child(2)') // geo-lockin setting is hidden ->click('div.row:nth-child(1) input[type=checkbox]:checked') ->click('button[type=submit]') ->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.'); }); }); - $this->assertSame('false', $john->fresh()->getSetting('greylist_enabled')); + $this->assertSame('false', $john->getSetting('greylist_enabled')); + + $this->addBetaEntitlement($john); + + $this->browse(function (Browser $browser) use ($john) { + $browser->refresh() + ->on(new UserInfo()) + ->click('@nav #tab-settings') + ->with('#settings form', function (Browser $browser) use ($john) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting') + ->assertSeeIn('div.row:nth-child(2) label', 'Geo-lockin') + ->with(new CountrySelect('#limit_geo'), function ($browser) { + $browser->assertCountries([]) + ->setCountries(['PL', 'DE']) + ->assertCountries(['PL', 'DE']); + }) + ->click('button[type=submit]') + ->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.'); + + $this->assertSame('["PL","DE"]', $john->getSetting('limit_geo')); + + $browser + ->with(new CountrySelect('#limit_geo'), function ($browser) { + $browser->setCountries([]) + ->assertCountries([]); + }) + ->click('button[type=submit]') + ->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.'); + + $this->assertSame(null, $john->getSetting('limit_geo')); + }); + }); } /** diff --git a/src/tests/Feature/Controller/MeetTest.php b/src/tests/Feature/Controller/MeetTest.php --- a/src/tests/Feature/Controller/MeetTest.php +++ b/src/tests/Feature/Controller/MeetTest.php @@ -20,6 +20,9 @@ $room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]); } + /** + * {@inheritDoc} + */ public function tearDown(): void { $this->clearMeetEntitlements(); diff --git a/src/tests/Feature/Controller/NGINXTest.php b/src/tests/Feature/Controller/NGINXTest.php --- a/src/tests/Feature/Controller/NGINXTest.php +++ b/src/tests/Feature/Controller/NGINXTest.php @@ -16,27 +16,29 @@ $john = $this->getTestUser('john@kolab.org'); \App\CompanionApp::where('user_id', $john->id)->delete(); \App\AuthAttempt::where('user_id', $john->id)->delete(); - $john->setSettings( - [ - // 'limit_geo' => json_encode(["CH"]), + $john->setSettings([ + 'limit_geo' => null, 'guam_enabled' => false, - ] - ); + ]); + \App\IP4Net::where('net_number', '127.0.0.0')->delete(); + $this->useServicesUrl(); } + /** + * {@inheritDoc} + */ public function tearDown(): void { - $john = $this->getTestUser('john@kolab.org'); \App\CompanionApp::where('user_id', $john->id)->delete(); \App\AuthAttempt::where('user_id', $john->id)->delete(); - $john->setSettings( - [ - // 'limit_geo' => json_encode(["CH"]), + $john->setSettings([ + 'limit_geo' => null, 'guam_enabled' => false, - ] - ); + ]); + \App\IP4Net::where('net_number', '127.0.0.0')->delete(); + parent::tearDown(); } @@ -129,11 +131,7 @@ // Guam - $john->setSettings( - [ - 'guam_enabled' => true, - ] - ); + $john->setSettings(['guam_enabled' => true]); $response = $this->withHeaders($headers)->get("api/webhooks/nginx"); $response->assertStatus(200); @@ -171,6 +169,37 @@ $response = $this->withHeaders($headers)->get("api/webhooks/nginx"); $response->assertStatus(200); $response->assertHeader('auth-status', 'OK'); + + + // Geo-lockin (failure) + $john->setSettings(['limit_geo' => '["PL","US"]']); + + $headers['Auth-Protocol'] = 'imap'; + $headers['Client-Ip'] = '127.0.0.1'; + + $response = $this->withHeaders($headers)->get("api/webhooks/nginx"); + $response->assertStatus(200); + $response->assertHeader('auth-status', 'authentication failure'); + + $authAttempt = \App\AuthAttempt::where('ip', $headers['Client-Ip'])->where('user_id', $john->id)->first(); + $this->assertSame('geolocation', $authAttempt->reason); + \App\AuthAttempt::where('user_id', $john->id)->delete(); + + // Geo-lockin (success) + \App\IP4Net::create([ + 'net_number' => '127.0.0.0', + 'net_broadcast' => '127.255.255.255', + 'net_mask' => 8, + 'country' => 'US', + 'rir_name' => 'test', + 'serial' => 1, + ]); + + $response = $this->withHeaders($headers)->get("api/webhooks/nginx"); + $response->assertStatus(200); + $response->assertHeader('auth-status', 'OK'); + + $this->assertCount(0, \App\AuthAttempt::where('user_id', $john->id)->get()); } /** diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -464,6 +464,7 @@ $john->setSetting('greylist_enabled', null); $john->setSetting('password_policy', null); $john->setSetting('max_password_age', null); + $john->setSetting('limit_geo', null); // greylist_enabled $this->assertSame(true, $john->getConfig()['greylist_enabled']); @@ -530,6 +531,31 @@ $this->assertSame([], $result); $this->assertSame('min:10,max:255', $john->getConfig()['password_policy']); $this->assertSame('min:10,max:255', $john->getSetting('password_policy')); + + // limit_geo + $this->assertSame([], $john->getConfig()['limit_geo']); + + $result = $john->setConfig(['limit_geo' => '']); + + $err = "Specified configuration is invalid. Expected a list of two-letter country codes."; + $this->assertSame(['limit_geo' => $err], $result); + $this->assertSame(null, $john->getSetting('limit_geo')); + + $result = $john->setConfig(['limit_geo' => ['usa']]); + + $this->assertSame(['limit_geo' => $err], $result); + $this->assertSame(null, $john->getSetting('limit_geo')); + + $result = $john->setConfig(['limit_geo' => []]); + + $this->assertSame([], $result); + $this->assertSame(null, $john->getSetting('limit_geo')); + + $result = $john->setConfig(['limit_geo' => ['US', 'ru']]); + + $this->assertSame([], $result); + $this->assertSame(['US', 'RU'], $john->getConfig()['limit_geo']); + $this->assertSame('["US","RU"]', $john->getSetting('limit_geo')); } /** diff --git a/src/webpack.mix.js b/src/webpack.mix.js --- a/src/webpack.mix.js +++ b/src/webpack.mix.js @@ -22,15 +22,37 @@ } }) -mix.js('resources/js/user/app.js', 'public/js/user.js') - .js('resources/js/admin/app.js', 'public/js/admin.js') - .js('resources/js/reseller/app.js', 'public/js/reseller.js') - .vue() +// Make Laravel Mix ignore .svgs +Mix.listen('configReady', function (config) { + for (let rule of config.module.rules) { + if (/\.svg/.test(rule.test.toString())) { + rule.exclude = /\.svg$/ + break + } + } +}) + +// Hande .svgs with html-loader instead +mix.webpackConfig({ + module: { + rules: [{ + test: /\.svg$/, + use: [{ loader: 'html-loader', options: { minimize: true } }] + }] + } +}) mix.before(() => { spawn('php', ['resources/build/before.php'], { stdio: 'inherit' }) }) +// Compile the Vue/js resources +mix.js('resources/js/user/app.js', 'public/js/user.js') + .js('resources/js/admin/app.js', 'public/js/admin.js') + .js('resources/js/reseller/app.js', 'public/js/reseller.js') + .vue() + +// Compile the themes/css resources glob.sync('resources/themes/*/', {}).forEach(fromDir => { const toDir = fromDir.replace('resources/themes/', 'public/themes/')