Merge branch 'license-refactor'

This commit is contained in:
Jan Prochazka 2024-08-14 13:12:54 +02:00
commit ddf385caac
14 changed files with 364 additions and 19 deletions

View File

@ -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"
},

View File

@ -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,

12
app/src/proTools.js Normal file
View File

@ -0,0 +1,12 @@
function isProApp() {
return false;
}
function checkLicense(license) {
return null;
}
module.exports = {
isProApp,
checkLicense,
};

View File

@ -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"

View File

@ -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',

View File

@ -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;
},
};

View File

@ -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,
};

View File

@ -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',

View File

@ -0,0 +1,114 @@
<script lang="ts">
import { onMount } from 'svelte';
import { useConfig } from './utility/metadataLoaders';
import ErrorInfo from './elements/ErrorInfo.svelte';
import Link from './elements/Link.svelte';
import { internalRedirectTo } from './clientAuth';
import TextAreaField from './forms/TextAreaField.svelte';
import { writable } from 'svelte/store';
import FormProviderCore from './forms/FormProviderCore.svelte';
import FormTextAreaField from './forms/FormTextAreaField.svelte';
import FormSubmit from './forms/FormSubmit.svelte';
import { apiCall } from './utility/api';
const config = useConfig();
const values = writable({ amoid: null, databaseServer: null });
const params = new URLSearchParams(location.search);
const error = params.get('error');
onMount(() => {
const removed = document.getElementById('starting_dbgate_zero');
if (removed) removed.remove();
});
</script>
<FormProviderCore {values}>
<div class="root theme-light theme-type-light">
<div class="text">DbGate</div>
<div class="wrap">
<div class="logo">
<img class="img" src="logo192.png" />
</div>
<div class="box">
<div class="heading">License</div>
<FormTextAreaField label="License key" name="licenseKey" rows={5} />
<div class="submit">
<FormSubmit
value="Save license"
on:click={async e => {
const { licenseKey } = e.detail;
await apiCall('config/save-license-key', { licenseKey });
internalRedirectTo('/');
}}
/>
</div>
</div>
</div>
</div>
</FormProviderCore>
<style>
.logo {
display: flex;
margin-bottom: 1rem;
align-items: center;
justify-content: center;
}
.img {
width: 80px;
}
.text {
position: fixed;
top: 1rem;
left: 1rem;
font-size: 30pt;
font-family: monospace;
color: var(--theme-bg-2);
text-transform: uppercase;
}
.root {
color: var(--theme-font-1);
display: flex;
justify-content: center;
background-color: var(--theme-bg-1);
align-items: baseline;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.box {
width: 600px;
max-width: 80vw;
/* max-width: 600px;
width: 40vw; */
border: 1px solid var(--theme-border);
border-radius: 4px;
background-color: var(--theme-bg-0);
}
.wrap {
margin-top: 20vh;
}
.heading {
text-align: center;
margin: 1em;
font-size: xx-large;
}
.submit {
margin: var(--dim-large-form-margin);
display: flex;
}
.submit :global(input) {
flex: 1;
font-size: larger;
}
</style>

View File

@ -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;

View File

@ -5,6 +5,7 @@
export let name;
export let defaultValue = undefined;
export let saveOnInput = false;
export let onChange = null;
const { values, setFieldValue } = getFormContext();
</script>
@ -17,5 +18,8 @@
if (saveOnInput) {
setFieldValue(name, e.target['value']);
}
if (onChange) {
onChange(e.target['value']);
}
}}
/>

View File

@ -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,

View File

@ -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;
});
}
</script>
<SettingsFormProvider>
@ -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
<svelte:fragment slot="6">
<div class="heading">Other</div>
<FormTextField
name="other.gistCreateToken"
label="API token for creating error gists"
defaultValue=""
<FormTextField name="other.gistCreateToken" label="API token for creating error gists" defaultValue="" />
</svelte:fragment>
<svelte:fragment slot="7">
<div class="heading">License</div>
<FormTextAreaField
name="other.licenseKey"
label="License key"
rows={7}
onChange={async value => {
licenseKeyCheckResult = await apiCall('config/check-license', { licenseKey: value });
}}
/>
{#if licenseKeyCheckResult}
<div class="m-3 ml-5">
{#if licenseKeyCheckResult.status == 'ok'}
<div>
<FontIcon icon="img ok" /> License key is valid
</div>
<div>
License valid to: {licenseKeyCheckResult.validTo}
</div>
<div>License key expiration: {licenseKeyCheckResult.expiration}</div>
{:else if licenseKeyCheckResult.status == 'error'}
<FontIcon icon="img error" /> License key is invalid
{/if}
</div>
{/if}
</svelte:fragment>
</TabControl>
</FormValues>

View File

@ -0,0 +1,3 @@
export function isProApp() {
return false;
}