mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 04:52:58 +00:00
Create remote profiles during onboarding
This commit is contained in:
@@ -8,9 +8,9 @@ import {
|
||||
LOG_LEVEL_VERBOSE,
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
REMOTE_P2P,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { upsertRemoteConfigurationInPlace } from "@vrtmrz/livesync-commonlib/remote-configurations";
|
||||
import { isObjectDifferent } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import Intro from "./SetupWizard/dialogs/Intro.svelte";
|
||||
import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte";
|
||||
@@ -27,7 +27,6 @@ import SetupRemoteP2P from "./SetupWizard/dialogs/SetupRemoteP2P.svelte";
|
||||
import SetupRemoteE2EE from "./SetupWizard/dialogs/SetupRemoteE2EE.svelte";
|
||||
import { decodeSettingsFromQRCodeData } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
|
||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
import type {
|
||||
OutroAskUserModeResultType,
|
||||
OutroExistingUserResultType,
|
||||
@@ -45,6 +44,13 @@ import {
|
||||
applySettingsWithScheduledInitialisation,
|
||||
} from "@/serviceFeatures/setupObsidian/setupActivationLifecycle.ts";
|
||||
|
||||
function copySettingsForRemoteProfileUpdate(settings: ObsidianLiveSyncSettings): ObsidianLiveSyncSettings {
|
||||
return {
|
||||
...settings,
|
||||
remoteConfigurations: { ...(settings.remoteConfigurations ?? {}) },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* User modes for onboarding and setup
|
||||
*/
|
||||
@@ -162,20 +168,22 @@ export class SetupManager extends AbstractModule {
|
||||
currentSetting: ObsidianLiveSyncSettings,
|
||||
activate = true
|
||||
): Promise<boolean> {
|
||||
const originalSetting = JSON.parse(JSON.stringify(currentSetting)) as ObsidianLiveSyncSettings;
|
||||
const baseSetting = JSON.parse(JSON.stringify(originalSetting)) as ObsidianLiveSyncSettings;
|
||||
const couchConf = await this.dialogManager.openWithExplicitCancel<
|
||||
SetupRemoteCouchDBResultType,
|
||||
CouchDBConnection
|
||||
>(SetupRemoteCouchDB, originalSetting);
|
||||
>(SetupRemoteCouchDB, currentSetting);
|
||||
if (couchConf === "cancelled") {
|
||||
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||
return await this.onOnboard(userMode);
|
||||
}
|
||||
const newSetting = { ...baseSetting, ...couchConf } as ObsidianLiveSyncSettings;
|
||||
const newSetting = {
|
||||
...copySettingsForRemoteProfileUpdate(currentSetting),
|
||||
...couchConf,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
if (activate) {
|
||||
newSetting.remoteType = REMOTE_COUCHDB;
|
||||
}
|
||||
upsertRemoteConfigurationInPlace(newSetting, "couchdb", { activate });
|
||||
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
|
||||
}
|
||||
|
||||
@@ -199,10 +207,14 @@ export class SetupManager extends AbstractModule {
|
||||
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||
return await this.onOnboard(userMode);
|
||||
}
|
||||
const newSetting = { ...currentSetting, ...bucketConf } as ObsidianLiveSyncSettings;
|
||||
const newSetting = {
|
||||
...copySettingsForRemoteProfileUpdate(currentSetting),
|
||||
...bucketConf,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
if (activate) {
|
||||
newSetting.remoteType = REMOTE_MINIO;
|
||||
}
|
||||
upsertRemoteConfigurationInPlace(newSetting, "s3", { activate });
|
||||
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
|
||||
}
|
||||
|
||||
@@ -226,26 +238,15 @@ export class SetupManager extends AbstractModule {
|
||||
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||
return await this.onOnboard(userMode);
|
||||
}
|
||||
const newSetting = { ...currentSetting, ...p2pConf } as ObsidianLiveSyncSettings;
|
||||
// Apply remoteConfigurations
|
||||
if (newSetting.P2P_ActiveRemoteConfigurationId) {
|
||||
const id = newSetting.P2P_ActiveRemoteConfigurationId;
|
||||
const merged = {
|
||||
...newSetting,
|
||||
...p2pConf,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
const uri = ConnectionStringParser.serialize({ type: "p2p", settings: merged });
|
||||
newSetting.remoteConfigurations[id] = {
|
||||
...newSetting.remoteConfigurations[id],
|
||||
uri,
|
||||
isEncrypted: false,
|
||||
};
|
||||
newSetting.P2P_ActiveRemoteConfigurationId = id;
|
||||
}
|
||||
if (activate) {
|
||||
newSetting.remoteType = REMOTE_P2P;
|
||||
newSetting.activeConfigurationId = newSetting.P2P_ActiveRemoteConfigurationId;
|
||||
}
|
||||
const newSetting = {
|
||||
...copySettingsForRemoteProfileUpdate(currentSetting),
|
||||
...p2pConf,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
upsertRemoteConfigurationInPlace(newSetting, "p2p", {
|
||||
id: newSetting.P2P_ActiveRemoteConfigurationId || undefined,
|
||||
activate,
|
||||
activateForP2P: true,
|
||||
});
|
||||
return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate);
|
||||
}
|
||||
|
||||
|
||||
@@ -240,4 +240,268 @@ describe("SetupManager", () => {
|
||||
expect(applyExternalSettings).not.toHaveBeenCalled();
|
||||
expect(setting.currentSettings().isConfigured).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves modern profiles, display names, and the active selection from a Setup URI", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
const imported = {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteConfigurations: {
|
||||
couch: {
|
||||
id: "couch",
|
||||
name: "Office CouchDB",
|
||||
uri: "sls+https://alice:secret@couch.example/?db=notes",
|
||||
isEncrypted: false,
|
||||
},
|
||||
archive: {
|
||||
id: "archive",
|
||||
name: "Archive bucket",
|
||||
uri: "sls+s3://key:secret@storage.example/?endpoint=https%3A%2F%2Fstorage.example&bucket=archive®ion=auto",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "archive",
|
||||
} as ObsidianLiveSyncSettings;
|
||||
dialogManager.openWithExplicitCancel
|
||||
.mockResolvedValueOnce(imported)
|
||||
.mockResolvedValueOnce("compatible-existing-user");
|
||||
|
||||
await manager.onUseSetupURI(UserMode.Unknown, "mock-config://modern-settings");
|
||||
|
||||
const current = setting.currentSettings();
|
||||
expect(current.remoteConfigurations).toEqual(imported.remoteConfigurations);
|
||||
expect(current.activeConfigurationId).toBe("archive");
|
||||
expect(Object.keys(current.remoteConfigurations).some((id) => id.startsWith("legacy-"))).toBe(false);
|
||||
});
|
||||
|
||||
it("adds and activates a manually configured CouchDB without replacing existing profiles", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = {
|
||||
...setting.currentSettings(),
|
||||
isConfigured: true,
|
||||
remoteConfigurations: {
|
||||
existing: {
|
||||
id: "existing",
|
||||
name: "Existing remote",
|
||||
uri: "sls+http://old:secret@old.example/?db=old",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "existing",
|
||||
};
|
||||
dialogManager.openWithExplicitCancel
|
||||
.mockResolvedValueOnce({
|
||||
couchDB_URI: "https://couch.example",
|
||||
couchDB_USER: "alice",
|
||||
couchDB_PASSWORD: "secret",
|
||||
couchDB_DBNAME: "notes",
|
||||
couchDB_CustomHeaders: "",
|
||||
useJWT: false,
|
||||
jwtAlgorithm: "",
|
||||
jwtKey: "",
|
||||
jwtKid: "",
|
||||
jwtSub: "",
|
||||
jwtExpDuration: 5,
|
||||
useRequestAPI: false,
|
||||
})
|
||||
.mockResolvedValueOnce(true);
|
||||
|
||||
await manager.onCouchDBManualSetup(UserMode.ExistingUser, setting.currentSettings());
|
||||
|
||||
const current = setting.currentSettings();
|
||||
expect(current.remoteConfigurations.existing).toBeDefined();
|
||||
expect(Object.keys(current.remoteConfigurations)).toHaveLength(2);
|
||||
expect(current.activeConfigurationId).not.toBe("existing");
|
||||
const activeProfile = current.remoteConfigurations[current.activeConfigurationId];
|
||||
expect(activeProfile?.name).toBe("CouchDB couch.example");
|
||||
expect(activeProfile?.uri).toContain("sls+https://alice:secret@couch.example");
|
||||
});
|
||||
|
||||
it("adds and activates a manually configured Object Storage profile without replacing existing profiles", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = {
|
||||
...setting.currentSettings(),
|
||||
isConfigured: true,
|
||||
remoteConfigurations: {
|
||||
existing: {
|
||||
id: "existing",
|
||||
name: "Existing remote",
|
||||
uri: "sls+http://old:secret@old.example/?db=old",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "existing",
|
||||
};
|
||||
dialogManager.openWithExplicitCancel
|
||||
.mockResolvedValueOnce({
|
||||
endpoint: "https://storage.example",
|
||||
accessKey: "key",
|
||||
secretKey: "secret",
|
||||
bucket: "notes",
|
||||
region: "auto",
|
||||
bucketPrefix: "",
|
||||
useCustomRequestHandler: false,
|
||||
bucketCustomHeaders: "",
|
||||
forcePathStyle: true,
|
||||
})
|
||||
.mockResolvedValueOnce(true);
|
||||
|
||||
await manager.onBucketManualSetup(UserMode.ExistingUser, setting.currentSettings());
|
||||
|
||||
const current = setting.currentSettings();
|
||||
expect(current.remoteConfigurations.existing).toBeDefined();
|
||||
expect(Object.keys(current.remoteConfigurations)).toHaveLength(2);
|
||||
expect(current.activeConfigurationId).not.toBe("existing");
|
||||
const activeProfile = current.remoteConfigurations[current.activeConfigurationId];
|
||||
expect(activeProfile?.name).toBe("S3 notes");
|
||||
expect(activeProfile?.uri).toContain("sls+s3://key:secret@storage.example");
|
||||
});
|
||||
|
||||
it("creates and selects a P2P profile during fresh manual onboarding", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = {
|
||||
...setting.currentSettings(),
|
||||
isConfigured: false,
|
||||
remoteConfigurations: {},
|
||||
activeConfigurationId: "",
|
||||
P2P_ActiveRemoteConfigurationId: "",
|
||||
};
|
||||
dialogManager.openWithExplicitCancel
|
||||
.mockResolvedValueOnce({
|
||||
P2P_Enabled: true,
|
||||
P2P_roomID: "team-room",
|
||||
P2P_passphrase: "secret",
|
||||
P2P_relays: "wss://relay.example",
|
||||
P2P_AppID: "self-hosted-livesync",
|
||||
P2P_AutoStart: true,
|
||||
P2P_AutoBroadcast: false,
|
||||
P2P_turnServers: "",
|
||||
P2P_turnUsername: "",
|
||||
P2P_turnCredential: "",
|
||||
})
|
||||
.mockResolvedValueOnce(true);
|
||||
|
||||
await manager.onP2PManualSetup(UserMode.NewUser, setting.currentSettings());
|
||||
|
||||
const current = setting.currentSettings();
|
||||
expect(Object.keys(current.remoteConfigurations)).toHaveLength(1);
|
||||
expect(current.activeConfigurationId).not.toBe("");
|
||||
expect(current.P2P_ActiveRemoteConfigurationId).toBe(current.activeConfigurationId);
|
||||
const activeProfile = current.remoteConfigurations[current.activeConfigurationId];
|
||||
expect(activeProfile?.name).toBe("P2P team-room");
|
||||
expect(activeProfile?.uri).toContain("sls+p2p://");
|
||||
});
|
||||
|
||||
it("selects a configured P2P profile without replacing the active main remote", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = {
|
||||
...setting.currentSettings(),
|
||||
isConfigured: true,
|
||||
remoteConfigurations: {
|
||||
main: {
|
||||
id: "main",
|
||||
name: "Main CouchDB",
|
||||
uri: "sls+http://old:secret@old.example/?db=old",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "main",
|
||||
P2P_ActiveRemoteConfigurationId: "",
|
||||
};
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce({
|
||||
P2P_Enabled: true,
|
||||
P2P_roomID: "team-room",
|
||||
P2P_passphrase: "secret",
|
||||
P2P_relays: "wss://relay.example",
|
||||
P2P_AppID: "self-hosted-livesync",
|
||||
P2P_AutoStart: true,
|
||||
P2P_AutoBroadcast: false,
|
||||
P2P_turnServers: "",
|
||||
P2P_turnUsername: "",
|
||||
P2P_turnCredential: "",
|
||||
});
|
||||
|
||||
await manager.onP2PManualSetup(UserMode.Unknown, setting.currentSettings(), false);
|
||||
|
||||
const current = setting.currentSettings();
|
||||
expect(Object.keys(current.remoteConfigurations)).toHaveLength(2);
|
||||
expect(current.activeConfigurationId).toBe("main");
|
||||
expect(current.P2P_ActiveRemoteConfigurationId).not.toBe("");
|
||||
expect(current.P2P_ActiveRemoteConfigurationId).not.toBe("main");
|
||||
expect(current.remoteConfigurations[current.P2P_ActiveRemoteConfigurationId]?.name).toBe("P2P team-room");
|
||||
});
|
||||
|
||||
it("does not register Object Storage when final confirmation is cancelled", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = {
|
||||
...setting.currentSettings(),
|
||||
isConfigured: true,
|
||||
remoteConfigurations: {
|
||||
existing: {
|
||||
id: "existing",
|
||||
name: "Existing remote",
|
||||
uri: "sls+http://old:secret@old.example/?db=old",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "existing",
|
||||
};
|
||||
const before = structuredClone(setting.currentSettings().remoteConfigurations);
|
||||
dialogManager.openWithExplicitCancel
|
||||
.mockResolvedValueOnce({
|
||||
endpoint: "https://storage.example",
|
||||
accessKey: "key",
|
||||
secretKey: "secret",
|
||||
bucket: "notes",
|
||||
region: "auto",
|
||||
bucketPrefix: "",
|
||||
useCustomRequestHandler: false,
|
||||
bucketCustomHeaders: "",
|
||||
forcePathStyle: true,
|
||||
})
|
||||
.mockResolvedValueOnce("cancelled");
|
||||
|
||||
await manager.onBucketManualSetup(UserMode.ExistingUser, setting.currentSettings());
|
||||
|
||||
expect(setting.currentSettings().remoteConfigurations).toEqual(before);
|
||||
expect(setting.currentSettings().activeConfigurationId).toBe("existing");
|
||||
});
|
||||
|
||||
it("does not mutate an existing P2P profile when final confirmation is cancelled", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = {
|
||||
...setting.currentSettings(),
|
||||
isConfigured: true,
|
||||
remoteConfigurations: {
|
||||
existing: {
|
||||
id: "existing",
|
||||
name: "Existing P2P remote",
|
||||
uri: "sls+p2p://old-room?passphrase=old-secret",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "existing",
|
||||
P2P_ActiveRemoteConfigurationId: "existing",
|
||||
};
|
||||
const before = structuredClone(setting.currentSettings().remoteConfigurations);
|
||||
dialogManager.openWithExplicitCancel
|
||||
.mockResolvedValueOnce({
|
||||
P2P_Enabled: true,
|
||||
P2P_roomID: "new-room",
|
||||
P2P_passphrase: "new-secret",
|
||||
P2P_relays: "wss://relay.example",
|
||||
P2P_AppID: "self-hosted-livesync",
|
||||
P2P_AutoStart: true,
|
||||
P2P_AutoBroadcast: false,
|
||||
P2P_turnServers: "",
|
||||
P2P_turnUsername: "",
|
||||
P2P_turnCredential: "",
|
||||
})
|
||||
.mockResolvedValueOnce("cancelled");
|
||||
|
||||
await manager.onP2PManualSetup(UserMode.ExistingUser, setting.currentSettings());
|
||||
|
||||
expect(setting.currentSettings().remoteConfigurations).toEqual(before);
|
||||
expect(setting.currentSettings().activeConfigurationId).toBe("existing");
|
||||
expect(setting.currentSettings().P2P_ActiveRemoteConfigurationId).toBe("existing");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user