diff --git a/app/package.json b/app/package.json index 7a732767..0477168d 100644 --- a/app/package.json +++ b/app/package.json @@ -7,6 +7,7 @@ "dependencies": { "electron-log": "^4.4.1", "electron-updater": "^4.6.1", + "jsonwebtoken": "^9.0.2", "lodash.clonedeepwith": "^4.5.0", "patch-package": "^6.4.7" }, diff --git a/app/src/electron.js b/app/src/electron.js index 9f51f771..4b658863 100644 --- a/app/src/electron.js +++ b/app/src/electron.js @@ -16,7 +16,7 @@ const BrowserWindow = electron.BrowserWindow; const path = require('path'); const url = require('url'); const mainMenuDefinition = require('./mainMenuDefinition'); -const { settings } = require('cluster'); +const { isProApp, checkLicense } = require('./proTools'); let disableAutoUpgrade = false; // require('@electron/remote/main').initialize(); @@ -299,9 +299,11 @@ function ensureBoundsVisible(bounds) { } function createWindow() { + const datadir = path.join(os.homedir(), '.dbgate'); + let settingsJson = {}; + let licenseKey = null; try { - const datadir = path.join(os.homedir(), '.dbgate'); settingsJson = fillMissingSettings( JSON.parse(fs.readFileSync(path.join(datadir, 'settings.json'), { encoding: 'utf-8' })) ); @@ -309,12 +311,22 @@ function createWindow() { console.log('Error loading settings.json:', err.message); settingsJson = fillMissingSettings({}); } + if (isProApp()) { + try { + licenseKey = fs.readFileSync(path.join(datadir, 'license.key'), { encoding: 'utf-8' }); + } catch (err) { + console.log('Error loading license.key:', err.message); + licenseKey = null; + } + } + + const licenseOk = !isProApp() || checkLicense(licenseKey) == 'premium'; let bounds = initialConfig['winBounds']; if (bounds) { bounds = ensureBoundsVisible(bounds); } - useNativeMenu = settingsJson['app.useNativeMenu']; + useNativeMenu = settingsJson['app.useNativeMenu'] || !licenseOk; mainWindow = new BrowserWindow({ width: 1200, diff --git a/app/src/proTools.js b/app/src/proTools.js new file mode 100644 index 00000000..8f884bd6 --- /dev/null +++ b/app/src/proTools.js @@ -0,0 +1,12 @@ +function isProApp() { + return false; +} + +function checkLicense(license) { + return null; +} + +module.exports = { + isProApp, + checkLicense, +}; diff --git a/app/yarn.lock b/app/yarn.lock index acc10fc8..6b649ffa 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -476,6 +476,11 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-equal@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" @@ -927,6 +932,13 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ejs@^3.1.7: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" @@ -1663,6 +1675,39 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonwebtoken@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -1718,11 +1763,46 @@ lodash.escaperegexp@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash@^4.17.15: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -1860,6 +1940,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + msnodesqlv8@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/msnodesqlv8/-/msnodesqlv8-4.2.1.tgz#59f2930e7f3b9b201d7288425a6ffa923ea1a573" @@ -2322,6 +2407,11 @@ semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.1.tgz#60bfe090bf907a25aa8119a72b9f90ef7ca281b2" integrity sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA== +semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index fb456542..c7845a9f 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -30,6 +30,7 @@ function authMiddleware(req, res, next) { '/config/get', '/config/logout', '/config/get-settings', + '/config/save-license-key', '/auth/oauth-token', '/auth/login', '/auth/redirect', diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 9456b56a..26cd5845 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -12,6 +12,8 @@ const currentVersion = require('../currentVersion'); const platformInfo = require('../utility/platformInfo'); const connections = require('../controllers/connections'); const { getAuthProviderFromReq } = require('../auth/authProvider'); +const { checkLicense, checkLicenseKey } = require('../utility/checkLicense'); +const { storageWriteConfig } = require('./storageDb'); const lock = new AsyncLock(); @@ -45,6 +47,9 @@ module.exports = { 'Basic authentization is not allowed, when using storage. Cannot use both STORAGE_DATABASE and BASIC_AUTH'; } + const checkedLicense = await checkLicense(); + const isLicenseValid = checkedLicense?.status == 'ok'; + return { runAsPortal: !!connections.portalConnections, singleDbConnection: connections.singleDbConnection, @@ -55,8 +60,8 @@ module.exports = { allowShellScripting: platformInfo.allowShellScripting, isDocker: platformInfo.isDocker, isElectron: platformInfo.isElectron, - isLicenseValid: platformInfo.isLicenseValid, - checkedLicense: platformInfo.checkedLicense, + isLicenseValid, + checkedLicense, configurationError, logoutUrl: await authProvider.getLogoutUrl(), permissions, @@ -119,12 +124,38 @@ module.exports = { async loadSettings() { try { const settingsText = await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }); - return this.fillMissingSettings(JSON.parse(settingsText)); + return { + ...this.fillMissingSettings(JSON.parse(settingsText)), + 'other.licenseKey': platformInfo.isElectron ? await this.loadLicenseKey() : undefined, + }; } catch (err) { return this.fillMissingSettings({}); } }, + async loadLicenseKey() { + try { + const licenseKey = await fs.readFile(path.join(datadir(), 'license.key'), { encoding: 'utf-8' }); + return licenseKey; + } catch (err) { + return null; + } + }, + + saveLicenseKey_meta: true, + async saveLicenseKey({ licenseKey }) { + try { + if (process.env.STORAGE_DATABASE) { + await storageWriteConfig('license', { licenseKey }); + } else { + await fs.writeFile(path.join(datadir(), 'license.key'), licenseKey); + } + socket.emitChanged(`config-changed`); + } catch (err) { + return null; + } + }, + updateSettings_meta: true, async updateSettings(values, req) { if (!hasPermission(`settings/change`, req)) return false; @@ -134,10 +165,16 @@ module.exports = { try { const updated = { ...currentValue, - ...values, + ..._.omit(values, ['other.licenseKey']), }; await fs.writeFile(path.join(datadir(), 'settings.json'), JSON.stringify(updated, undefined, 2)); // this.settingsValue = updated; + + if (currentValue['other.licenseKey'] != values['other.licenseKey']) { + await this.saveLicenseKey({ licenseKey: values['other.licenseKey'] }); + socket.emitChanged(`config-changed`); + } + socket.emitChanged(`settings-changed`); return updated; } catch (err) { @@ -152,4 +189,10 @@ module.exports = { const resp = await axios.default.get('https://raw.githubusercontent.com/dbgate/dbgate/master/CHANGELOG.md'); return resp.data; }, + + checkLicense_meta: true, + async checkLicense({ licenseKey }) { + const resp = await checkLicenseKey(licenseKey); + return resp; + }, }; diff --git a/packages/api/src/utility/checkLicense.js b/packages/api/src/utility/checkLicense.js index b43b3c1f..f3534477 100644 --- a/packages/api/src/utility/checkLicense.js +++ b/packages/api/src/utility/checkLicense.js @@ -1,4 +1,11 @@ -function checkLicense() { +function checkLicenseWeb() { + return { + status: 'ok', + type: 'community', + }; +} + +function checkLicenseApp() { return { status: 'ok', type: 'community', @@ -6,5 +13,6 @@ function checkLicense() { } module.exports = { - checkLicense, + checkLicenseWeb, + checkLicenseApp, }; diff --git a/packages/api/src/utility/platformInfo.js b/packages/api/src/utility/platformInfo.js index 9464b77a..d6af1323 100644 --- a/packages/api/src/utility/platformInfo.js +++ b/packages/api/src/utility/platformInfo.js @@ -3,7 +3,6 @@ const os = require('os'); const path = require('path'); const processArgs = require('./processArgs'); const isElectron = require('is-electron'); -const { checkLicense } = require('./checkLicense'); const platform = process.env.OS_OVERRIDE ? process.env.OS_OVERRIDE : process.platform; const isWindows = platform === 'win32'; @@ -13,7 +12,6 @@ const isDocker = fs.existsSync('/home/dbgate-docker/public'); const isDevMode = process.env.DEVMODE == '1'; const isNpmDist = !!global['IS_NPM_DIST']; const isForkedApi = processArgs.isForkedApi; -const checkedLicense = checkLicense(); // function moduleAvailable(name) { // try { @@ -32,8 +30,6 @@ const platformInfo = { isElectronBundle: isElectron() && !isDevMode, isForkedApi, isElectron: isElectron(), - checkedLicense, - isLicenseValid: checkedLicense?.status == 'ok', isDevMode, isNpmDist, isSnap: process.env.ELECTRON_SNAP == 'true', diff --git a/packages/web/src/EnterLicensePage.svelte b/packages/web/src/EnterLicensePage.svelte new file mode 100644 index 00000000..56a19a80 --- /dev/null +++ b/packages/web/src/EnterLicensePage.svelte @@ -0,0 +1,114 @@ + + + +
+
DbGate
+
+ +
+
License
+ + +
+ { + const { licenseKey } = e.detail; + await apiCall('config/save-license-key', { licenseKey }); + internalRedirectTo('/'); + }} + /> +
+
+
+
+
+ + diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index a5fffd7f..cd20f37d 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -2,6 +2,7 @@ import { ca } from 'date-fns/locale'; import { apiCall, enableApi, getAuthCategory } from './utility/api'; import { getConfig } from './utility/metadataLoaders'; import { isAdminPage } from './utility/pageDefs'; +import getElectron from './utility/getElectron'; export function isOauthCallback() { const params = new URLSearchParams(location.search); @@ -117,11 +118,19 @@ export function handleOauthCallback() { } export async function handleAuthOnStartup(config, isAdminPage = false) { - if (!config.isLicenseValid || config.configurationError) { + if (config.configurationError) { internalRedirectTo(`/?page=error`); return; } + if (!config.isLicenseValid) { + if (config.storageDatabase || getElectron()) { + internalRedirectTo(`/?page=license`); + } else { + internalRedirectTo(`/?page=error`); + } + } + if (getAuthCategory(config) == 'admin') { if (localStorage.getItem('adminAccessToken')) { return; diff --git a/packages/web/src/forms/FormTextAreaFieldRaw.svelte b/packages/web/src/forms/FormTextAreaFieldRaw.svelte index 041c8ced..dc624a05 100644 --- a/packages/web/src/forms/FormTextAreaFieldRaw.svelte +++ b/packages/web/src/forms/FormTextAreaFieldRaw.svelte @@ -5,6 +5,7 @@ export let name; export let defaultValue = undefined; export let saveOnInput = false; + export let onChange = null; const { values, setFieldValue } = getFormContext(); @@ -17,5 +18,8 @@ if (saveOnInput) { setFieldValue(name, e.target['value']); } + if (onChange) { + onChange(e.target['value']); + } }} /> diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts index a134b123..4d2e78ec 100644 --- a/packages/web/src/main.ts +++ b/packages/web/src/main.ts @@ -7,6 +7,7 @@ import { handleOauthCallback } from './clientAuth'; import LoginPage from './LoginPage.svelte'; import NotLoggedPage from './NotLoggedPage.svelte'; import ErrorPage from './ErrorPage.svelte'; +import EnterLicensePage from './EnterLicensePage.svelte'; const params = new URLSearchParams(location.search); const page = params.get('page'); @@ -15,7 +16,6 @@ const isOauthCallback = handleOauthCallback(); localStorageGarbageCollector(); - function createApp() { if (isOauthCallback) { return null; @@ -34,6 +34,11 @@ function createApp() { target: document.body, props: {}, }); + case 'license': + return new EnterLicensePage({ + target: document.body, + props: {}, + }); case 'admin-login': return new LoginPage({ target: document.body, diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index 31843583..5f384e00 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -31,9 +31,15 @@ import { isMac } from '../utility/common'; import getElectron from '../utility/getElectron'; import ThemeSkeleton from './ThemeSkeleton.svelte'; + import { isProApp } from '../utility/proTools'; + import FormTextAreaField from '../forms/FormTextAreaField.svelte'; + import { apiCall } from '../utility/api'; + import { useSettings } from '../utility/metadataLoaders'; + import { derived } from 'svelte/store'; const electron = getElectron(); let restartWarning = false; + let licenseKeyCheckResult = null; export let selectedTab = 0; @@ -58,6 +64,23 @@ ORDER BY $selectedWidget = 'plugins'; $visibleWidgetSideBar = true; } + + const settings = useSettings(); + const settingsValues = derived(settings, $settings => { + if (!$settings) { + return {}; + } + return $settings; + }); + + $: licenseKey = $settingsValues['other.licenseKey']; + let checkedLicenseKey = false; + $: if (licenseKey && !checkedLicenseKey) { + checkedLicenseKey = true; + apiCall('config/check-license', { licenseKey }).then(result => { + licenseKeyCheckResult = result; + }); + } @@ -70,6 +93,7 @@ ORDER BY isInline tabs={[ { label: 'General', slot: 1 }, + isProApp() && electron && { label: 'License', slot: 7 }, { label: 'Connection', slot: 2 }, { label: 'Themes', slot: 3 }, { label: 'Default Actions', slot: 4 }, @@ -317,11 +341,34 @@ ORDER BY
Other
- +
+ + +
License
+ { + licenseKeyCheckResult = await apiCall('config/check-license', { licenseKey: value }); + }} /> + {#if licenseKeyCheckResult} +
+ {#if licenseKeyCheckResult.status == 'ok'} +
+ License key is valid +
+
+ License valid to: {licenseKeyCheckResult.validTo} +
+
License key expiration: {licenseKeyCheckResult.expiration}
+ {:else if licenseKeyCheckResult.status == 'error'} + License key is invalid + {/if} +
+ {/if}
diff --git a/packages/web/src/utility/proTools.ts b/packages/web/src/utility/proTools.ts new file mode 100644 index 00000000..abde44fd --- /dev/null +++ b/packages/web/src/utility/proTools.ts @@ -0,0 +1,3 @@ +export function isProApp() { + return false; +}