123-lang 2.1.11 → 2.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ const languages = ['en', 'ru', 'uk'];
2
+ const messages = {
3
+ en: {
4
+ 'Update is available': 'Update is available',
5
+ 'Now application will start updating and restart.': 'Now application will start updating and restart.',
6
+ Ok: 'Ok',
7
+ Later: 'Later',
8
+ 'There was a problem updating the application': 'There was a problem updating the application',
9
+ Cancel: 'Cancel',
10
+ 'Download manual': 'Download manual',
11
+ 'Download installer': 'Download installer',
12
+ },
13
+ ru: {
14
+ 'Update is available': 'Доступно обновление',
15
+ 'Now application will start updating and restart.': 'Сейчас будет произведено обновление и перезапуск приложения.',
16
+ Ok: 'Ок',
17
+ Later: 'Позже',
18
+ 'There was a problem updating the application': 'Возникла проблема с обновлением приложения',
19
+ Cancel: 'Закрыть',
20
+ 'Download manual': 'Скачать инструкцию',
21
+ 'Download installer': 'Скачать установщик',
22
+ },
23
+ uk: {
24
+ 'Update is available': 'Доступне оновлення',
25
+ 'Now application will start updating and restart.': 'Зараз буде виконано оновлення та перезавантаження додатку.',
26
+ Ok: 'Ок',
27
+ Later: 'Пiзнiше',
28
+ 'There was a problem updating the application': 'Під час оновлення програми виникла проблема',
29
+ Cancel: 'Закрити',
30
+ 'Download manual': 'Завантажити інструкцію',
31
+ 'Download installer': 'Завантажити інсталятор',
32
+ },
33
+ };
34
+
35
+ const t = (translation = '', lang = 'en') => messages[lang][translation];
36
+
37
+ module.exports = {
38
+ languages,
39
+ messages,
40
+ t,
41
+ };
@@ -0,0 +1,248 @@
1
+ const fs = require('fs');
2
+ const fsPromises = require('fs/promises');
3
+ const path = require('path');
4
+ const crypto = require('crypto');
5
+ const { app, nativeImage, shell } = require('electron');
6
+ const log = require('electron-log');
7
+ const { localStorage } = require('electron-browser-storage');
8
+ const os = require('os');
9
+
10
+ const { whiteLabelUpdaterConfiguration, config } = require('../configuration');
11
+
12
+ class WhiteLabelUpdater {
13
+ constructor() {
14
+ this.cachePath = path.join(
15
+ app.getPath(whiteLabelUpdaterConfiguration?.systemDir),
16
+ whiteLabelUpdaterConfiguration?.cacheDirName
17
+ );
18
+ this.ensureDir();
19
+ }
20
+
21
+ ensureDir() {
22
+ if (!fs.existsSync(this.cachePath)) {
23
+ fs.mkdirSync(this.cachePath, { recursive: true });
24
+ }
25
+ }
26
+
27
+ getFilePath(serverUrl) {
28
+ const hash = crypto.createHash('md5').update(serverUrl).digest('hex');
29
+ return path.join(
30
+ this.cachePath,
31
+ `${hash}.${whiteLabelUpdaterConfiguration?.fileExtension?.json}`
32
+ );
33
+ }
34
+
35
+ async safeWrite(filePath, data) {
36
+ const tempPath = `${filePath}.${whiteLabelUpdaterConfiguration?.fileExtension?.tmp}`;
37
+ await fsPromises.writeFile(tempPath, JSON.stringify(data), 'utf8');
38
+ await fsPromises.rename(tempPath, filePath);
39
+ }
40
+
41
+ async fetchWithTimeout(url) {
42
+ const controller = new AbortController();
43
+ const id = setTimeout(
44
+ () => controller.abort(),
45
+ whiteLabelUpdaterConfiguration?.timeOutMs
46
+ );
47
+ try {
48
+ const response = await fetch(url, { signal: controller.signal });
49
+ clearTimeout(id);
50
+ return response;
51
+ } catch (error) {
52
+ clearTimeout(id);
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ async getWhiteLabelData(serverUrl) {
58
+ if (!serverUrl) return whiteLabelUpdaterConfiguration?.defaultResult;
59
+
60
+ const normalizedUrl = serverUrl.replace(/\/$/, '');
61
+ const cacheFilePath = this.getFilePath(normalizedUrl);
62
+
63
+ let localCache = null;
64
+ try {
65
+ const raw = await fsPromises.readFile(cacheFilePath, 'utf8');
66
+ localCache = JSON.parse(raw);
67
+ } catch (e) {
68
+ log.error('[WL] There is something wrong with the cache');
69
+ }
70
+ let remoteConfig = null;
71
+ try {
72
+ const res = await this.fetchWithTimeout(
73
+ `${normalizedUrl}${whiteLabelUpdaterConfiguration?.internalAssetsDir}${whiteLabelUpdaterConfiguration?.configFileName}`
74
+ );
75
+ if (res.ok) {
76
+ remoteConfig = await res.json();
77
+ }
78
+ } catch (e) {
79
+ log.error('[WL] Server unavailable, using cache fallback.');
80
+ }
81
+
82
+ if (!remoteConfig) {
83
+ return localCache
84
+ ? localCache.data
85
+ : whiteLabelUpdaterConfiguration?.defaultResult;
86
+ }
87
+
88
+ if (
89
+ localCache &&
90
+ localCache.data.version === remoteConfig.version &&
91
+ localCache.data.assets
92
+ ) {
93
+ log.info('[WL] Version match. Serving from cache.');
94
+
95
+ return { ...remoteConfig, assets: localCache.data.assets };
96
+ }
97
+
98
+ log.info('[WL] New version detected or no cache. Downloading assets...');
99
+
100
+ if (remoteConfig.enabled === false) {
101
+ const cleanData = {
102
+ ...whiteLabelUpdaterConfiguration?.defaultResult,
103
+ ...remoteConfig,
104
+ assets: { ...whiteLabelUpdaterConfiguration?.defaultResult?.assets },
105
+ };
106
+ await this.safeWrite(cacheFilePath, {
107
+ timestamp: Date.now(),
108
+ data: cleanData,
109
+ });
110
+ return cleanData;
111
+ }
112
+ const assets = { ...whiteLabelUpdaterConfiguration?.defaultResult?.assets };
113
+
114
+ await Promise.all(
115
+ whiteLabelUpdaterConfiguration?.assetFiles?.map(async file => {
116
+ try {
117
+ const res = await this.fetchWithTimeout(
118
+ `${normalizedUrl}${whiteLabelUpdaterConfiguration?.internalAssetsDir}${file}`
119
+ );
120
+ if (res.ok) {
121
+ const buf = await res.arrayBuffer();
122
+ const buffer = Buffer.from(buf);
123
+ const ext = path.extname(file).toLowerCase().replace('.', '');
124
+
125
+ let mime;
126
+ switch (ext) {
127
+ case 'svg':
128
+ mime = 'image/svg+xml';
129
+ break;
130
+ case 'ico':
131
+ mime = 'image/x-icon';
132
+ break;
133
+ case 'png':
134
+ mime = 'image/png';
135
+ break;
136
+ case 'jpg':
137
+ case 'jpeg':
138
+ mime = 'image/jpeg';
139
+ break;
140
+ default:
141
+ mime = `image/${ext}`;
142
+ }
143
+
144
+ // Ensure the base64 string is clean
145
+ const b64 = buffer.toString('base64');
146
+ assets[file] = `data:${mime};base64,${b64}`;
147
+ }
148
+ } catch (err) {
149
+ log.error(`[WL] Failed to download asset: ${file}`);
150
+ }
151
+ })
152
+ );
153
+
154
+ const freshData = {
155
+ ...whiteLabelUpdaterConfiguration?.defaultResult,
156
+ ...remoteConfig,
157
+ assets: assets,
158
+ };
159
+
160
+ await this.safeWrite(cacheFilePath, {
161
+ timestamp: Date.now(),
162
+ data: freshData,
163
+ });
164
+
165
+ return freshData;
166
+ }
167
+
168
+ async sendWhiteLabelDataForWeb(whiteLabelData) {
169
+ try {
170
+ await localStorage.setItem(
171
+ whiteLabelUpdaterConfiguration?.localStorageConfigName,
172
+ JSON.stringify(whiteLabelData)
173
+ );
174
+ log.info('[WL] Info successful send to web localstorage');
175
+ } catch (e) {
176
+ log.error('[WL] Error of saving to web localstorage', e);
177
+ }
178
+ }
179
+
180
+ createAppIcon(iconBase64String) {
181
+ try {
182
+ if (iconBase64String) {
183
+ const image = nativeImage.createFromDataURL(iconBase64String);
184
+ return image.resize(whiteLabelUpdaterConfiguration?.imageSize);
185
+ }
186
+ } catch (e) {
187
+ log.error('[WL] Unable to generate app icon', e);
188
+ }
189
+ return null;
190
+ }
191
+
192
+ updateShortcutWithBase64(base64String) {
193
+ const cleanBase64 = base64String.replace(/^data:.*?;base64,/, '');
194
+ const userDataPath = app.getPath(whiteLabelUpdaterConfiguration?.systemDir);
195
+ const uniqueIconName = `${
196
+ whiteLabelUpdaterConfiguration?.tempIconPrefix
197
+ }${Date.now()}.${whiteLabelUpdaterConfiguration?.fileExtension?.ico}`;
198
+ const iconPath = path.join(userDataPath, uniqueIconName);
199
+
200
+ try {
201
+ const files = fs.readdirSync(userDataPath);
202
+ files.forEach(file => {
203
+ if (
204
+ file.startsWith(whiteLabelUpdaterConfiguration?.tempIconPrefix) &&
205
+ file.endsWith(
206
+ `.${whiteLabelUpdaterConfiguration?.fileExtension?.ico}`
207
+ )
208
+ ) {
209
+ fs.unlinkSync(path.join(userDataPath, file));
210
+ }
211
+ });
212
+
213
+ fs.writeFileSync(
214
+ iconPath,
215
+ cleanBase64,
216
+ whiteLabelUpdaterConfiguration?.fileExtension?.base64
217
+ );
218
+
219
+ const desktopPath = path.join(
220
+ os.homedir(),
221
+ whiteLabelUpdaterConfiguration?.desktopDir,
222
+ `${config?.mainWindow?.title}.${whiteLabelUpdaterConfiguration?.fileExtension?.lnk}`
223
+ );
224
+
225
+ const success = shell.writeShortcutLink(desktopPath, 'replace', {
226
+ target: process.execPath,
227
+ icon: iconPath,
228
+ iconIndex: 0,
229
+ cwd: path.dirname(process.execPath),
230
+ });
231
+
232
+ if (success) {
233
+ const now = new Date();
234
+ fs.utimesSync(desktopPath, now, now);
235
+
236
+ log.info(
237
+ `[WL] Shortcut successfully replaced and redrawn with: ${uniqueIconName}`
238
+ );
239
+ } else {
240
+ log.info('[WL] Failed to replace the shortcut.');
241
+ }
242
+ } catch (e) {
243
+ log.error('[WL] Failed to update shortcut:', e);
244
+ }
245
+ }
246
+ }
247
+
248
+ module.exports = new WhiteLabelUpdater();
@@ -0,0 +1,336 @@
1
+ const path = require('path');
2
+
3
+ const {
4
+ app,
5
+ BrowserWindow,
6
+ shell,
7
+ dialog,
8
+ session,
9
+ globalShortcut,
10
+ } = require('electron');
11
+ const isDev = require('electron-is-dev');
12
+ // Auto updater
13
+ const fs = require('fs');
14
+ const { localStorage } = require('electron-browser-storage');
15
+ const { t } = require('./electron-configuration/messages');
16
+ const {
17
+ config,
18
+ platforms,
19
+ platformsNames,
20
+ } = require('./electron-configuration/configuration');
21
+ const { autoUpdater } = require('electron-updater');
22
+ const log = require('electron-log');
23
+ const forge = require('node-forge');
24
+ const os = require('os');
25
+
26
+ const getLang = async () => localStorage.getItem(config.storageNames.lang);
27
+
28
+ const { isEnableDevTools, isAvailableDomainInput, certificate } = config;
29
+
30
+ const rootCertificatePath = path.join(
31
+ __dirname,
32
+ certificate.dir,
33
+ certificate.fileName,
34
+ );
35
+ const rootCertificatePathSecondary = path.join(
36
+ __dirname,
37
+ certificate.dir,
38
+ certificate.fileNameSecondary,
39
+ );
40
+
41
+ const currentPlatform = platformsNames[os.platform()];
42
+ let updatesPath;
43
+
44
+ log.info(`Current Platform: ${currentPlatform}`);
45
+
46
+ // Disable downloading and installation updates for the MAC
47
+ autoUpdater.autoDownload = currentPlatform !== platforms.MAC;
48
+ autoUpdater.autoInstallOnAppQuit = currentPlatform !== platforms.MAC;
49
+
50
+ // Enable usage of Portal's globalShortcuts. This is essential for cases when
51
+ // the app runs in a Wayland session.
52
+ app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
53
+
54
+ const serverUrl = 'http://localhost:3100/';
55
+
56
+ async function createWindow() {
57
+ let appIcon = null;
58
+ if (config.isEnableWhiteLabelUpdater && !!config.isEnableWhiteLabelUpdater) {
59
+ // White Labels
60
+ const whiteLabelUpdater = require('./electron-configuration/modules/WhiteLabelUpdater.js');
61
+
62
+ const wlConfig = await whiteLabelUpdater.getWhiteLabelData(serverUrl);
63
+ appIcon = whiteLabelUpdater.createAppIcon(wlConfig?.assets['icon.png']);
64
+
65
+ await whiteLabelUpdater.sendWhiteLabelDataForWeb(wlConfig);
66
+
67
+ if (process.platform === 'win32' && wlConfig?.assets['icon.ico']) {
68
+ whiteLabelUpdater?.updateShortcutWithBase64(wlConfig?.assets['icon.ico']);
69
+ }
70
+
71
+ try {
72
+ if (process.platform === 'darwin') {
73
+ app.dock.setIcon(appIcon);
74
+ log.info('[WL][MAC] Icon successfully changed');
75
+ }
76
+ } catch (e) {
77
+ log.error('[WL][MAC] Icon error', e);
78
+ }
79
+ }
80
+
81
+ // Create the browser window.
82
+ const win = new BrowserWindow({
83
+ ...config.mainWindow,
84
+ icon: appIcon,
85
+ webPreferences: {
86
+ devTools: true,
87
+ nodeIntegration: true,
88
+ },
89
+ });
90
+
91
+ win.webContents.on('new-window', function (e, url) {
92
+ e.preventDefault();
93
+ shell.openExternal(url);
94
+ });
95
+ // Set Links Settings
96
+
97
+ win.webContents.setWindowOpenHandler(details => {
98
+ shell.openExternal(details.url); // Open URL in user's Browser
99
+ return { action: 'deny' };
100
+ });
101
+
102
+ if (!isEnableDevTools) {
103
+ win.removeMenu();
104
+ }
105
+
106
+ // Register a config?.shortCuts?.devTools shortcut listener.
107
+ const ret = globalShortcut.register(config?.shortCuts?.devTools, () => {
108
+ log.info(`${config?.shortCuts?.devTools} is pressed`);
109
+ if (win) {
110
+ const isOpen = win.webContents.isDevToolsOpened();
111
+ if (isOpen) {
112
+ win.webContents.closeDevTools();
113
+ } else {
114
+ win.webContents.openDevTools({ mode: 'detach' });
115
+ }
116
+ }
117
+ });
118
+
119
+ // and load the index.html of the app.
120
+ // win.loadFile("index.html");
121
+ win.loadURL(
122
+ isDev
123
+ ? 'http://localhost:3000'
124
+ : `file://${path.join(__dirname, '../build/index.html')}`,
125
+ );
126
+
127
+ // Open the DevTools.
128
+ if (isEnableDevTools) {
129
+ win.webContents.openDevTools({ mode: 'detach' });
130
+ }
131
+
132
+ // Check up for updates
133
+ // Verify certificate for the autoUpdater
134
+ autoUpdater.netSession.setCertificateVerifyProc((request, callback) => {
135
+ const certStatuses = {
136
+ trusted: 0,
137
+ failed: -2,
138
+ browserChecked: -3,
139
+ };
140
+ const { hostname: hostName, verificationResult: e, certificate } = request;
141
+ let subjectName = certificate?.subjectName?.split('\n')[0];
142
+ subjectName = subjectName?.replace('*.', '');
143
+ const status = hostName?.includes(subjectName)
144
+ ? certStatuses.trusted
145
+ : certStatuses.failed;
146
+ if (status === certStatuses.failed) {
147
+ log.error('Certificate validation error:');
148
+ log.error(e);
149
+ }
150
+ callback(status);
151
+ });
152
+
153
+ const generateUpdateConfig = async () => {
154
+ let status = true;
155
+ const domainURL = await localStorage.getItem(config.storageNames.domain);
156
+ // Test local server
157
+ // const domainURL = `http://localhost:3100/`;
158
+ if (!domainURL) {
159
+ status = false;
160
+ return status;
161
+ }
162
+ const platformPath =
163
+ config?.updatesPathForPlatform[currentPlatform] ??
164
+ config?.updatesPathForPlatform[platforms.WINDOWS];
165
+
166
+ updatesPath = `${domainURL}${platformPath}`;
167
+
168
+ try {
169
+ log.info('domain: ', domainURL);
170
+ log.info('updates path: ', updatesPath);
171
+ autoUpdater.setFeedURL({
172
+ url: updatesPath,
173
+ provider: config.provider,
174
+ });
175
+ } catch (err) {
176
+ log.error(err);
177
+ status = false;
178
+ }
179
+ return status;
180
+ };
181
+
182
+ if (app.isPackaged) {
183
+ if (isAvailableDomainInput) {
184
+ generateUpdateConfig()
185
+ .then(status => {
186
+ if (status) {
187
+ autoUpdater.logger = log;
188
+ autoUpdater.checkForUpdates();
189
+ }
190
+ })
191
+ .catch(e => log.error(e));
192
+ return;
193
+ }
194
+ autoUpdater.logger = log;
195
+ autoUpdater.checkForUpdates();
196
+ }
197
+ }
198
+
199
+ // This method will be called when Electron has finished
200
+ // initialization and is ready to create browser windows.
201
+ // Some APIs can only be used after this event occurs.
202
+ app.whenReady().then(() => {
203
+ createWindow();
204
+
205
+ // MAC OS FIX: Re-create a window in the app when the dock icon is clicked
206
+ app.on('activate', () => {
207
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
208
+ });
209
+ });
210
+
211
+ // Quit when all windows are closed, except on macOS. There, it's common
212
+ // for applications and their menu bar to stay active until the user quits
213
+ // explicitly with Cmd + Q.
214
+ app.on('window-all-closed', () => {
215
+ if (process.platform !== 'darwin') {
216
+ app.quit();
217
+ }
218
+ });
219
+
220
+ // Auto updater events
221
+ // Check is auto update available
222
+ autoUpdater.on('update-available', info => {
223
+ log.info('Update available:', info);
224
+ log.info('Updates path:', updatesPath);
225
+
226
+ const showInfoDialog = (lang = 'en') => {
227
+ log.info('showInfoDialog lang', lang);
228
+ const options = {
229
+ type: 'info',
230
+ buttons: [
231
+ t('Download installer', lang),
232
+ t('Download manual', lang),
233
+ t('Cancel', lang),
234
+ ],
235
+ defaultId: 2,
236
+ title: t('Update is available', lang),
237
+ message: t('Update is available', lang),
238
+ detail: '',
239
+ };
240
+ dialog.showMessageBox(options).then(response => {
241
+ if (response.response === 0) {
242
+ shell.openExternal(
243
+ `${updatesPath}${config?.macUpdateConfig?.installerName}`,
244
+ );
245
+ showInfoDialog(lang);
246
+ } else if (response.response === 1) {
247
+ shell.openExternal(
248
+ `${updatesPath}${config?.macUpdateConfig?.manualMap[lang]}`,
249
+ );
250
+ showInfoDialog(lang);
251
+ }
252
+ });
253
+ };
254
+ if (currentPlatform === platforms.MAC) {
255
+ getLang()
256
+ .then(lang => {
257
+ showInfoDialog(lang);
258
+ })
259
+ .catch(() => {
260
+ log.error('Unable get language');
261
+ showInfoDialog();
262
+ });
263
+ }
264
+ });
265
+ autoUpdater.on('update-downloaded', (_event, releaseNotes, releaseName) => {
266
+ const showTranslatedDialog = lang => {
267
+ const dialogOpts = {
268
+ type: 'info',
269
+ buttons: [t('Ok', lang), t('Later', lang)],
270
+ title: t('Update is available', lang),
271
+ message: process.platform === 'win32' ? releaseNotes : releaseName,
272
+ detail: t('Now application will start updating and restart.', lang),
273
+ };
274
+ dialog.showMessageBox(dialogOpts).then(returnValue => {
275
+ if (returnValue.response === 0) autoUpdater.quitAndInstall();
276
+ });
277
+ };
278
+ getLang()
279
+ .then(lang => {
280
+ showTranslatedDialog(lang);
281
+ })
282
+ .catch(() => {
283
+ log.error('Unable get language');
284
+ showTranslatedDialog();
285
+ });
286
+ });
287
+
288
+ autoUpdater.on('error', message => {
289
+ log.error(t('There was a problem updating the application'));
290
+ log.error(message);
291
+ });
292
+
293
+ app.on(
294
+ 'certificate-error',
295
+ (event, webContents, url, error, certificate, callback) => {
296
+ try {
297
+ const rootCertificateContent = fs.readFileSync(
298
+ rootCertificatePath,
299
+ 'utf8',
300
+ );
301
+ const rootCertificateContentSecondary = fs.readFileSync(
302
+ rootCertificatePathSecondary,
303
+ 'utf8',
304
+ );
305
+ const rootCert = forge.pki.certificateFromPem(rootCertificateContent);
306
+ const rootCertSecondary = forge.pki.certificateFromPem(
307
+ rootCertificateContentSecondary,
308
+ );
309
+ const clientCert = forge.pki.certificateFromPem(
310
+ certificate.data.toString(),
311
+ );
312
+ callback(
313
+ clientCert.isIssuer(rootCert) || clientCert.isIssuer(rootCertSecondary),
314
+ );
315
+ } catch (e) {
316
+ log.error('Certificate validation error:');
317
+ log.error(e);
318
+ callback(false);
319
+ }
320
+ },
321
+ );
322
+
323
+ // Clear Cache
324
+ app.on('ready', () => {
325
+ session.defaultSession.clearCache().then(() => {
326
+ log.info('Cache cleared!');
327
+ });
328
+ });
329
+
330
+ app.on('will-quit', () => {
331
+ // Unregister a shortcut.
332
+ globalShortcut.unregister(config?.shortCuts?.devTools);
333
+
334
+ // Unregister all shortcuts.
335
+ globalShortcut.unregisterAll();
336
+ });