Skip to content

Commit

Permalink
Desktop: Resolves #8625: Show missing sync password warning and link …
Browse files Browse the repository at this point in the history
…to FAQ (#8644)
  • Loading branch information
personalizedrefrigerator committed Aug 14, 2023
1 parent 9e55d90 commit c6c2733
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 8 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Expand Up @@ -516,6 +516,8 @@ packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/note-screen-shared.js
packages/lib/components/shared/reduxSharedMiddleware.js
packages/lib/database-driver-better-sqlite.js
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -502,6 +502,8 @@ packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/note-screen-shared.js
packages/lib/components/shared/reduxSharedMiddleware.js
packages/lib/database-driver-better-sqlite.js
Expand Down
33 changes: 32 additions & 1 deletion packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
Expand Up @@ -12,14 +12,17 @@ const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils');
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
const shared = require('@joplin/lib/components/shared/config-shared.js');
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
import ClipperConfigScreen from '../ClipperConfigScreen';
import restart from '../../services/restart';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { getDefaultPluginsInstallState, updateDefaultPluginsInstallState } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen';
import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton';
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
import shim from '@joplin/lib/shim';
import StyledLink from '../style/StyledLink';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');

const settingKeyToControl: any = {
Expand Down Expand Up @@ -181,6 +184,34 @@ class ConfigScreenComponent extends React.Component<any, any> {
if (section.name === 'sync') {
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
const statusStyle = { ...theme.textStyle, marginTop: 10 };
const warningStyle = { ...theme.textStyle, color: theme.colorWarn };

// Don't show the missing password warning if the user just changed the sync target (but hasn't
// saved yet).
const matchesSavedTarget = settings['sync.target'] === this.props.settings['sync.target'];
if (matchesSavedTarget && shouldShowMissingPasswordWarning(settings['sync.target'], settings)) {
const openMissingPasswordFAQ = () =>
bridge().openExternal('https://joplinapp.org/faq#why-did-my-sync-and-encryption-passwords-disappear-after-updating-joplin');

const macInfoLink = (
<StyledLink href="#"
onClick={openMissingPasswordFAQ}
style={theme.linkStyle}
>
{_('Help')}
</StyledLink>
);

// The FAQ section related to missing passwords is specific to MacOS/ARM -- only show it
// in that case.
const showMacInfoLink = shim.isMac() && process.arch === 'arm64';

settingComps.push(
<p key='missing-password-warning' style={warningStyle}>
{_('Warning: Missing password.')}{' '}{showMacInfoLink ? macInfoLink : null}
</p>
);
}

if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
Expand Down
21 changes: 20 additions & 1 deletion packages/app-desktop/gui/MainScreen/MainScreen.tsx
Expand Up @@ -20,6 +20,7 @@ import NoteListWrapper from '../NoteListWrapper/NoteListWrapper';
import { AppState } from '../../app.reducer';
import { saveLayout, loadLayout } from '../ResizableLayout/utils/persist';
import Setting from '@joplin/lib/models/Setting';
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
import produce from 'immer';
import shim from '@joplin/lib/shim';
import bridge from '../../services/bridge';
Expand Down Expand Up @@ -67,6 +68,7 @@ interface Props {
shouldUpgradeSyncTarget: boolean;
hasDisabledSyncItems: boolean;
hasDisabledEncryptionItems: boolean;
hasMissingSyncCredentials: boolean;
showMissingMasterKeyMessage: boolean;
showNeedUpgradingMasterKeyMessage: boolean;
showShouldReencryptMessage: boolean;
Expand Down Expand Up @@ -561,6 +563,16 @@ class MainScreenComponent extends React.Component<Props, State> {
});
};

const onViewSyncSettingsScreen = () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Config',
props: {
defaultSection: 'sync',
},
});
};

