mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-13 11:01:16 +00:00
v0.25.23.beta1
### Fixed (This should be backported to 0.25.22 if the beta phase is prolonged) - No longer larger files will not create a chunks during preparing `Reset Synchronisation on This Device`. ### Behaviour changes - Setup wizard is now more `goal-oriented`. Brand-new screens are introduced. - `Fetch everything` and `Rebuild everything` is now `Reset Synchronisation on This Device` and `Overwrite Server Data with This Device's Files`. - Remote configuration and E2EE settings are now separated to each modal dialogue. - Peer-to-Peer settings is also separated into its own modal dialogue. - Setup-URI, and Report for the Issue are now not copied to clipboard automatically. Instead, there are copy dialogue and buttons to copy them explicitly. - No longer optional features are introduced during the setup or `Reset Synchronisation on This Device`, `Overwrite Server Data with This Device's Files`. - We cannot preform `Fetch everything` and `Rebuild everything` (Removed, so the old name) without restarting Obsidian now. ### Miscellaneous - Setup QR Code generation is separated into a src/lib/src/API/processSetting.ts file. Please use it as a subrepository if you want to generate QR codes in your own application. - Setup-URI is also separated into a src/lib/src/API/processSetting.ts - Some direct access to web-APIs are now wrapped into the services layer. ### Dependency updates - Many dependencies are updated. Please see `package.json`. - As upgrading TypeScript, Fixed many UInt8Array<ArrayBuffer> and Uint8Array type mismatches.
This commit is contained in:
154
src/modules/features/SetupWizard/dialogs/FetchEverything.svelte
Normal file
154
src/modules/features/SetupWizard/dialogs/FetchEverything.svelte
Normal file
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||
const TYPE_IDENTICAL = "identical";
|
||||
const TYPE_INDEPENDENT = "independent";
|
||||
const TYPE_UNBALANCED = "unbalanced";
|
||||
const TYPE_CANCEL = "cancelled";
|
||||
|
||||
const TYPE_BACKUP_DONE = "backup_done";
|
||||
const TYPE_BACKUP_SKIPPED = "backup_skipped";
|
||||
const TYPE_UNABLE_TO_BACKUP = "unable_to_backup";
|
||||
|
||||
type ResultTypeVault =
|
||||
| typeof TYPE_IDENTICAL
|
||||
| typeof TYPE_INDEPENDENT
|
||||
| typeof TYPE_UNBALANCED
|
||||
| typeof TYPE_CANCEL;
|
||||
type ResultTypeBackup =
|
||||
| typeof TYPE_BACKUP_DONE
|
||||
| typeof TYPE_BACKUP_SKIPPED
|
||||
| typeof TYPE_UNABLE_TO_BACKUP
|
||||
| typeof TYPE_CANCEL;
|
||||
|
||||
type ResultTypeExtra = {
|
||||
preventFetchingConfig: boolean;
|
||||
};
|
||||
type ResultType =
|
||||
| {
|
||||
vault: ResultTypeVault;
|
||||
backup: ResultTypeBackup;
|
||||
extra: ResultTypeExtra;
|
||||
}
|
||||
| typeof TYPE_CANCEL;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
let vaultType = $state<ResultTypeVault>(TYPE_CANCEL);
|
||||
let backupType = $state<ResultTypeBackup>(TYPE_CANCEL);
|
||||
const canProceed = $derived.by(() => {
|
||||
return (
|
||||
(vaultType === TYPE_IDENTICAL || vaultType === TYPE_INDEPENDENT || vaultType === TYPE_UNBALANCED) &&
|
||||
(backupType === TYPE_BACKUP_DONE || backupType === TYPE_BACKUP_SKIPPED)
|
||||
);
|
||||
});
|
||||
let preventFetchingConfig = $state(false);
|
||||
|
||||
function commit() {
|
||||
setResult({
|
||||
vault: vaultType,
|
||||
backup: backupType,
|
||||
extra: {
|
||||
preventFetchingConfig,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Reset Synchronisation on This Device" />
|
||||
<Guidance
|
||||
>This will rebuild the local database on this device using the most recent data from the server. This action is
|
||||
designed to resolve synchronisation inconsistencies and restore correct functionality.</Guidance
|
||||
>
|
||||
<Guidance important title="⚠️ Important Notice">
|
||||
<strong
|
||||
>If you have unsynchronised changes in your Vault on this device, they will likely diverge from the server's
|
||||
versions after the reset. This may result in a large number of file conflicts.</strong
|
||||
><br />
|
||||
Furthermore, if conflicts are already present in the server data, they will be synchronised to this device as they are,
|
||||
and you will need to resolve them locally.
|
||||
</Guidance>
|
||||
<hr />
|
||||
<Instruction>
|
||||
<Question
|
||||
><strong>To minimise the creation of new conflicts</strong>, please select the option that best describes the
|
||||
current state of your Vault. The application will then check your files in the most appropriate way based on
|
||||
your selection.</Question
|
||||
>
|
||||
<Options>
|
||||
<Option
|
||||
selectedValue={TYPE_IDENTICAL}
|
||||
title="The files in this Vault are almost identical to the server's."
|
||||
bind:value={vaultType}
|
||||
>
|
||||
(e.g., immediately after restoring on another computer, or having recovered from a backup)
|
||||
</Option>
|
||||
<Option
|
||||
selectedValue={TYPE_INDEPENDENT}
|
||||
title="This Vault is empty, or contains only new files that are not on the server."
|
||||
bind:value={vaultType}
|
||||
>
|
||||
(e.g., setting up for the first time on a new smartphone, starting from a clean slate)
|
||||
</Option>
|
||||
<Option
|
||||
selectedValue={TYPE_UNBALANCED}
|
||||
title="There may be differences between the files in this Vault and the server."
|
||||
bind:value={vaultType}
|
||||
>
|
||||
(e.g., after editing many files whilst offline)
|
||||
<InfoNote info>
|
||||
In this scenario, Self-hosted LiveSync will recreate metadata for every file and deliberately generate
|
||||
conflicts. Where the file content is identical, these conflicts will be resolved automatically.
|
||||
</InfoNote>
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
<hr />
|
||||
<Instruction>
|
||||
<Question>Have you created a backup before proceeding?</Question>
|
||||
<InfoNote>
|
||||
We recommend that you copy your Vault folder to a safe location. This will provide a safeguard in case a large
|
||||
number of conflicts arise, or if you accidentally synchronise with an incorrect destination.
|
||||
</InfoNote>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_BACKUP_DONE} title="I have created a backup of my Vault." bind:value={backupType} />
|
||||
<Option
|
||||
selectedValue={TYPE_BACKUP_SKIPPED}
|
||||
title="I understand the risks and will proceed without a backup."
|
||||
bind:value={backupType}
|
||||
/>
|
||||
<Option
|
||||
selectedValue={TYPE_UNABLE_TO_BACKUP}
|
||||
title="I am unable to create a backup of my Vault."
|
||||
bind:value={backupType}
|
||||
>
|
||||
<InfoNote error visible={backupType === TYPE_UNABLE_TO_BACKUP}>
|
||||
<strong
|
||||
>It is strongly advised to create a backup before proceeding. Continuing without a backup may lead
|
||||
to data loss.
|
||||
</strong>
|
||||
<br />
|
||||
If you understand the risks and still wish to proceed, select so.
|
||||
</InfoNote>
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
<Instruction>
|
||||
<ExtraItems title="Advanced">
|
||||
<Check title="Prevent fetching configuration from server" bind:value={preventFetchingConfig} />
|
||||
</ExtraItems>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title="Reset and Resume Synchronisation" important disabled={!canProceed} commit={() => commit()} />
|
||||
<Decision title="Cancel" commit={() => setResult(TYPE_CANCEL)} />
|
||||
</UserDecisions>
|
||||
55
src/modules/features/SetupWizard/dialogs/Intro.svelte
Normal file
55
src/modules/features/SetupWizard/dialogs/Intro.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
const TYPE_NEW_USER = "new-user";
|
||||
const TYPE_EXISTING_USER = "existing-user";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_NEW_USER | typeof TYPE_EXISTING_USER | typeof TYPE_CANCELLED;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||
let proceedTitle = $derived.by(() => {
|
||||
if (userType === TYPE_NEW_USER) {
|
||||
return "Yes, I want to set up a new synchronisation";
|
||||
} else if (userType === TYPE_EXISTING_USER) {
|
||||
return "Yes, I want to add this device to my existing synchronisation";
|
||||
} else {
|
||||
return "Please select an option to proceed";
|
||||
}
|
||||
});
|
||||
const canProceed = $derived.by(() => {
|
||||
return userType === TYPE_NEW_USER || userType === TYPE_EXISTING_USER;
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Welcome to Self-hosted LiveSync" />
|
||||
<Guidance>We will now guide you through a few questions to simplify the synchronisation setup.</Guidance>
|
||||
<Instruction>
|
||||
<Question>First, please select the option that best describes your current situation.</Question>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_NEW_USER} title="I am setting this up for the first time" bind:value={userType}>
|
||||
(Select this if you are configuring this device as the first synchronisation device.) This option is
|
||||
suitable if you are new to LiveSync and want to set it up from scratch.
|
||||
</Option>
|
||||
<Option
|
||||
selectedValue={TYPE_EXISTING_USER}
|
||||
title="I am adding a device to an existing synchronisation setup"
|
||||
bind:value={userType}
|
||||
>
|
||||
(Select this if you are already using synchronisation on another computer or smartphone.) This option is
|
||||
suitable if you are new to LiveSync and want to set it up from scratch.
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
const TYPE_EXISTING = "existing-user";
|
||||
const TYPE_NEW = "new-user";
|
||||
const TYPE_COMPATIBLE_EXISTING = "compatible-existing-user";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_EXISTING | typeof TYPE_NEW | typeof TYPE_COMPATIBLE_EXISTING | typeof TYPE_CANCELLED;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||
const canProceed = $derived.by(() => {
|
||||
return userType === TYPE_EXISTING || userType === TYPE_NEW || userType === TYPE_COMPATIBLE_EXISTING;
|
||||
});
|
||||
const proceedMessage = $derived.by(() => {
|
||||
if (userType === TYPE_NEW) {
|
||||
return "Proceed to the next step.";
|
||||
} else if (userType === TYPE_EXISTING) {
|
||||
return "Proceed to the next step.";
|
||||
} else if (userType === TYPE_COMPATIBLE_EXISTING) {
|
||||
return "Apply the settings";
|
||||
} else {
|
||||
return "Please select an option to proceed";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Mostly Complete: Decision Required" />
|
||||
<Guidance>
|
||||
The connection to the server has been configured successfully. As the next step, <strong
|
||||
>the local database, that is to say the synchronisation information, must be reconstituted.</strong
|
||||
>
|
||||
</Guidance>
|
||||
<Instruction>
|
||||
<Question>Please select your situation.</Question>
|
||||
<Option title="I am setting up a new server for the first time / I want to reset my existing server." bind:value={userType} selectedValue={TYPE_NEW}>
|
||||
<InfoNote>
|
||||
Selecting this option will result in the current data on this device being used to initialise the server.
|
||||
Any existing data on the server will be completely overwritten.
|
||||
</InfoNote>
|
||||
</Option>
|
||||
<Option
|
||||
title="My remote server is already set up. I want to join this device."
|
||||
bind:value={userType}
|
||||
selectedValue={TYPE_EXISTING}
|
||||
>
|
||||
<InfoNote>
|
||||
Selecting this option will result in this device joining the existing server. You need to fetching the
|
||||
existing synchronisation data from the server to this device.
|
||||
</InfoNote>
|
||||
</Option>
|
||||
<Option
|
||||
title="The remote is already set up, and the configuration is compatible (or got compatible by this operation)."
|
||||
bind:value={userType}
|
||||
selectedValue={TYPE_COMPATIBLE_EXISTING}
|
||||
>
|
||||
<InfoNote warning>
|
||||
Unless you are certain, selecting this options is bit dangerous. It assumes that the server configuration is
|
||||
compatible with this device. If this is not the case, data loss may occur. Please ensure you know what you
|
||||
are doing.
|
||||
</InfoNote>
|
||||
</Option>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title={proceedMessage} important={true} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
const TYPE_APPLY = "apply";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Setup Complete: Preparing to Fetch Synchronisation Data" />
|
||||
<Guidance>
|
||||
<p>
|
||||
The connection to the server has been configured successfully. As the next step, <strong
|
||||
>the latest synchronisation data will be downloaded from the server to this device.</strong
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>PLEASE NOTE</strong>
|
||||
<br />
|
||||
After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised
|
||||
files in this vault, conflicts may occur with the server data.
|
||||
</p>
|
||||
</Guidance>
|
||||
<Instruction>
|
||||
<Question>Please select the button below to restart and proceed to the data fetching confirmation.</Question>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title="Restart and Fetch Data" important={true} commit={() => setResult(TYPE_APPLY)} />
|
||||
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
38
src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte
Normal file
38
src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
const TYPE_APPLY = "apply";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
// let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Setup Complete: Preparing to Initialise Server" />
|
||||
<Guidance>
|
||||
<p>
|
||||
The connection to the server has been configured successfully. As the next step, <strong
|
||||
>the synchronisation data on the server will be built based on the current data on this device.</strong
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>IMPORTANT</strong>
|
||||
<br />
|
||||
After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware that
|
||||
any unintended data currently on the server will be completely overwritten.
|
||||
</p>
|
||||
</Guidance>
|
||||
<Instruction>
|
||||
<Question>Please select the button below to restart and proceed to the final confirmation.</Question>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title="Restart and Initialise Server" important={true} commit={() => setResult(TYPE_APPLY)} />
|
||||
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Panel to check and fix CouchDB configuration issues
|
||||
*/
|
||||
import type { ObsidianLiveSyncSettings } from "../../../../lib/src/common/types";
|
||||
import Decision from "../../../../lib/src/UI/components/Decision.svelte";
|
||||
import UserDecisions from "../../../../lib/src/UI/components/UserDecisions.svelte";
|
||||
import { checkConfig, type ConfigCheckResult, type ResultError, type ResultErrorMessage } from "./utilCheckCouchDB";
|
||||
type Props = {
|
||||
trialRemoteSetting: ObsidianLiveSyncSettings;
|
||||
};
|
||||
const { trialRemoteSetting }: Props = $props();
|
||||
let detectedIssues = $state<ConfigCheckResult[]>([]);
|
||||
async function testAndFixSettings() {
|
||||
detectedIssues = [];
|
||||
try {
|
||||
const fixResults = await checkConfig(trialRemoteSetting);
|
||||
console.dir(fixResults);
|
||||
detectedIssues = fixResults;
|
||||
} catch (e) {
|
||||
console.error("Error during testAndFixSettings:", e);
|
||||
detectedIssues.push({ message: `Error during testAndFixSettings: ${e}`, result: "error", classes: [] });
|
||||
}
|
||||
}
|
||||
function isErrorResult(result: ConfigCheckResult): result is ResultError | ResultErrorMessage {
|
||||
return "result" in result && result.result === "error";
|
||||
}
|
||||
function isFixableError(result: ConfigCheckResult): result is ResultError {
|
||||
return isErrorResult(result) && "fix" in result && typeof result.fix === "function";
|
||||
}
|
||||
function isSuccessResult(result: ConfigCheckResult): result is { message: string; result: "ok"; value?: any } {
|
||||
return "result" in result && result.result === "ok";
|
||||
}
|
||||
let processing = $state(false);
|
||||
async function fixIssue(issue: ResultError) {
|
||||
try {
|
||||
processing = true;
|
||||
await issue.fix();
|
||||
} catch (e) {
|
||||
console.error("Error during fixIssue:", e);
|
||||
}
|
||||
await testAndFixSettings();
|
||||
processing = false;
|
||||
}
|
||||
const errorIssueCount = $derived.by(() => {
|
||||
return detectedIssues.filter((issue) => isErrorResult(issue)).length;
|
||||
});
|
||||
const isAllSuccess = $derived.by(() => {
|
||||
return !(errorIssueCount > 0 && detectedIssues.length > 0);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#snippet result(issue: ConfigCheckResult)}
|
||||
<div class="check-result {isErrorResult(issue) ? 'error' : isSuccessResult(issue) ? 'success' : ''}">
|
||||
<div class="message">
|
||||
{issue.message}
|
||||
</div>
|
||||
{#if isFixableError(issue)}
|
||||
<div class="operations">
|
||||
<button onclick={() => fixIssue(issue)} class="mod-cta" disabled={processing}>Fix</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
<UserDecisions>
|
||||
<Decision title="Detect and Fix CouchDB Issues" important={true} commit={testAndFixSettings} />
|
||||
</UserDecisions>
|
||||
<div class="check-results">
|
||||
<details open={!isAllSuccess}>
|
||||
<summary>
|
||||
{#if detectedIssues.length === 0}
|
||||
No checks have been performed yet.
|
||||
{:else if isAllSuccess}
|
||||
All checks passed successfully!
|
||||
{:else}
|
||||
{errorIssueCount} issue(s) detected!
|
||||
{/if}
|
||||
</summary>
|
||||
{#if detectedIssues.length > 0}
|
||||
<h3>Issue detection log:</h3>
|
||||
{#each detectedIssues as issue}
|
||||
{@render result(issue)}
|
||||
{/each}
|
||||
{/if}
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Make .check-result a CSS Grid: let .message expand and keep .operations at minimum width, aligned to the right */
|
||||
.check-results {
|
||||
/* Adjust spacing as required */
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.check-result {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto; /* message takes remaining space, operations use minimum width */
|
||||
align-items: center; /* vertically centre align */
|
||||
gap: 0.5rem 1rem;
|
||||
padding: 0rem 0.5rem;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border-left: 0.5em solid var(--interactive-accent);
|
||||
margin-bottom: 0.25lh;
|
||||
}
|
||||
.check-result.error {
|
||||
border-left: 0.5em solid var(--text-error);
|
||||
}
|
||||
.check-result.success {
|
||||
border-left: 0.5em solid var(--text-success);
|
||||
}
|
||||
|
||||
.check-result .message {
|
||||
/* Wrap long messages */
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.check-result .operations {
|
||||
/* Centre the button(s) vertically and align to the right */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* For small screens: move .operations below and stack vertically */
|
||||
@media (max-width: 520px) {
|
||||
.check-result {
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
.check-result .operations {
|
||||
justify-content: flex-start;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||
const TYPE_CANCEL = "cancelled";
|
||||
|
||||
const TYPE_BACKUP_DONE = "backup_done";
|
||||
const TYPE_BACKUP_SKIPPED = "backup_skipped";
|
||||
const TYPE_UNABLE_TO_BACKUP = "unable_to_backup";
|
||||
|
||||
type ResultTypeBackup =
|
||||
| typeof TYPE_BACKUP_DONE
|
||||
| typeof TYPE_BACKUP_SKIPPED
|
||||
| typeof TYPE_UNABLE_TO_BACKUP
|
||||
| typeof TYPE_CANCEL;
|
||||
|
||||
type ResultTypeExtra = {
|
||||
preventFetchingConfig: boolean;
|
||||
};
|
||||
type ResultType =
|
||||
| {
|
||||
backup: ResultTypeBackup;
|
||||
extra: ResultTypeExtra;
|
||||
}
|
||||
| typeof TYPE_CANCEL;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
|
||||
let backupType = $state<ResultTypeBackup>(TYPE_CANCEL);
|
||||
let confirmationCheck1 = $state(false);
|
||||
let confirmationCheck2 = $state(false);
|
||||
let confirmationCheck3 = $state(false);
|
||||
const canProceed = $derived.by(() => {
|
||||
return (
|
||||
(backupType === TYPE_BACKUP_DONE || backupType === TYPE_BACKUP_SKIPPED) &&
|
||||
confirmationCheck1 &&
|
||||
confirmationCheck2 &&
|
||||
confirmationCheck3
|
||||
);
|
||||
});
|
||||
let preventFetchingConfig = $state(false);
|
||||
|
||||
function commit() {
|
||||
setResult({
|
||||
backup: backupType,
|
||||
extra: {
|
||||
preventFetchingConfig,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Final Confirmation: Overwrite Server Data with This Device's Files" />
|
||||
<Guidance
|
||||
>This procedure will first delete all existing synchronisation data from the server. Following this, the server data
|
||||
will be completely rebuilt, using the current state of your Vault on this device (including its local database) as
|
||||
<strong>the single, authoritative master copy</strong>.</Guidance
|
||||
>
|
||||
<InfoNote>
|
||||
You should perform this operation only in exceptional circumstances, such as when the server data is completely
|
||||
corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually
|
||||
large in comparison to the Vault size.
|
||||
</InfoNote>
|
||||
<Guidance important title="⚠️ Please Confirm the Following">
|
||||
<Check
|
||||
title="I understand that all changes made on other smartphones or computers possibly could be lost."
|
||||
bind:value={confirmationCheck1}
|
||||
>
|
||||
<InfoNote>There is a way to resolve this on other devices.</InfoNote>
|
||||
<InfoNote>Of course, we can back up the data before proceeding.</InfoNote>
|
||||
</Check>
|
||||
<Check
|
||||
title="I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information."
|
||||
bind:value={confirmationCheck2}
|
||||
>
|
||||
<InfoNote>by resetting the remote, you will be informed on other devices.</InfoNote>
|
||||
</Check>
|
||||
<Check title="I understand that this action is irreversible once performed." bind:value={confirmationCheck3} />
|
||||
</Guidance>
|
||||
<hr />
|
||||
<Instruction>
|
||||
<Question>Have you created a backup before proceeding?</Question>
|
||||
<InfoNote warning>
|
||||
This is an extremely powerful operation. We strongly recommend that you copy your Vault folder to a safe
|
||||
location.
|
||||
</InfoNote>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_BACKUP_DONE} title="I have created a backup of my Vault." bind:value={backupType} />
|
||||
<Option
|
||||
selectedValue={TYPE_BACKUP_SKIPPED}
|
||||
title="I understand the risks and will proceed without a backup."
|
||||
bind:value={backupType}
|
||||
/>
|
||||
<Option
|
||||
selectedValue={TYPE_UNABLE_TO_BACKUP}
|
||||
title="I am unable to create a backup of my Vaults."
|
||||
bind:value={backupType}
|
||||
>
|
||||
<InfoNote error visible={backupType === TYPE_UNABLE_TO_BACKUP}>
|
||||
<strong
|
||||
>You should create a new synchronisation destination and rebuild your data there. <br /> After that,
|
||||
synchronise to a brand new vault on each other device with the new remote one by one.</strong
|
||||
>
|
||||
</InfoNote>
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
<Instruction>
|
||||
<ExtraItems title="Advanced">
|
||||
<Check title="Prevent fetching configuration from server" bind:value={preventFetchingConfig} />
|
||||
</ExtraItems>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title="I Understand, Overwrite Server" important disabled={!canProceed} commit={() => commit()} />
|
||||
<Decision title="Cancel" commit={() => setResult(TYPE_CANCEL)} />
|
||||
</UserDecisions>
|
||||
28
src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte
Normal file
28
src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
const TYPE_CLOSE = "close";
|
||||
type ResultType = typeof TYPE_CLOSE;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Scan QR Code" />
|
||||
<Guidance>Please follow the steps below to import settings from your existing device.</Guidance>
|
||||
<Instruction>
|
||||
<!-- <Question>How would you like to configure the connection to your server?</Question> -->
|
||||
<ol>
|
||||
<li>On this device, please keep this Vault open.</li>
|
||||
<li>On the source device, open Obsidian.</li>
|
||||
<li>On the source device, from the command palette, run the 'Show settings as a QR code' command.</li>
|
||||
<li>On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code.</li>
|
||||
</ol>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title="Close this dialog" important={true} commit={() => setResult(TYPE_CLOSE)} />
|
||||
</UserDecisions>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||
const TYPE_USE_SETUP_URI = "use-setup-uri";
|
||||
const TYPE_SCAN_QR_CODE = "scan-qr-code";
|
||||
const TYPE_CONFIGURE_MANUALLY = "configure-manually";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_SCAN_QR_CODE | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||
let proceedTitle = $derived.by(() => {
|
||||
if (userType === TYPE_USE_SETUP_URI) {
|
||||
return "Proceed with Setup URI";
|
||||
} else if (userType === TYPE_CONFIGURE_MANUALLY) {
|
||||
return "I know my server details, let me enter them";
|
||||
} else if (userType === TYPE_SCAN_QR_CODE) {
|
||||
return "Scan the QR code displayed on an active device using this device's camera.";
|
||||
} else {
|
||||
return "Please select an option to proceed";
|
||||
}
|
||||
});
|
||||
const canProceed = $derived.by(() => {
|
||||
return userType === TYPE_USE_SETUP_URI || userType === TYPE_CONFIGURE_MANUALLY || userType === TYPE_SCAN_QR_CODE;
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Device Setup Method" />
|
||||
<Guidance>You are adding this device to an existing synchronisation setup.</Guidance>
|
||||
<Instruction>
|
||||
<Question>Please select a method to import the settings from another device.</Question>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_USE_SETUP_URI} title="Use a Setup URI (Recommended)" bind:value={userType}>
|
||||
Paste the Setup URI generated from one of your active devices.
|
||||
</Option>
|
||||
<Option selectedValue={TYPE_SCAN_QR_CODE} title="Scan a QR Code (Recommended for mobile)" bind:value={userType}>
|
||||
Scan the QR code displayed on an active device using this device's camera.
|
||||
</Option>
|
||||
<Option
|
||||
selectedValue={TYPE_CONFIGURE_MANUALLY}
|
||||
title="Enter the server information manually"
|
||||
bind:value={userType}
|
||||
>
|
||||
Configure the same server information as your other devices again, manually, very advanced users only.
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||
<Decision title="Cancel" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||
import Check from "@/lib/src/UI/components/Check.svelte";
|
||||
const TYPE_USE_SETUP_URI = "use-setup-uri";
|
||||
const TYPE_CONFIGURE_MANUALLY = "configure-manually";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||
let proceedTitle = $derived.by(() => {
|
||||
if (userType === TYPE_USE_SETUP_URI) {
|
||||
return "Proceed with Setup URI";
|
||||
} else if (userType === TYPE_CONFIGURE_MANUALLY) {
|
||||
return "I know my server details, let me enter them";
|
||||
} else {
|
||||
return "Please select an option to proceed";
|
||||
}
|
||||
});
|
||||
const canProceed = $derived.by(() => {
|
||||
return userType === TYPE_USE_SETUP_URI || userType === TYPE_CONFIGURE_MANUALLY;
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Connection Method" />
|
||||
<Guidance>We will now proceed with the server configuration.</Guidance>
|
||||
<Instruction>
|
||||
<Question>How would you like to configure the connection to your server?</Question>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_USE_SETUP_URI} title="Use a Setup URI (Recommended)" bind:value={userType}>
|
||||
A Setup URI is a single string of text containing your server address and authentication details. Using a
|
||||
URI, if one was generated by your server installation script, provides a simple and secure configuration.
|
||||
</Option>
|
||||
<Option
|
||||
selectedValue={TYPE_CONFIGURE_MANUALLY}
|
||||
title="Enter the server information manually"
|
||||
bind:value={userType}
|
||||
>
|
||||
This is an advanced option for users who do not have a URI or who wish to configure detailed settings.
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||
<Decision title="Cancel" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
56
src/modules/features/SetupWizard/dialogs/SetupRemote.svelte
Normal file
56
src/modules/features/SetupWizard/dialogs/SetupRemote.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import Question from "@/lib/src/UI/components/Question.svelte";
|
||||
import Option from "@/lib/src/UI/components/Option.svelte";
|
||||
import Options from "@/lib/src/UI/components/Options.svelte";
|
||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
const TYPE_COUCHDB = "couchdb";
|
||||
const TYPE_BUCKET = "bucket";
|
||||
const TYPE_P2P = "p2p";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_COUCHDB | typeof TYPE_BUCKET | typeof TYPE_P2P | typeof TYPE_CANCELLED;
|
||||
type Props = {
|
||||
setResult: (result: ResultType) => void;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
let userType = $state<ResultType>(TYPE_CANCELLED);
|
||||
let proceedTitle = $derived.by(() => {
|
||||
if (userType === TYPE_COUCHDB) {
|
||||
return "Continue to CouchDB setup";
|
||||
} else if (userType === TYPE_BUCKET) {
|
||||
return "Continue to S3/MinIO/R2 setup";
|
||||
} else if (userType === TYPE_P2P) {
|
||||
return "Continue to Peer-to-Peer only setup";
|
||||
} else {
|
||||
return "Please select an option to proceed";
|
||||
}
|
||||
});
|
||||
const canProceed = $derived.by(() => {
|
||||
return userType === TYPE_COUCHDB || userType === TYPE_BUCKET || userType === TYPE_P2P;
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Enter Server Information" />
|
||||
<Instruction>
|
||||
<Question>Please select the type of server to which you are connecting.</Question>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_COUCHDB} title="CouchDB" bind:value={userType}>
|
||||
This is the most suitable synchronisation method for the design. All functions are available. You must have
|
||||
set up a CouchDB instance.
|
||||
</Option>
|
||||
<Option selectedValue={TYPE_BUCKET} title="S3/MinIO/R2 Object Storage" bind:value={userType}>
|
||||
Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.
|
||||
</Option>
|
||||
<Option selectedValue={TYPE_P2P} title="Peer-to-Peer only" bind:value={userType}>
|
||||
This is an experimental feature enabling direct synchronisation between devices. No server is required, but
|
||||
both devices must be online at the same time for synchronisation to occur, and some features may be limited.
|
||||
Internet connection is only required to signalling (detecting peers) and not for data transfer.
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
<Decision title={proceedTitle} important={canProceed} disabled={!canProceed} commit={() => setResult(userType)} />
|
||||
<Decision title="No, please take me back" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
@@ -0,0 +1,241 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||
import {
|
||||
type BucketSyncSetting,
|
||||
type ObsidianLiveSyncSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
RemoteTypes,
|
||||
} from "../../../../lib/src/common/types";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||
import { copyTo, pickBucketSyncSettings } from "../../../../lib/src/common/utils";
|
||||
|
||||
const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS);
|
||||
|
||||
let syncSetting = $state<BucketSyncSetting>({ ...default_setting });
|
||||
|
||||
type ResultType = typeof TYPE_CANCELLED | BucketSyncSetting;
|
||||
type Props = GuestDialogProps<ResultType, BucketSyncSetting>;
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (getInitialData) {
|
||||
const initialData = getInitialData();
|
||||
if (initialData) {
|
||||
copyTo(initialData, syncSetting);
|
||||
}
|
||||
}
|
||||
});
|
||||
let error = $state("");
|
||||
const context = getObsidianDialogContext();
|
||||
const isEndpointSecure = $derived.by(() => {
|
||||
return syncSetting.endpoint.trim().toLowerCase().startsWith("https://");
|
||||
});
|
||||
const isEndpointInsecure = $derived.by(() => {
|
||||
return syncSetting.endpoint.trim().toLowerCase().startsWith("http://");
|
||||
});
|
||||
const isEndpointSupplied = $derived.by(() => {
|
||||
return isEndpointInsecure || isEndpointSecure;
|
||||
});
|
||||
const canProceed = $derived.by(() => {
|
||||
return (
|
||||
syncSetting.accessKey.trim() !== "" &&
|
||||
syncSetting.secretKey.trim() !== "" &&
|
||||
syncSetting.bucket.trim() !== "" &&
|
||||
syncSetting.endpoint.trim() !== "" &&
|
||||
syncSetting.region.trim() !== "" &&
|
||||
isEndpointSupplied
|
||||
);
|
||||
});
|
||||
|
||||
function generateSetting() {
|
||||
const connSetting: BucketSyncSetting = {
|
||||
...syncSetting,
|
||||
};
|
||||
const trialSettings: BucketSyncSetting = {
|
||||
...connSetting,
|
||||
};
|
||||
|
||||
const trialRemoteSetting: ObsidianLiveSyncSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...PREFERRED_JOURNAL_SYNC,
|
||||
remoteType: RemoteTypes.REMOTE_MINIO,
|
||||
...trialSettings,
|
||||
};
|
||||
return trialRemoteSetting;
|
||||
}
|
||||
|
||||
let processing = $state(false);
|
||||
async function checkConnection() {
|
||||
try {
|
||||
processing = true;
|
||||
const trialRemoteSetting = generateSetting();
|
||||
const replicator = await context.services.replicator.getNewReplicator(trialRemoteSetting);
|
||||
if (!replicator) {
|
||||
return "Failed to create replicator instance.";
|
||||
}
|
||||
try {
|
||||
const result = await replicator.tryConnectRemote(trialRemoteSetting, false);
|
||||
if (result) {
|
||||
return "";
|
||||
} else {
|
||||
return "Failed to connect to the server. Please check your settings.";
|
||||
}
|
||||
} catch (e) {
|
||||
return `Failed to connect to the server: ${e}`;
|
||||
}
|
||||
} finally {
|
||||
processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndCommit() {
|
||||
error = "";
|
||||
try {
|
||||
error = (await checkConnection()) || "";
|
||||
if (!error) {
|
||||
const setting = generateSetting();
|
||||
setResult(pickBucketSyncSettings(setting));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
error = `Error during connection test: ${e}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
function commit() {
|
||||
const setting = pickBucketSyncSettings(generateSetting());
|
||||
setResult(setting);
|
||||
}
|
||||
function cancel() {
|
||||
setResult(TYPE_CANCELLED);
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogHeader title="S3/MinIO/R2 Configuration" />
|
||||
<Guidance>Please enter the details required to connect to your S3/MinIO/R2 compatible object storage service.</Guidance>
|
||||
<InputRow label="Endpoint URL">
|
||||
<input
|
||||
type="text"
|
||||
name="s3-endpoint"
|
||||
placeholder="https://s3.amazonaws.com"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
required
|
||||
pattern="^https?://.+"
|
||||
bind:value={syncSetting.endpoint}
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote warning visible={isEndpointInsecure}>We can use only Secure (HTTPS) connections on Obsidian Mobile.</InfoNote>
|
||||
|
||||
<InputRow label="Access Key ID">
|
||||
<input
|
||||
type="text"
|
||||
name="s3-access-key-id"
|
||||
placeholder="Enter your Access Key ID"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
required
|
||||
bind:value={syncSetting.accessKey}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InputRow label="Secret Access Key">
|
||||
<Password
|
||||
name="s3-secret-access-key"
|
||||
placeholder="Enter your Secret Access Key"
|
||||
required
|
||||
bind:value={syncSetting.secretKey}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow label="Bucket Name">
|
||||
<input
|
||||
type="text"
|
||||
name="s3-bucket-name"
|
||||
placeholder="Enter your Bucket Name"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
required
|
||||
bind:value={syncSetting.bucket}
|
||||
/></InputRow
|
||||
>
|
||||
<InputRow label="Region">
|
||||
<input
|
||||
type="text"
|
||||
name="s3-region"
|
||||
placeholder="Enter your Region (e.g., us-east-1, auto for R2)"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
bind:value={syncSetting.region}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow label="Use Path-Style Access">
|
||||
<input type="checkbox" name="s3-use-path-style" bind:checked={syncSetting.forcePathStyle} />
|
||||
</InputRow>
|
||||
|
||||
<InputRow label="Folder Prefix">
|
||||
<input
|
||||
type="text"
|
||||
name="s3-folder-prefix"
|
||||
placeholder="Enter a folder prefix (optional)"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
bind:value={syncSetting.bucketPrefix}
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote>
|
||||
If you want to store the data in a specific folder within the bucket, you can specify a folder prefix here.
|
||||
Otherwise, leave it blank to store data at the root of the bucket.
|
||||
</InfoNote>
|
||||
<InputRow label="Use internal API">
|
||||
<input type="checkbox" name="s3-use-internal-api" bind:checked={syncSetting.useCustomRequestHandler} />
|
||||
</InputRow>
|
||||
<InfoNote>
|
||||
If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate
|
||||
with the S3 server. Not compliant with web standards, but works. Note that this might break in future Obsidian
|
||||
versions.
|
||||
</InfoNote>
|
||||
|
||||
<ExtraItems title="Advanced Settings">
|
||||
<InputRow label="Custom Headers">
|
||||
<textarea
|
||||
name="bucket-custom-headers"
|
||||
placeholder="e.g., x-example-header: value\n another-header: value2"
|
||||
bind:value={syncSetting.bucketCustomHeaders}
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</InputRow>
|
||||
</ExtraItems>
|
||||
|
||||
<InfoNote error visible={error !== ""}>
|
||||
{error}
|
||||
</InfoNote>
|
||||
|
||||
{#if processing}
|
||||
Checking connection... Please wait.
|
||||
{:else}
|
||||
<UserDecisions>
|
||||
<Decision title="Test Settings and Continue" important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||
<Decision title="Continue anyway" commit={() => commit()} />
|
||||
<Decision title="Cancel" commit={() => cancel()} />
|
||||
</UserDecisions>
|
||||
{/if}
|
||||
@@ -0,0 +1,284 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
PREFERRED_SETTING_CLOUDANT,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type CouchDBConnection,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "../../../../lib/src/common/types";
|
||||
import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb";
|
||||
|
||||
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||
import { onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { copyTo, pickCouchDBSyncSettings } from "../../../../lib/src/common/utils";
|
||||
import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte";
|
||||
|
||||
const default_setting = pickCouchDBSyncSettings(DEFAULT_SETTINGS);
|
||||
|
||||
let syncSetting = $state<CouchDBConnection>({ ...default_setting });
|
||||
type ResultType = typeof TYPE_CANCELLED | CouchDBConnection;
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type Props = GuestDialogProps<ResultType, CouchDBConnection>;
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
onMount(() => {
|
||||
if (getInitialData) {
|
||||
const initialData = getInitialData();
|
||||
if (initialData) {
|
||||
copyTo(initialData, syncSetting);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let error = $state("");
|
||||
const context = getObsidianDialogContext();
|
||||
|
||||
function generateSetting() {
|
||||
const connSetting: CouchDBConnection = {
|
||||
...syncSetting,
|
||||
};
|
||||
const trialSettings: CouchDBConnection = {
|
||||
...connSetting,
|
||||
// ...encryptionSettings,
|
||||
};
|
||||
const preferredSetting = isCloudantURI(syncSetting.couchDB_URI)
|
||||
? PREFERRED_SETTING_CLOUDANT
|
||||
: PREFERRED_SETTING_SELF_HOSTED;
|
||||
const trialRemoteSetting: ObsidianLiveSyncSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...preferredSetting,
|
||||
remoteType: RemoteTypes.REMOTE_COUCHDB,
|
||||
...trialSettings,
|
||||
};
|
||||
return trialRemoteSetting;
|
||||
}
|
||||
let processing = $state(false);
|
||||
async function checkConnection() {
|
||||
try {
|
||||
processing = true;
|
||||
const trialRemoteSetting = generateSetting();
|
||||
const replicator = await context.services.replicator.getNewReplicator(trialRemoteSetting);
|
||||
if (!replicator) {
|
||||
return "Failed to create replicator instance.";
|
||||
}
|
||||
try {
|
||||
const result = await replicator.tryConnectRemote(trialRemoteSetting, false);
|
||||
if (result) {
|
||||
return "";
|
||||
} else {
|
||||
return "Failed to connect to the server. Please check your settings.";
|
||||
}
|
||||
} catch (e) {
|
||||
return `Failed to connect to the server: ${e}`;
|
||||
}
|
||||
} finally {
|
||||
processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndCommit() {
|
||||
error = "";
|
||||
try {
|
||||
error = (await checkConnection()) || "";
|
||||
if (!error) {
|
||||
const setting = generateSetting();
|
||||
setResult(pickCouchDBSyncSettings(setting));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
error = `Error during connection test: ${e}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
function commit() {
|
||||
const setting = pickCouchDBSyncSettings(generateSetting());
|
||||
setResult(setting);
|
||||
}
|
||||
function cancel() {
|
||||
setResult(TYPE_CANCELLED);
|
||||
}
|
||||
|
||||
// const isURICloudant = $derived.by(() => {
|
||||
// return syncSetting.couchDB_URI && isCloudantURI(syncSetting.couchDB_URI);
|
||||
// });
|
||||
// const isURISelfHosted = $derived.by(() => {
|
||||
// return syncSetting.couchDB_URI && !isCloudantURI(syncSetting.couchDB_URI);
|
||||
// });
|
||||
// const isURISecure = $derived.by(() => {
|
||||
// return syncSetting.couchDB_URI && syncSetting.couchDB_URI.startsWith("https://");
|
||||
// });
|
||||
const isURIInsecure = $derived.by(() => {
|
||||
return !!(syncSetting.couchDB_URI && syncSetting.couchDB_URI.startsWith("http://"));
|
||||
});
|
||||
const isUseJWT = $derived.by(() => {
|
||||
return syncSetting.useJWT;
|
||||
});
|
||||
const canProceed = $derived.by(() => {
|
||||
return (
|
||||
syncSetting.couchDB_URI.trim().length > 0 &&
|
||||
syncSetting.couchDB_USER.trim().length > 0 &&
|
||||
syncSetting.couchDB_PASSWORD.trim().length > 0 &&
|
||||
syncSetting.couchDB_DBNAME.trim().length > 0 &&
|
||||
(isUseJWT ? syncSetting.jwtKey.trim().length > 0 : true)
|
||||
);
|
||||
});
|
||||
const testSettings = $derived.by(() => {
|
||||
return generateSetting();
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="CouchDB Configuration" />
|
||||
<Guidance>Please enter the CouchDB server information below.</Guidance>
|
||||
<InputRow label="URL">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-url"
|
||||
placeholder="https://example.com"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
bind:value={syncSetting.couchDB_URI}
|
||||
required
|
||||
pattern="^https?://.+"
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote warning visible={isURIInsecure}>We can use only Secure (HTTPS) connections on Obsidian Mobile.</InfoNote>
|
||||
<InputRow label="Username">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-username"
|
||||
placeholder="Enter your username"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
required
|
||||
bind:value={syncSetting.couchDB_USER}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow label="Password">
|
||||
<Password
|
||||
name="couchdb-password"
|
||||
placeholder="Enter your password"
|
||||
bind:value={syncSetting.couchDB_PASSWORD}
|
||||
required
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InputRow label="Database Name">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-database"
|
||||
placeholder="Enter your database name"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
required
|
||||
pattern="^[a-z0-9][a-z0-9_]*$"
|
||||
bind:value={syncSetting.couchDB_DBNAME}
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote>
|
||||
You cannot use capital letters, spaces, or special characters in the database name. And not allowed to start with an
|
||||
underscore (_).
|
||||
</InfoNote>
|
||||
<InputRow label="Use Internal API">
|
||||
<input type="checkbox" name="couchdb-use-internal-api" bind:checked={syncSetting.useRequestAPI} />
|
||||
</InputRow>
|
||||
<InfoNote>
|
||||
If you cannot avoid CORS issues, you might want to try this option. It uses Obsidian's internal API to communicate
|
||||
with the CouchDB server. Not compliant with web standards, but works. Note that this might break in future Obsidian
|
||||
versions.
|
||||
</InfoNote>
|
||||
|
||||
<ExtraItems title="Advanced Settings">
|
||||
<InputRow label="Custom Headers">
|
||||
<textarea
|
||||
name="couchdb-custom-headers"
|
||||
placeholder="e.g., x-example-header: value\n another-header: value2"
|
||||
bind:value={syncSetting.couchDB_CustomHeaders}
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
rows="4"
|
||||
></textarea>
|
||||
</InputRow>
|
||||
</ExtraItems>
|
||||
<ExtraItems title="Experimental Settings">
|
||||
<InputRow label="Use JWT Authentication">
|
||||
<input type="checkbox" name="couchdb-use-jwt" bind:checked={syncSetting.useJWT} />
|
||||
</InputRow>
|
||||
<InputRow label="JWT Algorithm">
|
||||
<select bind:value={syncSetting.jwtAlgorithm} disabled={!isUseJWT}>
|
||||
<option value="HS256">HS256</option>
|
||||
<option value="HS512">HS512</option>
|
||||
<option value="ES256">ES256</option>
|
||||
<option value="ES512">ES512</option>
|
||||
</select>
|
||||
</InputRow>
|
||||
<InputRow label="JWT Expiration Duration (seconds)">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-jwt-exp-duration"
|
||||
placeholder="0"
|
||||
bind:value={() => `${syncSetting.jwtExpDuration}`, (v) => (syncSetting.jwtExpDuration = parseInt(v) || 0)}
|
||||
disabled={!isUseJWT}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow label="JWT Key">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-jwt-key"
|
||||
placeholder="Enter your JWT secret or private key"
|
||||
bind:value={syncSetting.jwtKey}
|
||||
disabled={!isUseJWT}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow label="JWT Key ID (kid)">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-jwt-kid"
|
||||
placeholder="Enter your JWT Key ID (optional)"
|
||||
bind:value={syncSetting.jwtKid}
|
||||
disabled={!isUseJWT}
|
||||
/>
|
||||
</InputRow>
|
||||
<InputRow label="JWT Subject (sub)">
|
||||
<input
|
||||
type="text"
|
||||
name="couchdb-jwt-sub"
|
||||
placeholder="Enter your JWT Subject (optional)"
|
||||
bind:value={syncSetting.jwtSub}
|
||||
disabled={!isUseJWT}
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote warning>
|
||||
JWT (JSON Web Token) authentication allows you to securely authenticate with the CouchDB server using tokens.
|
||||
Ensure that your CouchDB server is configured to accept JWTs and that the provided key and settings match the
|
||||
server's configuration. Incidentally, I have not verified it very thoroughly.
|
||||
</InfoNote>
|
||||
</ExtraItems>
|
||||
|
||||
<PanelCouchDBCheck trialRemoteSetting={testSettings}></PanelCouchDBCheck>
|
||||
<hr />
|
||||
|
||||
<InfoNote error visible={error !== ""}>
|
||||
{error}
|
||||
</InfoNote>
|
||||
|
||||
{#if processing}
|
||||
Checking connection... Please wait.
|
||||
{:else}
|
||||
<UserDecisions>
|
||||
<Decision title="Test Settings and Continue" important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||
<Decision title="Continue anyway" commit={() => commit()} />
|
||||
<Decision title="Cancel" commit={() => cancel()} />
|
||||
</UserDecisions>
|
||||
{/if}
|
||||
123
src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte
Normal file
123
src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte
Normal file
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte";
|
||||
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
E2EEAlgorithmNames,
|
||||
E2EEAlgorithms,
|
||||
type EncryptionSettings,
|
||||
} from "../../../../lib/src/common/types";
|
||||
import { onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { copyTo, pickEncryptionSettings } from "../../../../lib/src/common/utils";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_CANCELLED | EncryptionSettings;
|
||||
type Props = GuestDialogProps<ResultType, EncryptionSettings>;
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
let default_encryption: EncryptionSettings = {
|
||||
encrypt: true,
|
||||
passphrase: "",
|
||||
E2EEAlgorithm: DEFAULT_SETTINGS.E2EEAlgorithm,
|
||||
usePathObfuscation: true,
|
||||
} as EncryptionSettings;
|
||||
|
||||
let encryptionSettings = $state<EncryptionSettings>({ ...default_encryption });
|
||||
|
||||
onMount(() => {
|
||||
if (getInitialData) {
|
||||
const initialData = getInitialData();
|
||||
if (initialData) {
|
||||
copyTo(initialData, encryptionSettings);
|
||||
}
|
||||
}
|
||||
});
|
||||
let e2eeValid = $derived.by(() => {
|
||||
if (!encryptionSettings.encrypt) return true;
|
||||
return encryptionSettings.passphrase.trim().length >= 1;
|
||||
});
|
||||
|
||||
function commit() {
|
||||
setResult(pickEncryptionSettings(encryptionSettings));
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogHeader title="End-to-End Encryption" />
|
||||
<Guidance>Please configure your end-to-end encryption settings.</Guidance>
|
||||
<InputRow label="End-to-End Encryption">
|
||||
<input type="checkbox" bind:checked={encryptionSettings.encrypt} />
|
||||
<Password
|
||||
name="e2ee-passphrase"
|
||||
placeholder="Enter your passphrase"
|
||||
bind:value={encryptionSettings.passphrase}
|
||||
disabled={!encryptionSettings.encrypt}
|
||||
required={encryptionSettings.encrypt}
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote title="Strongly Recommended">
|
||||
Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote
|
||||
server. This means that even if someone gains access to the server, they won't be able to read your data without the
|
||||
passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices.
|
||||
<br />
|
||||
Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch
|
||||
to other methods and connect to a remote server in the future.
|
||||
</InfoNote>
|
||||
<InfoNote warning>
|
||||
This setting must be the same even when connecting to multiple synchronisation destinations.
|
||||
</InfoNote>
|
||||
<InputRow label="Obfuscate Properties">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={encryptionSettings.usePathObfuscation}
|
||||
disabled={!encryptionSettings.encrypt}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InfoNote>
|
||||
Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of
|
||||
security by making it harder to identify the structure and names of your files and folders on the remote server.
|
||||
This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your
|
||||
data.
|
||||
</InfoNote>
|
||||
|
||||
<ExtraItems title="Advanced">
|
||||
<InputRow label="Encryption Algorithm">
|
||||
<select bind:value={encryptionSettings.E2EEAlgorithm} disabled={!encryptionSettings.encrypt}>
|
||||
{#each Object.values(E2EEAlgorithms) as alg}
|
||||
<option value={alg}>{E2EEAlgorithmNames[alg] ?? alg}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</InputRow>
|
||||
<InfoNote>
|
||||
In most cases, you should stick with the default algorithm ({E2EEAlgorithmNames[
|
||||
DEFAULT_SETTINGS.E2EEAlgorithm
|
||||
]}), This setting is only required if you have an existing Vault encrypted in a different format.
|
||||
</InfoNote>
|
||||
<InfoNote warning>
|
||||
Changing the encryption algorithm will prevent access to any data previously encrypted with a different
|
||||
algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your
|
||||
data.
|
||||
</InfoNote>
|
||||
</ExtraItems>
|
||||
|
||||
<InfoNote warning>
|
||||
<p>
|
||||
Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process
|
||||
actually commences. This is a security measure designed to protect your data.
|
||||
</p>
|
||||
<p>
|
||||
Therefore, we ask that you exercise extreme caution when configuring server information manually. If an
|
||||
incorrect passphrase is entered, the data on the server will become corrupted. <br /><br />
|
||||
Please understand that this is intended behaviour.
|
||||
</p>
|
||||
</InfoNote>
|
||||
|
||||
<UserDecisions>
|
||||
<Decision title="Proceed" important disabled={!e2eeValid} commit={() => commit()} />
|
||||
<Decision title="Cancel" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
255
src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte
Normal file
255
src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte
Normal file
@@ -0,0 +1,255 @@
|
||||
<script lang="ts">
|
||||
// import { delay } from "octagonal-wheels/promises";
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||
import { PouchDB } from "../../../../lib/src/pouchdb/pouchdb-browser";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
P2P_DEFAULT_SETTINGS,
|
||||
PREFERRED_BASE,
|
||||
RemoteTypes,
|
||||
type EntryDoc,
|
||||
type ObsidianLiveSyncSettings,
|
||||
type P2PConnectionInfo,
|
||||
type P2PSyncSetting,
|
||||
} from "../../../../lib/src/common/types";
|
||||
|
||||
import { TrysteroReplicator } from "../../../../lib/src/replication/trystero/TrysteroReplicator";
|
||||
import type { ReplicatorHostEnv } from "../../../../lib/src/replication/trystero/types";
|
||||
import { copyTo, pickP2PSyncSettings, type SimpleStore } from "../../../../lib/src/common/utils";
|
||||
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
|
||||
import { onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
|
||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../../lib/src/common/types";
|
||||
|
||||
const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS);
|
||||
let syncSetting = $state<P2PConnectionInfo>({ ...default_setting });
|
||||
|
||||
const context = getObsidianDialogContext();
|
||||
let error = $state("");
|
||||
let devicePeerId = $state("");
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type SettingInfo = {
|
||||
info: P2PConnectionInfo;
|
||||
devicePeerId: string;
|
||||
};
|
||||
type ResultType = typeof TYPE_CANCELLED | SettingInfo;
|
||||
type Props = GuestDialogProps<ResultType, P2PSyncSetting>;
|
||||
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
onMount(() => {
|
||||
if (getInitialData) {
|
||||
const initialData = getInitialData();
|
||||
if (initialData) {
|
||||
copyTo(initialData, syncSetting);
|
||||
}
|
||||
if (context.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME)) {
|
||||
devicePeerId = context.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) as string;
|
||||
}
|
||||
}
|
||||
});
|
||||
function generateSetting() {
|
||||
const connSetting: P2PSyncSetting = {
|
||||
// remoteType: ",
|
||||
...P2P_DEFAULT_SETTINGS,
|
||||
...syncSetting,
|
||||
P2P_Enabled: true,
|
||||
};
|
||||
const trialSettings: P2PSyncSetting = {
|
||||
...connSetting,
|
||||
};
|
||||
const trialRemoteSetting: ObsidianLiveSyncSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...PREFERRED_BASE,
|
||||
remoteType: RemoteTypes.REMOTE_P2P,
|
||||
...trialSettings,
|
||||
};
|
||||
return trialRemoteSetting;
|
||||
}
|
||||
|
||||
async function checkConnection() {
|
||||
try {
|
||||
processing = true;
|
||||
const trialRemoteSetting = generateSetting();
|
||||
const map = new Map<string, string>();
|
||||
const store = {
|
||||
get: (key: string) => {
|
||||
return Promise.resolve(map.get(key) || null);
|
||||
},
|
||||
set: (key: string, value: any) => {
|
||||
map.set(key, value);
|
||||
return Promise.resolve();
|
||||
},
|
||||
delete: (key: string) => {
|
||||
map.delete(key);
|
||||
return Promise.resolve();
|
||||
},
|
||||
keys: () => {
|
||||
return Promise.resolve(Array.from(map.keys()));
|
||||
},
|
||||
} as SimpleStore<any>;
|
||||
|
||||
const dummyPouch = new PouchDB<EntryDoc>("dummy");
|
||||
const env: ReplicatorHostEnv = {
|
||||
settings: trialRemoteSetting,
|
||||
processReplicatedDocs: async (docs: any[]) => {
|
||||
return;
|
||||
},
|
||||
confirm: context.plugin.confirm,
|
||||
db: dummyPouch,
|
||||
simpleStore: store,
|
||||
deviceName: devicePeerId || "unnamed-device",
|
||||
platform: "setup-wizard",
|
||||
};
|
||||
const replicator = new TrysteroReplicator(env);
|
||||
try {
|
||||
await replicator.setOnSetup();
|
||||
await replicator.allowReconnection();
|
||||
await replicator.open();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// await delay(1000);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Logger(`Checking known advertisements... (${i})`, LOG_LEVEL_INFO);
|
||||
if (replicator.knownAdvertisements.length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// context.holdingSettings = trialRemoteSetting;
|
||||
|
||||
if (replicator.knownAdvertisements.length === 0) {
|
||||
return "Your settings seem correct, but no other peers were found.";
|
||||
}
|
||||
return "";
|
||||
} catch (e) {
|
||||
return `Failed to connect to other peers: ${e}`;
|
||||
} finally {
|
||||
try {
|
||||
replicator.close();
|
||||
dummyPouch.destroy();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
processing = false;
|
||||
}
|
||||
}
|
||||
function setDefaultRelay() {
|
||||
syncSetting.P2P_relays = P2P_DEFAULT_SETTINGS.P2P_relays;
|
||||
}
|
||||
|
||||
let processing = $state(false);
|
||||
function generateDefaultGroupId() {
|
||||
const randomValues = new Uint16Array(4);
|
||||
crypto.getRandomValues(randomValues);
|
||||
const MAX_UINT16 = 65536;
|
||||
const a = Math.floor((randomValues[0] / MAX_UINT16) * 1000);
|
||||
const b = Math.floor((randomValues[1] / MAX_UINT16) * 1000);
|
||||
const c = Math.floor((randomValues[2] / MAX_UINT16) * 1000);
|
||||
const d_range = 36 * 36 * 36;
|
||||
const d = Math.floor((randomValues[3] / MAX_UINT16) * d_range);
|
||||
syncSetting.P2P_roomID = `${a.toString().padStart(3, "0")}-${b
|
||||
.toString()
|
||||
.padStart(3, "0")}-${c.toString().padStart(3, "0")}-${d.toString(36).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
async function checkAndCommit() {
|
||||
error = "";
|
||||
try {
|
||||
error = (await checkConnection()) || "";
|
||||
if (!error) {
|
||||
const setting = generateSetting();
|
||||
setResult({
|
||||
info: pickP2PSyncSettings(setting),
|
||||
devicePeerId: devicePeerId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
error = `Error during connection test: ${e}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
function commit() {
|
||||
const setting = pickP2PSyncSettings(generateSetting());
|
||||
setResult({
|
||||
info: setting,
|
||||
devicePeerId: devicePeerId,
|
||||
});
|
||||
}
|
||||
function cancel() {
|
||||
setResult(TYPE_CANCELLED);
|
||||
}
|
||||
const canProceed = $derived.by(() => {
|
||||
return (
|
||||
syncSetting.P2P_relays.trim() !== "" &&
|
||||
syncSetting.P2P_roomID.trim() !== "" &&
|
||||
syncSetting.P2P_passphrase.trim() !== "" &&
|
||||
devicePeerId.trim() !== ""
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="P2P Configuration" />
|
||||
<Guidance>Please enter the Peer-to-Peer Synchronisation information below.</Guidance>
|
||||
<InputRow label="Relay URL">
|
||||
<input
|
||||
type="text"
|
||||
name="p2p-relay-url"
|
||||
placeholder="Enter the Relay URL)"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
bind:value={syncSetting.P2P_relays}
|
||||
/>
|
||||
<button class="button" onclick={() => setDefaultRelay()}>Use vrtmrz's relay</button>
|
||||
</InputRow>
|
||||
<InputRow label="Group ID">
|
||||
<input
|
||||
type="text"
|
||||
name="p2p-room-id"
|
||||
placeholder="123-456-789-abc"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
bind:value={syncSetting.P2P_roomID}
|
||||
/>
|
||||
<button class="button" onclick={() => generateDefaultGroupId()}>Generate Random ID</button>
|
||||
</InputRow>
|
||||
<InputRow label="Passphrase">
|
||||
<Password name="p2p-password" placeholder="Enter your passphrase" bind:value={syncSetting.P2P_passphrase} />
|
||||
</InputRow>
|
||||
<InfoNote>
|
||||
The Group ID and passphrase are used to identify your group of devices. Make sure to use the same Group ID and
|
||||
passphrase on all devices you want to synchronise.<br />
|
||||
Note that the Group ID is not limited to the generated format; you can use any string as the Group ID.
|
||||
</InfoNote>
|
||||
<InputRow label="Device Peer ID">
|
||||
<input
|
||||
type="text"
|
||||
name="p2p-device-peer-id"
|
||||
placeholder="main-iphone16"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
bind:value={devicePeerId}
|
||||
/>
|
||||
</InputRow>
|
||||
|
||||
<InfoNote error visible={error !== ""}>
|
||||
{error}
|
||||
</InfoNote>
|
||||
{#if processing}
|
||||
Checking connection... Please wait.
|
||||
{:else}
|
||||
<UserDecisions>
|
||||
<Decision title="Test Settings and Continue" important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||
<Decision title="Continue anyway" commit={() => commit()} />
|
||||
<Decision title="Cancel" commit={() => cancel()} />
|
||||
</UserDecisions>
|
||||
{/if}
|
||||
96
src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte
Normal file
96
src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { configURIBase } from "../../../../common/types";
|
||||
import type { ObsidianLiveSyncSettings } from "../../../../lib/src/common/types";
|
||||
import DialogHeader from "@/lib/src/UI/components/DialogHeader.svelte";
|
||||
import Guidance from "@/lib/src/UI/components/Guidance.svelte";
|
||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/lib/src/UI/components/InfoNote.svelte";
|
||||
import InputRow from "@/lib/src/UI/components/InputRow.svelte";
|
||||
import Password from "@/lib/src/UI/components/Password.svelte";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import { decryptString } from "../../../../lib/src/encryption/stringEncryption.ts";
|
||||
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog.ts";
|
||||
const TYPE_CANCELLED = "cancelled";
|
||||
type ResultType = typeof TYPE_CANCELLED | ObsidianLiveSyncSettings;
|
||||
type Props = GuestDialogProps<ResultType, string>;
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
|
||||
let setupURI = $state("");
|
||||
let passphrase = $state("");
|
||||
let error = $state("");
|
||||
onMount(() => {
|
||||
if (getInitialData) {
|
||||
const initialURI = getInitialData();
|
||||
if (initialURI) {
|
||||
setupURI = initialURI;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const seemsValid = $derived.by(() => setupURI.startsWith(configURIBase));
|
||||
async function processSetupURI() {
|
||||
error = "";
|
||||
if (!seemsValid) return;
|
||||
if (!passphrase) {
|
||||
error = "Passphrase is required.";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const settingPieces = setupURI.substring(configURIBase.length);
|
||||
const encodedConfig = decodeURIComponent(settingPieces);
|
||||
const newConf = (await JSON.parse(
|
||||
await decryptString(encodedConfig, passphrase)
|
||||
)) as ObsidianLiveSyncSettings;
|
||||
setResult(newConf);
|
||||
// Logger("Settings imported successfully", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
} catch (e) {
|
||||
error = "Failed to parse Setup-URI.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
async function canProceed() {
|
||||
return (await processSetupURI()) ?? false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Enter Setup URI" />
|
||||
<Guidance
|
||||
>Please enter the Setup URI that was generated during server installation or on another device, along with the vault
|
||||
passphrase.<br />
|
||||
Note that you can generate a new Setup URI by running the "Copy settings as a new Setup URI" command in the command palette.</Guidance
|
||||
>
|
||||
|
||||
<InputRow label="Setup-URI">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="obsidian://setuplivesync?settings=...."
|
||||
bind:value={setupURI}
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
required
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote visible={seemsValid}>The Setup-URI is valid and ready to use.</InfoNote>
|
||||
<InfoNote warning visible={!seemsValid && setupURI.trim() != ""}>
|
||||
The Setup-URI does not appear to be valid. Please check that you have copied it correctly.
|
||||
</InfoNote>
|
||||
<InputRow label="Passphrase">
|
||||
<Password placeholder="Enter your passphrase" bind:value={passphrase} required />
|
||||
</InputRow>
|
||||
<InfoNote error visible={error.trim() != ""}>
|
||||
{error}
|
||||
</InfoNote>
|
||||
|
||||
<UserDecisions>
|
||||
<Decision
|
||||
title="Test Settings and Continue"
|
||||
important={true}
|
||||
disabled={!canProceed}
|
||||
commit={() => processSetupURI()}
|
||||
/>
|
||||
<Decision title="Cancel" commit={() => setResult(TYPE_CANCELLED)} />
|
||||
</UserDecisions>
|
||||
293
src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts
Normal file
293
src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { requestToCouchDBWithCredentials } from "../../../../common/utils";
|
||||
import { $msg } from "../../../../lib/src/common/i18n";
|
||||
import { Logger } from "../../../../lib/src/common/logger";
|
||||
import type { ObsidianLiveSyncSettings } from "../../../../lib/src/common/types";
|
||||
import { parseHeaderValues } from "../../../../lib/src/common/utils";
|
||||
import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb";
|
||||
import { generateCredentialObject } from "../../../../lib/src/replication/httplib";
|
||||
export type ResultMessage = { message: string; classes: string[] };
|
||||
export type ResultErrorMessage = { message: string; result: "error"; classes: string[] };
|
||||
export type ResultOk = { message: string; result: "ok"; value?: any };
|
||||
export type ResultError = { message: string; result: "error"; value: any; fixMessage: string; fix(): Promise<void> };
|
||||
export type ConfigCheckResult = ResultOk | ResultError | ResultMessage | ResultErrorMessage;
|
||||
/**
|
||||
* Compares two version strings to determine if the baseVersion is greater than or equal to the version.
|
||||
* @param baseVersion a.b.c format
|
||||
* @param version a.b.c format
|
||||
* @returns true if baseVersion is greater than or equal to version, false otherwise
|
||||
*/
|
||||
function isGreaterThanOrEqual(baseVersion: string, version: string) {
|
||||
const versionParts = `${baseVersion}.0.0.0`.split(".");
|
||||
const targetParts = version.split(".");
|
||||
for (let i = 0; i < targetParts.length; i++) {
|
||||
// compare as number if possible (so 3.10 > 3.2, 3.10.1b > 3.10.1a)
|
||||
const result = versionParts[i].localeCompare(targetParts[i], undefined, { numeric: true });
|
||||
if (result > 0) return true;
|
||||
if (result < 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the remote CouchDB setting with the given key and value.
|
||||
* @param setting Connection settings
|
||||
* @param key setting key to update
|
||||
* @param value setting value to update
|
||||
* @returns true if the update was successful, false otherwise
|
||||
*/
|
||||
async function updateRemoteSetting(setting: ObsidianLiveSyncSettings, key: string, value: any) {
|
||||
const customHeaders = parseHeaderValues(setting.couchDB_CustomHeaders);
|
||||
const credential = generateCredentialObject(setting);
|
||||
const res = await requestToCouchDBWithCredentials(
|
||||
setting.couchDB_URI,
|
||||
credential,
|
||||
undefined,
|
||||
key,
|
||||
value,
|
||||
undefined,
|
||||
customHeaders
|
||||
);
|
||||
if (res.status == 200) {
|
||||
return true;
|
||||
} else {
|
||||
return res.text || "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the CouchDB configuration and returns the results.
|
||||
* @param editingSettings
|
||||
* @returns Array of ConfigCheckResult
|
||||
*/
|
||||
export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) => {
|
||||
const result = [] as ConfigCheckResult[];
|
||||
const addMessage = (msg: string, classes: string[] = []) => {
|
||||
result.push({ message: msg, classes });
|
||||
};
|
||||
const addSuccess = (msg: string, value?: any) => {
|
||||
result.push({ message: msg, result: "ok", value });
|
||||
};
|
||||
const _addError = (message: string, fixMessage: string, fix: () => Promise<void>, value?: any) => {
|
||||
result.push({ message, result: "error", fixMessage, fix, value });
|
||||
};
|
||||
const addErrorMessage = (msg: string, classes: string[] = []) => {
|
||||
result.push({ message: msg, result: "error", classes });
|
||||
};
|
||||
|
||||
const addError = (message: string, fixMessage: string, key: string, expected: any) => {
|
||||
_addError(message, fixMessage, async () => {
|
||||
await updateRemoteSetting(editingSettings, key, expected);
|
||||
});
|
||||
};
|
||||
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"));
|
||||
|
||||
try {
|
||||
if (isCloudantURI(editingSettings.couchDB_URI)) {
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCannotUseCloudant"));
|
||||
return result;
|
||||
}
|
||||
// Tip: Add log for cloudant as Logger($msg("obsidianLiveSyncSettingTab.logServerConfigurationCheck"));
|
||||
const customHeaders = parseHeaderValues(editingSettings.couchDB_CustomHeaders);
|
||||
const credential = generateCredentialObject(editingSettings);
|
||||
const r = await requestToCouchDBWithCredentials(
|
||||
editingSettings.couchDB_URI,
|
||||
credential,
|
||||
window.origin,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
customHeaders
|
||||
);
|
||||
const responseConfig = r.json;
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]);
|
||||
|
||||
const serverBanner = r.headers["server"] ?? r.headers["Server"] ?? "unknown";
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.serverVersion", { info: serverBanner }));
|
||||
const versionMatch = serverBanner.match(/CouchDB(\/([0-9.]+))?/);
|
||||
const versionStr = versionMatch ? versionMatch[2] : "0.0.0";
|
||||
|
||||
// Compare version string with the target version.
|
||||
// version must be a string like "3.2.1" or "3.10.2", and must be two or three parts.
|
||||
|
||||
// Admin check
|
||||
// for database creation and deletion
|
||||
if (!(editingSettings.couchDB_USER in responseConfig.admins)) {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.warnNoAdmin"));
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okAdminPrivileges"));
|
||||
}
|
||||
if (isGreaterThanOrEqual(versionStr, "3.2.0")) {
|
||||
// HTTP user-authorization check
|
||||
if (responseConfig?.chttpd?.require_valid_user != "true") {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errRequireValidUser"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUser"),
|
||||
"chttpd/require_valid_user",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okRequireValidUser"));
|
||||
}
|
||||
} else {
|
||||
if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errRequireValidUserAuth"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth"),
|
||||
"chttpd_auth/require_valid_user",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okRequireValidUserAuth"));
|
||||
}
|
||||
}
|
||||
// HTTPD check
|
||||
// Check Authentication header
|
||||
if (!responseConfig?.httpd["WWW-Authenticate"]) {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errMissingWwwAuth"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetWwwAuth"),
|
||||
"httpd/WWW-Authenticate",
|
||||
'Basic realm="couchdb"'
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okWwwAuth"));
|
||||
}
|
||||
if (isGreaterThanOrEqual(versionStr, "3.2.0")) {
|
||||
if (responseConfig?.chttpd?.enable_cors != "true") {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errEnableCorsChttpd"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgEnableCorsChttpd"),
|
||||
"chttpd/enable_cors",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okEnableCorsChttpd"));
|
||||
}
|
||||
} else {
|
||||
if (responseConfig?.httpd?.enable_cors != "true") {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errEnableCors"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgEnableCors"),
|
||||
"httpd/enable_cors",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okEnableCors"));
|
||||
}
|
||||
}
|
||||
// If the server is not cloudant, configure request size
|
||||
if (!isCloudantURI(editingSettings.couchDB_URI)) {
|
||||
// REQUEST SIZE
|
||||
if (Number(responseConfig?.chttpd?.max_http_request_size ?? 0) < 4294967296) {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errMaxRequestSize"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetMaxRequestSize"),
|
||||
"chttpd/max_http_request_size",
|
||||
"4294967296"
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okMaxRequestSize"));
|
||||
}
|
||||
if (Number(responseConfig?.couchdb?.max_document_size ?? 0) < 50000000) {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errMaxDocumentSize"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetMaxDocSize"),
|
||||
"couchdb/max_document_size",
|
||||
"50000000"
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okMaxDocumentSize"));
|
||||
}
|
||||
}
|
||||
// CORS check
|
||||
// checking connectivity for mobile
|
||||
if (responseConfig?.cors?.credentials != "true") {
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errCorsCredentials"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetCorsCredentials"),
|
||||
"cors/credentials",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okCorsCredentials"));
|
||||
}
|
||||
const ConfiguredOrigins = ((responseConfig?.cors?.origins ?? "") + "").split(",");
|
||||
if (
|
||||
responseConfig?.cors?.origins == "*" ||
|
||||
(ConfiguredOrigins.indexOf("app://obsidian.md") !== -1 &&
|
||||
ConfiguredOrigins.indexOf("capacitor://localhost") !== -1 &&
|
||||
ConfiguredOrigins.indexOf("http://localhost") !== -1)
|
||||
) {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okCorsOrigins"));
|
||||
} else {
|
||||
const fixedValue = [
|
||||
...new Set([
|
||||
...ConfiguredOrigins.map((e) => e.trim()),
|
||||
"app://obsidian.md",
|
||||
"capacitor://localhost",
|
||||
"http://localhost",
|
||||
]),
|
||||
].join(",");
|
||||
addError(
|
||||
$msg("obsidianLiveSyncSettingTab.errCorsOrigins"),
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetCorsOrigins"),
|
||||
"cors/origins",
|
||||
fixedValue
|
||||
);
|
||||
}
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgConnectionCheck"), ["ob-btn-config-head"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: window.location.origin }));
|
||||
|
||||
// Request header check
|
||||
const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"];
|
||||
for (const org of origins) {
|
||||
const rr = await requestToCouchDBWithCredentials(
|
||||
editingSettings.couchDB_URI,
|
||||
credential,
|
||||
org,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
customHeaders
|
||||
);
|
||||
const responseHeaders = Object.fromEntries(
|
||||
Object.entries(rr.headers).map((e) => {
|
||||
e[0] = `${e[0]}`.toLowerCase();
|
||||
return e;
|
||||
})
|
||||
);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgOriginCheck", { org }));
|
||||
if (responseHeaders["access-control-allow-credentials"] != "true") {
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials"));
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin"));
|
||||
}
|
||||
if (responseHeaders["access-control-allow-origin"] != org) {
|
||||
addErrorMessage(
|
||||
$msg("obsidianLiveSyncSettingTab.warnCorsOriginUnmatched", {
|
||||
from: origin,
|
||||
to: responseHeaders["access-control-allow-origin"],
|
||||
})
|
||||
);
|
||||
} else {
|
||||
addSuccess($msg("obsidianLiveSyncSettingTab.okCorsOriginMatched"));
|
||||
}
|
||||
}
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"));
|
||||
} catch (ex: any) {
|
||||
if (ex?.status == 401) {
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.errAccessForbidden"));
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.errCannotContinueTest"));
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"));
|
||||
} else {
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.logCheckingConfigFailed"));
|
||||
Logger(ex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user