diff options
author | Willy Woitas <dutscher_sbf@hotmail.com> | 2024-02-18 01:49:42 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-18 06:19:42 +0530 |
commit | 315728415b2269981a04ee51af7ef18412d7bf70 (patch) | |
tree | 1ee3e41e7022fa376b99bff5413eb6119481b839 | |
parent | 6.7.1-nightly.20 [skip ci] (diff) | |
download | ferdium-app-315728415b2269981a04ee51af7ef18412d7bf70.tar.gz ferdium-app-315728415b2269981a04ee51af7ef18412d7bf70.tar.zst ferdium-app-315728415b2269981a04ee51af7ef18412d7bf70.zip |
feat: Parse 2FA SMS token and copy to clipboard (#1561)
-rw-r--r-- | src/@types/stores.types.ts | 2 | ||||
-rw-r--r-- | src/components/settings/settings/EditSettingsForm.tsx | 24 | ||||
-rw-r--r-- | src/config.ts | 2 | ||||
-rw-r--r-- | src/containers/settings/EditSettingsScreen.tsx | 37 | ||||
-rw-r--r-- | src/i18n/locales/en-US.json | 2 | ||||
-rw-r--r-- | src/stores/ServicesStore.ts | 52 | ||||
-rw-r--r-- | src/webview/notifications.ts | 4 |
7 files changed, 117 insertions, 6 deletions
diff --git a/src/@types/stores.types.ts b/src/@types/stores.types.ts index c0670ee87..c2d7bd78a 100644 --- a/src/@types/stores.types.ts +++ b/src/@types/stores.types.ts | |||
@@ -87,6 +87,8 @@ export interface AppStore extends TypedStore { | |||
87 | authRequestFailed: () => void; | 87 | authRequestFailed: () => void; |
88 | autoLaunchOnStart: () => void; | 88 | autoLaunchOnStart: () => void; |
89 | automaticUpdates: boolean; | 89 | automaticUpdates: boolean; |
90 | isTwoFactorAutoCatcherEnabled: boolean; | ||
91 | twoFactorAutoCatcherMatcher: string; | ||
90 | clearAppCacheRequest: () => void; | 92 | clearAppCacheRequest: () => void; |
91 | clipboardNotifications: boolean; | 93 | clipboardNotifications: boolean; |
92 | darkMode: boolean; | 94 | darkMode: boolean; |
diff --git a/src/components/settings/settings/EditSettingsForm.tsx b/src/components/settings/settings/EditSettingsForm.tsx index 6a79fcc8f..81cfe8b12 100644 --- a/src/components/settings/settings/EditSettingsForm.tsx +++ b/src/components/settings/settings/EditSettingsForm.tsx | |||
@@ -303,6 +303,8 @@ interface IProps extends WrappedComponentProps { | |||
303 | isClearingAllCache: boolean; | 303 | isClearingAllCache: boolean; |
304 | isTodosActivated: boolean; | 304 | isTodosActivated: boolean; |
305 | automaticUpdates: boolean; | 305 | automaticUpdates: boolean; |
306 | isTwoFactorAutoCatcherEnabled: boolean; | ||
307 | twoFactorAutoCatcherMatcher: string; | ||
306 | isDarkmodeEnabled: boolean; | 308 | isDarkmodeEnabled: boolean; |
307 | isAdaptableDarkModeEnabled: boolean; | 309 | isAdaptableDarkModeEnabled: boolean; |
308 | isUseGrayscaleServicesEnabled: boolean; | 310 | isUseGrayscaleServicesEnabled: boolean; |
@@ -354,7 +356,8 @@ class EditSettingsForm extends Component<IProps, IState> { | |||
354 | this.props.form.submit({ | 356 | this.props.form.submit({ |
355 | onSuccess: (form: Form) => { | 357 | onSuccess: (form: Form) => { |
356 | const values = form.values(); | 358 | const values = form.values(); |
357 | const { accentColor } = values; | 359 | const { accentColor, isTwoFactorAutoCatcherEnabled } = values; |
360 | |||
358 | if (accentColor.trim().length === 0) { | 361 | if (accentColor.trim().length === 0) { |
359 | values.accentColor = DEFAULT_ACCENT_COLOR; | 362 | values.accentColor = DEFAULT_ACCENT_COLOR; |
360 | } | 363 | } |
@@ -362,6 +365,15 @@ class EditSettingsForm extends Component<IProps, IState> { | |||
362 | if (progressbarAccentColor.trim().length === 0) { | 365 | if (progressbarAccentColor.trim().length === 0) { |
363 | values.progressbarAccentColor = DEFAULT_ACCENT_COLOR; | 366 | values.progressbarAccentColor = DEFAULT_ACCENT_COLOR; |
364 | } | 367 | } |
368 | |||
369 | // set twoFactorAutoCatcherMatcher to the default value, if its get enabled the input is prefilled | ||
370 | if ( | ||
371 | !isTwoFactorAutoCatcherEnabled && | ||
372 | values.twoFactorAutoCatcherMatcher.length === 0 | ||
373 | ) { | ||
374 | values.twoFactorAutoCatcherMatcher = | ||
375 | DEFAULT_APP_SETTINGS.twoFactorAutoCatcherMatcher; | ||
376 | } | ||
365 | this.props.onSubmit(values); | 377 | this.props.onSubmit(values); |
366 | }, | 378 | }, |
367 | onError: noop, | 379 | onError: noop, |
@@ -386,6 +398,7 @@ class EditSettingsForm extends Component<IProps, IState> { | |||
386 | onClearAllCache, | 398 | onClearAllCache, |
387 | getCacheSize, | 399 | getCacheSize, |
388 | automaticUpdates, | 400 | automaticUpdates, |
401 | isTwoFactorAutoCatcherEnabled, | ||
389 | isDarkmodeEnabled, | 402 | isDarkmodeEnabled, |
390 | isSplitModeEnabled, | 403 | isSplitModeEnabled, |
391 | openProcessManager, | 404 | openProcessManager, |
@@ -837,6 +850,15 @@ class EditSettingsForm extends Component<IProps, IState> { | |||
837 | <Toggle {...form.$('notifyTaskBarOnMessage').bind()} /> | 850 | <Toggle {...form.$('notifyTaskBarOnMessage').bind()} /> |
838 | )} | 851 | )} |
839 | 852 | ||
853 | <Toggle {...form.$('isTwoFactorAutoCatcherEnabled').bind()} /> | ||
854 | |||
855 | {isTwoFactorAutoCatcherEnabled && ( | ||
856 | <Input | ||
857 | onChange={e => this.submit(e)} | ||
858 | {...form.$('twoFactorAutoCatcherMatcher').bind()} | ||
859 | /> | ||
860 | )} | ||
861 | |||
840 | <Hr /> | 862 | <Hr /> |
841 | 863 | ||
842 | <Select field={form.$('webRTCIPHandlingPolicy')} /> | 864 | <Select field={form.$('webRTCIPHandlingPolicy')} /> |
diff --git a/src/config.ts b/src/config.ts index 055fc9ad1..5b9a5eaba 100644 --- a/src/config.ts +++ b/src/config.ts | |||
@@ -360,6 +360,8 @@ export const DEFAULT_APP_SETTINGS = { | |||
360 | clipboardNotifications: true, | 360 | clipboardNotifications: true, |
361 | notifyTaskBarOnMessage: false, | 361 | notifyTaskBarOnMessage: false, |
362 | showDisabledServices: true, | 362 | showDisabledServices: true, |
363 | isTwoFactorAutoCatcherEnabled: false, | ||
364 | twoFactorAutoCatcherMatcher: 'token, code, sms, verify', | ||
363 | showServiceName: false, | 365 | showServiceName: false, |
364 | showMessageBadgeWhenMuted: true, | 366 | showMessageBadgeWhenMuted: true, |
365 | showDragArea: false, | 367 | showDragArea: false, |
diff --git a/src/containers/settings/EditSettingsScreen.tsx b/src/containers/settings/EditSettingsScreen.tsx index 272297700..5d1b09296 100644 --- a/src/containers/settings/EditSettingsScreen.tsx +++ b/src/containers/settings/EditSettingsScreen.tsx | |||
@@ -99,6 +99,16 @@ const messages = defineMessages({ | |||
99 | id: 'settings.app.form.notifyTaskBarOnMessage', | 99 | id: 'settings.app.form.notifyTaskBarOnMessage', |
100 | defaultMessage: 'Notify TaskBar/Dock on new message', | 100 | defaultMessage: 'Notify TaskBar/Dock on new message', |
101 | }, | 101 | }, |
102 | isTwoFactorAutoCatcherEnabled: { | ||
103 | id: 'settings.app.form.isTwoFactorAutoCatcherEnabled', | ||
104 | defaultMessage: | ||
105 | 'Auto-catch two-factor codes from notifications (Ex.: android messages) and copy to clipboard', | ||
106 | }, | ||
107 | twoFactorAutoCatcherMatcher: { | ||
108 | id: 'settings.app.form.twoFactorAutoCatcherMatcher', | ||
109 | defaultMessage: | ||
110 | 'Comma-separated and case-insensitive words/expressions to catch two-factor codes from. Ex.: token, code, sms, verify', | ||
111 | }, | ||
102 | navigationBarBehaviour: { | 112 | navigationBarBehaviour: { |
103 | id: 'settings.app.form.navigationBarBehaviour', | 113 | id: 'settings.app.form.navigationBarBehaviour', |
104 | defaultMessage: 'Navigation bar behaviour', | 114 | defaultMessage: 'Navigation bar behaviour', |
@@ -383,6 +393,10 @@ class EditSettingsScreen extends Component< | |||
383 | privateNotifications: Boolean(settingsData.privateNotifications), | 393 | privateNotifications: Boolean(settingsData.privateNotifications), |
384 | clipboardNotifications: Boolean(settingsData.clipboardNotifications), | 394 | clipboardNotifications: Boolean(settingsData.clipboardNotifications), |
385 | notifyTaskBarOnMessage: Boolean(settingsData.notifyTaskBarOnMessage), | 395 | notifyTaskBarOnMessage: Boolean(settingsData.notifyTaskBarOnMessage), |
396 | isTwoFactorAutoCatcherEnabled: Boolean( | ||
397 | settingsData.isTwoFactorAutoCatcherEnabled, | ||
398 | ), | ||
399 | twoFactorAutoCatcherMatcher: settingsData.twoFactorAutoCatcherMatcher, | ||
386 | navigationBarBehaviour: settingsData.navigationBarBehaviour, | 400 | navigationBarBehaviour: settingsData.navigationBarBehaviour, |
387 | webRTCIPHandlingPolicy: settingsData.webRTCIPHandlingPolicy, | 401 | webRTCIPHandlingPolicy: settingsData.webRTCIPHandlingPolicy, |
388 | searchEngine: settingsData.searchEngine, | 402 | searchEngine: settingsData.searchEngine, |
@@ -680,6 +694,23 @@ class EditSettingsScreen extends Component< | |||
680 | default: DEFAULT_APP_SETTINGS.notifyTaskBarOnMessage, | 694 | default: DEFAULT_APP_SETTINGS.notifyTaskBarOnMessage, |
681 | type: 'checkbox', | 695 | type: 'checkbox', |
682 | }, | 696 | }, |
697 | isTwoFactorAutoCatcherEnabled: { | ||
698 | label: intl.formatMessage(messages.isTwoFactorAutoCatcherEnabled), | ||
699 | value: ifUndefined<boolean>( | ||
700 | settings.all.app.isTwoFactorAutoCatcherEnabled, | ||
701 | DEFAULT_APP_SETTINGS.isTwoFactorAutoCatcherEnabled, | ||
702 | ), | ||
703 | default: DEFAULT_APP_SETTINGS.isTwoFactorAutoCatcherEnabled, | ||
704 | type: 'checkbox', | ||
705 | }, | ||
706 | twoFactorAutoCatcherMatcher: { | ||
707 | label: intl.formatMessage(messages.twoFactorAutoCatcherMatcher), | ||
708 | value: ifUndefined<string>( | ||
709 | settings.all.app.twoFactorAutoCatcherMatcher, | ||
710 | DEFAULT_APP_SETTINGS.twoFactorAutoCatcherMatcher, | ||
711 | ), | ||
712 | default: DEFAULT_APP_SETTINGS.twoFactorAutoCatcherMatcher, | ||
713 | }, | ||
683 | navigationBarBehaviour: { | 714 | navigationBarBehaviour: { |
684 | label: intl.formatMessage(messages.navigationBarBehaviour), | 715 | label: intl.formatMessage(messages.navigationBarBehaviour), |
685 | value: ifUndefined<string>( | 716 | value: ifUndefined<string>( |
@@ -1257,6 +1288,12 @@ class EditSettingsScreen extends Component< | |||
1257 | this.props.stores.settings.app.useGrayscaleServices | 1288 | this.props.stores.settings.app.useGrayscaleServices |
1258 | } | 1289 | } |
1259 | isSplitModeEnabled={this.props.stores.settings.app.splitMode} | 1290 | isSplitModeEnabled={this.props.stores.settings.app.splitMode} |
1291 | isTwoFactorAutoCatcherEnabled={ | ||
1292 | this.props.stores.settings.app.isTwoFactorAutoCatcherEnabled | ||
1293 | } | ||
1294 | twoFactorAutoCatcherMatcher={ | ||
1295 | this.props.stores.settings.app.twoFactorAutoCatcherMatcher | ||
1296 | } | ||
1260 | isTodosActivated={this.props.stores.todos.isFeatureEnabledByUser} | 1297 | isTodosActivated={this.props.stores.todos.isFeatureEnabledByUser} |
1261 | openProcessManager={() => this.openProcessManager()} | 1298 | openProcessManager={() => this.openProcessManager()} |
1262 | isOnline={app.isOnline} | 1299 | isOnline={app.isOnline} |
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 2781b67b7..90a45e83c 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -231,6 +231,7 @@ | |||
231 | "settings.app.form.hideWorkspacesButton": "Hide Workspace Drawer button", | 231 | "settings.app.form.hideWorkspacesButton": "Hide Workspace Drawer button", |
232 | "settings.app.form.iconSize": "Service icon size", | 232 | "settings.app.form.iconSize": "Service icon size", |
233 | "settings.app.form.inactivityLock": "Lock after inactivity", | 233 | "settings.app.form.inactivityLock": "Lock after inactivity", |
234 | "settings.app.form.isTwoFactorAutoCatcherEnabled": "Auto-catch two-factor codes from notifications (Ex.: android messages) and copy to clipboard", | ||
234 | "settings.app.form.keepAllWorkspacesLoaded": "Keep all workspaces loaded", | 235 | "settings.app.form.keepAllWorkspacesLoaded": "Keep all workspaces loaded", |
235 | "settings.app.form.language": "Language", | 236 | "settings.app.form.language": "Language", |
236 | "settings.app.form.lockPassword": "Password", | 237 | "settings.app.form.lockPassword": "Password", |
@@ -260,6 +261,7 @@ | |||
260 | "settings.app.form.startMinimized": "Start minimized", | 261 | "settings.app.form.startMinimized": "Start minimized", |
261 | "settings.app.form.translatorEngine": "Translator Engine", | 262 | "settings.app.form.translatorEngine": "Translator Engine", |
262 | "settings.app.form.translatorLanguage": "Default Translator language", | 263 | "settings.app.form.translatorLanguage": "Default Translator language", |
264 | "settings.app.form.twoFactorAutoCatcherMatcher": "Comma-separated and case-insensitive words/expressions to catch two-factor codes from. Ex.: token, code, sms, verify", | ||
263 | "settings.app.form.universalDarkMode": "Enable universal Dark Mode", | 265 | "settings.app.form.universalDarkMode": "Enable universal Dark Mode", |
264 | "settings.app.form.useGrayscaleServices": "Use grayscale services", | 266 | "settings.app.form.useGrayscaleServices": "Use grayscale services", |
265 | "settings.app.form.useHorizontalStyle": "Use horizontal style", | 267 | "settings.app.form.useHorizontalStyle": "Use horizontal style", |
diff --git a/src/stores/ServicesStore.ts b/src/stores/ServicesStore.ts index 175a2ce16..d7804a3fe 100644 --- a/src/stores/ServicesStore.ts +++ b/src/stores/ServicesStore.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'node:path'; | 1 | import { join } from 'node:path'; |
2 | import { ipcRenderer, shell } from 'electron'; | 2 | import { clipboard, ipcRenderer, shell } from 'electron'; |
3 | import { action, reaction, computed, observable, makeObservable } from 'mobx'; | 3 | import { action, reaction, computed, observable, makeObservable } from 'mobx'; |
4 | import { debounce, remove } from 'lodash'; | 4 | import { debounce, remove } from 'lodash'; |
5 | import ms from 'ms'; | 5 | import ms from 'ms'; |
@@ -834,6 +834,54 @@ export default class ServicesStore extends TypedStore { | |||
834 | break; | 834 | break; |
835 | } | 835 | } |
836 | case 'notification': { | 836 | case 'notification': { |
837 | const { notificationId, options } = args[0]; | ||
838 | |||
839 | const { isTwoFactorAutoCatcherEnabled, twoFactorAutoCatcherMatcher } = | ||
840 | this.stores.settings.all.app; | ||
841 | |||
842 | debug( | ||
843 | 'Settings for catch tokens', | ||
844 | isTwoFactorAutoCatcherEnabled, | ||
845 | twoFactorAutoCatcherMatcher, | ||
846 | ); | ||
847 | |||
848 | if (isTwoFactorAutoCatcherEnabled) { | ||
849 | /* | ||
850 | parse the token digits from sms body, find "token" or "code" in options.body which reflect the sms content | ||
851 | --- | ||
852 | Token: 03624 / SMS-Code = PIN Token | ||
853 | --- | ||
854 | Prüfcode 010313 für Microsoft-Authentifizierung verwenden. | ||
855 | --- | ||
856 | 483133 is your GitHub authentication code. @github.com #483133 | ||
857 | --- | ||
858 | eBay: Ihr Sicherheitscode lautet 080090. \nEr läuft in 15 Minuten ab. Geben Sie den Code nicht an andere weiter. | ||
859 | --- | ||
860 | PayPal: Ihr Sicherheitscode lautet: 989605. Geben Sie diesen Code nicht weiter. | ||
861 | */ | ||
862 | |||
863 | const rawBody = options.body; | ||
864 | const { 0: token } = /\d{5,6}/.exec(options.body) || []; | ||
865 | |||
866 | const wordsToCatch = twoFactorAutoCatcherMatcher | ||
867 | .replaceAll(', ', ',') | ||
868 | .split(','); | ||
869 | |||
870 | debug('wordsToCatch', wordsToCatch); | ||
871 | |||
872 | if ( | ||
873 | token && | ||
874 | wordsToCatch.some(a => | ||
875 | options.body.toLowerCase().includes(a.toLowerCase()), | ||
876 | ) | ||
877 | ) { | ||
878 | // with the extra "+ " it shows its copied to clipboard in the notification | ||
879 | options.body = `+ ${rawBody}`; | ||
880 | clipboard.writeText(token); | ||
881 | debug('Token parsed and copied to clipboard'); | ||
882 | } | ||
883 | } | ||
884 | |||
837 | // Check if we are in scheduled Do-not-Disturb time | 885 | // Check if we are in scheduled Do-not-Disturb time |
838 | const { scheduledDNDEnabled, scheduledDNDStart, scheduledDNDEnd } = | 886 | const { scheduledDNDEnabled, scheduledDNDStart, scheduledDNDEnd } = |
839 | this.stores.settings.all.app; | 887 | this.stores.settings.all.app; |
@@ -845,8 +893,6 @@ export default class ServicesStore extends TypedStore { | |||
845 | return; | 893 | return; |
846 | } | 894 | } |
847 | 895 | ||
848 | const { notificationId, options } = args[0]; | ||
849 | |||
850 | if (service.isMuted || this.stores.settings.all.app.isAppMuted) { | 896 | if (service.isMuted || this.stores.settings.all.app.isAppMuted) { |
851 | Object.assign(options, { | 897 | Object.assign(options, { |
852 | silent: true, | 898 | silent: true, |
diff --git a/src/webview/notifications.ts b/src/webview/notifications.ts index aa88f13a2..e4401ab6e 100644 --- a/src/webview/notifications.ts +++ b/src/webview/notifications.ts | |||
@@ -37,7 +37,7 @@ export const notificationsClassDefinition = `(() => { | |||
37 | constructor(title = '', options = {}) { | 37 | constructor(title = '', options = {}) { |
38 | this.title = title; | 38 | this.title = title; |
39 | this.options = options; | 39 | this.options = options; |
40 | try{ | 40 | try { |
41 | window.ferdium.displayNotification(title, options) | 41 | window.ferdium.displayNotification(title, options) |
42 | .then(() => { | 42 | .then(() => { |
43 | if (typeof (this.onClick) === 'function') { | 43 | if (typeof (this.onClick) === 'function') { |
@@ -51,7 +51,7 @@ export const notificationsClassDefinition = `(() => { | |||
51 | if (typeof (this.onClick) === 'function') { | 51 | if (typeof (this.onClick) === 'function') { |
52 | this.onClick(); | 52 | this.onClick(); |
53 | } | 53 | } |
54 | }); | 54 | }); |
55 | } | 55 | } |
56 | } | 56 | } |
57 | 57 | ||