const onViewPluginScreen = () => {
this.props.dispatch({
type: 'NAV_GO',
Expand Down Expand Up @@ -598,6 +610,12 @@ class MainScreenComponent extends React.Component<Props, State> {
_('Disable safe mode and restart'),
onDisableSafeModeAndRestart
);
} else if (this.props.hasMissingSyncCredentials) {
msg = this.renderNotificationMessage(
_('The synchronisation password is missing.'),
_('Set the password'),
onViewSyncSettingsScreen
);
} else if (this.props.shouldUpgradeSyncTarget) {
msg = this.renderNotificationMessage(
_('The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.'),
Expand Down Expand Up @@ -662,7 +680,7 @@ class MainScreenComponent extends React.Component<Props, State> {

public messageBoxVisible(props: Props = null) {
if (!props) props = this.props;
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || this.props.showInstallTemplatesPlugin;
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.hasMissingSyncCredentials || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || this.props.showInstallTemplatesPlugin;
}

public registerCommands() {
Expand Down Expand Up @@ -875,6 +893,7 @@ const mapStateToProps = (state: AppState) => {
showNeedUpgradingMasterKeyMessage: showNeedUpgradingEnabledMasterKeyMessage,
showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
hasMissingSyncCredentials: shouldShowMissingPasswordWarning(state.settings['sync.target'], state.settings),
pluginsLegacy: state.pluginsLegacy,
plugins: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
Expand Down
Expand Up @@ -20,7 +20,7 @@ const { _ } = require('@joplin/lib/locale');
const { BaseScreenComponent } = require('../../base-screen.js');
const { Dropdown } = require('../../Dropdown');
const { themeStyle } = require('../../global-style.js');
const shared = require('@joplin/lib/components/shared/config-shared.js');
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import { openDocumentTree } from '@joplin/react-native-saf-x';
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
Expand Down
6 changes: 6 additions & 0 deletions packages/lib/BaseSyncTarget.ts
Expand Up @@ -26,6 +26,12 @@ export default class BaseSyncTarget {
return false;
}

// Returns true if the sync target expects a non-empty sync.{id}.password
// setting.
public static requiresPassword() {
return false;
}

public static description(): string {
return '';
}
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/SyncTargetAmazonS3.js
Expand Up @@ -36,6 +36,10 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
return true;
}

static requiresPassword() {
return true;
}

static s3BucketName() {
return Setting.value('sync.8.path');
}
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/SyncTargetJoplinCloud.ts
Expand Up @@ -49,6 +49,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
return true;
}

public static requiresPassword() {
return true;
}

public async fileApi(): Promise<FileApi> {
return super.fileApi();
}
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/SyncTargetJoplinServer.ts
Expand Up @@ -65,6 +65,10 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
return true;
}

public static requiresPassword() {
return true;
}

public async fileApi(): Promise<FileApi> {
return super.fileApi();
}
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/SyncTargetNextcloud.js
Expand Up @@ -33,6 +33,10 @@ class SyncTargetNextcloud extends BaseSyncTarget {
return true;
}

static requiresPassword() {
return true;
}

static async checkConfig(options) {
return SyncTargetWebDAV.checkConfig(options);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/SyncTargetWebDAV.js
Expand Up @@ -32,6 +32,10 @@ class SyncTargetWebDAV extends BaseSyncTarget {
return true;
}

static requiresPassword() {
return true;
}

static async newFileApi_(syncTargetId, options) {
const apiOptions = {
baseUrl: () => options.path(),
Expand Down
@@ -1,7 +1,7 @@
const Setting = require('../../models/Setting').default;
const SyncTargetRegistry = require('../../SyncTargetRegistry').default;
const ObjectUtils = require('../../ObjectUtils');
const { _ } = require('../../locale');
const Setting = require('../../../models/Setting').default;
const SyncTargetRegistry = require('../../../SyncTargetRegistry').default;
const ObjectUtils = require('../../../ObjectUtils');
const { _ } = require('../../../locale');
const { createSelector } = require('reselect');
const Logger = require('@joplin/utils/Logger').default;

Expand Down
@@ -0,0 +1,42 @@
import SyncTargetRegistry from '../../../SyncTargetRegistry';
import shouldShowMissingPasswordWarning from './shouldShowMissingPasswordWarning';

// Maps targets to whether each target requires a password.
// A subset of all sync targets.
const targetToRequiresPassword: Record<string, boolean> = {
'nextcloud': true,
'webdav': true,
'amazon_s3': true,
'joplinServer': true,
'joplinCloud': true,
'onedrive': false,
'dropbox': false,
};

describe('shouldShowMissingPasswordWarning', () => {
it('should return true when sync target requires a password and the password is missing', () => {
for (const targetName in targetToRequiresPassword) {
const targetId = SyncTargetRegistry.nameToId(targetName);
const expected = targetToRequiresPassword[targetName];

expect(shouldShowMissingPasswordWarning(targetId, {})).toBe(expected);

// Should also consider an empty string to be missing
const settings = {
[`sync.${targetId}.password`]: '',
};
expect(shouldShowMissingPasswordWarning(targetId, settings)).toBe(expected);
}
});

it('should return false when a password is present', () => {
for (const targetName in targetToRequiresPassword) {
const targetId = SyncTargetRegistry.nameToId(targetName);
const settings = {
[`sync.${targetId}.password`]: 'some nonempty',
};

expect(shouldShowMissingPasswordWarning(targetId, settings)).toBe(false);
}
});
});
@@ -0,0 +1,9 @@
import SyncTargetRegistry from '../../../SyncTargetRegistry';

const shouldShowMissingPasswordWarning = (syncTargetId: number, settings: any) => {
const syncTargetClass = SyncTargetRegistry.classById(syncTargetId);

return syncTargetClass.requiresPassword() && !settings[`sync.${syncTargetId}.password`];
};

export default shouldShowMissingPasswordWarning;
Expand Up @@ -5,7 +5,7 @@ import shim from '../../../shim';
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '../PluginService';
import Logger from '@joplin/utils/Logger';
import * as React from 'react';
const shared = require('../../../components/shared/config-shared.js');
const shared = require('../../../components/shared/config/config-shared.js');

const logger = Logger.create('defaultPluginsUtils');

Expand Down
2 changes: 2 additions & 0 deletions packages/lib/testing/test-utils.ts
Expand Up @@ -41,6 +41,7 @@ const SyncTargetFilesystem = require('../SyncTargetFilesystem.js');
const SyncTargetNextcloud = require('../SyncTargetNextcloud.js');
const SyncTargetDropbox = require('../SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('../SyncTargetAmazonS3.js');
const SyncTargetWebDAV = require('../SyncTargetWebDAV.js');
import SyncTargetJoplinServer from '../SyncTargetJoplinServer';
import EncryptionService from '../services/e2ee/EncryptionService';
import DecryptionWorker from '../services/DecryptionWorker';
Expand Down Expand Up @@ -122,6 +123,7 @@ SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);

Expand Down
5 changes: 5 additions & 0 deletions readme/faq.md
Expand Up @@ -141,6 +141,11 @@ In this case, [make sure you enter the correct WebDAV URL](https://github.com/la
- Check the WebDAV URL - to get the correct URL, go to Nextcloud and, in the left sidebar, click on "Settings" and copy the WebDAV URL from there. **Do not forget to add the folder you've created to that URL**. For example, if the base the WebDAV URL is "https://example.com/nextcloud/remote.php/webdav/" and you want the notes to be synced in the "Joplin" directory, you need to give the URL "https://example.com/nextcloud/remote.php/webdav/Joplin" **and you need to create the "Joplin" directory yourself**.
- Did you enable **2FA** (Multi-factor authentication) on Nextcloud? In that case, you need to [create an app password for Joplin in the Nextcloud admin interface](https://github.com/laurent22/joplin/issues/1453#issuecomment-486640902).

## Why did my sync and encryption passwords disappear after updating Joplin?

- With version 2.12, Joplin supports M1 Macs natively! As a result, upgrading Joplin on one of these systems causes Joplin to lose access to information stored by older versions of the app in the system keychain. This includes sync and encryption passwords.
- Re-entering the passwords should fix related sync and encryption issues.

## How can I use self-signed SSL certificates on Android?

If you want to serve using https but can't or don't want to use SSL certificates signed by trusted certificate authorities (like "Let's Encrypt"), it's possible to generate a custom CA and sign your certificates with it. You can generate the CA and certificates using [openssl](https://gist.github.com/fntlnz/cf14feb5a46b2eda428e000157447309), but I like to use a tool called [mkcert](https://github.com/FiloSottile/mkcert) for it's simplicity. Finally, you have to add your CA certificate to Android settings so that Android can recognize the certificates you signed with your CA as valid ([link](https://support.google.com/nexus/answer/2844832?hl=en-GB)).
Expand Down

0 comments on commit c6c2733

Please sign in to comment.