web: add support for changing server urls

This commit is contained in:
Abdullah Atta
2024-08-02 12:05:27 +05:00
committed by Abdullah Atta
parent 22aae401ba
commit 5bbc0cd452
9 changed files with 268 additions and 61 deletions

View File

@@ -26,6 +26,7 @@ import { isFeatureSupported } from "../utils/feature-check";
import { generatePassword } from "../utils/password-generator";
import { deriveKey, useKeyStore } from "../interfaces/key-store";
import { logManager } from "@notesnook/core/dist/logger";
import Config from "../utils/config";
const db = database;
async function initializeDatabase(persistence: DatabasePersistence) {
@@ -45,7 +46,8 @@ async function initializeDatabase(persistence: DatabasePersistence) {
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co"
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
...Config.get("serverUrls", {})
});
const storage = new NNStorage(

View File

@@ -55,6 +55,7 @@ function Field(props: FieldProps) {
type,
inputRef,
validate,
disabled,
...inputProps
} = props;
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
@@ -66,6 +67,8 @@ function Field(props: FieldProps) {
sx={{
m: "2px",
mr: "2px",
opacity: disabled ? 0.7 : 1,
pointerEvents: disabled ? "none" : "all",
...sx,
flexDirection: "column"
}}

View File

@@ -214,7 +214,8 @@ import {
mdiLink,
mdiWindowClose,
mdiFileMusicOutline,
mdiBroom
mdiBroom,
mdiServerSecurity
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -541,6 +542,7 @@ export const Documentation = createIcon(mdiFileDocumentOutline);
export const Legal = createIcon(mdiGavel);
export const Desktop = createIcon(mdiDesktopClassic);
export const Notification = createIcon(mdiBellBadgeOutline);
export const Servers = createIcon(mdiServerSecurity);
export const Calendar = createIcon(mdiCalendarBlank);
export const WindowMinimize = createIcon("M4 20v-2h16v2H4Z");

View File

@@ -1,57 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect } from "react";
import { Box, Text } from "@theme-ui/components";
import Dialog from "../components/dialog";
import { Loading } from "../components/icons";
type LoadingDialogProps<T> = {
onClose: (result: T | boolean) => void;
action: () => T | Promise<T>;
title: string;
description: string;
message?: string;
};
function LoadingDialog<T>(props: LoadingDialogProps<T>) {
const { onClose, action, description, message, title } = props;
useEffect(() => {
(async function () {
try {
onClose(await action());
} catch (e) {
onClose(false);
throw e;
}
})();
}, [onClose, action]);
return (
<Dialog isOpen={true} title={title} description={description}>
<Box>
<Text as="span" variant="body">
{message}
</Text>
<Loading sx={{ my: 2 }} color="accent" />
</Box>
</Dialog>
);
}
export default LoadingDialog;

View File

@@ -0,0 +1,197 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, Text } from "@theme-ui/components";
import Field from "../../../components/field";
import { useState } from "react";
import { HostId, HostIds, useStore } from "../../../stores/setting-store";
import { useStore as useUserStore } from "../../../stores/user-store";
import { ErrorText } from "../../../components/error-text";
import { TaskManager } from "../../../common/task-manager";
export const ServerIds = ["notesnook-sync", "auth", "sse"] as const;
export type ServerId = (typeof ServerIds)[number];
type Server = {
id: ServerId;
host: HostId;
title: string;
example: string;
description: string;
};
type VersionResponse = {
version: string;
id: string;
instance: string;
};
const SERVERS: Server[] = [
{
id: "notesnook-sync",
host: "API_HOST",
title: "Sync server",
example: "http://localhost:4326",
description: "Server used to sync your notes & other data between devices."
},
{
id: "auth",
host: "AUTH_HOST",
title: "Auth server",
example: "http://localhost:5326",
description: "Server used for login/sign up and authentication."
},
{
id: "sse",
host: "SSE_HOST",
title: "Events server",
example: "http://localhost:7326",
description: "Server used to receive important notifications & events."
}
];
export function ServersConfiguration() {
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<boolean>();
const [urls, setUrls] = useState<Partial<Record<HostId, string>>>(
useStore.getState().serverUrls
);
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
return (
<>
{isLoggedIn ? (
<ErrorText
error="You must log out in order to change/reset server URLs."
sx={{ mb: 2, mt: 0 }}
/>
) : null}
<Flex sx={{ flexDirection: "column" }}>
{SERVERS.map((server) => (
<Field
disabled={isLoggedIn}
key={server.id}
label={`${server.title} URL`}
helpText={server.description}
placeholder={`e.g. ${server.example}`}
validate={(text) => URL.canParse(text)}
defaultValue={urls[server.host]}
onChange={(e) =>
setUrls((s) => {
s[server.host] = e.target.value;
return s;
})
}
/>
))}
<ErrorText error={error} />
{success === true ? (
<Text
className="selectable"
variant={"error"}
ml={1}
sx={{
whiteSpace: "pre-wrap",
bg: "shade",
color: "accent",
p: 1,
mt: 2,
borderRadius: "default"
}}
>
Connected to all servers sucessfully.
</Text>
) : null}
<Flex sx={{ mt: 1, justifyContent: "end", gap: 1 }}>
<Button
variant="accent"
disabled={!success}
onClick={async () => {
if (!success || isLoggedIn) return;
useStore.getState().setServerUrls(urls);
await TaskManager.startTask({
type: "modal",
title: "App will reload in 5 seconds",
subtitle:
"Your changes have been saved and will be reflected after the app has refreshed.",
action() {
return new Promise((resolve) => {
setTimeout(() => {
window.location.reload();
resolve(undefined);
}, 5000);
});
}
});
}}
>
Save
</Button>
<Button
disabled={isLoggedIn}
variant="secondary"
onClick={async () => {
setError(undefined);
try {
for (const host of HostIds) {
const url = urls[host];
const server = SERVERS.find((s) => s.host === host)!;
if (!url) throw new Error("All server urls are required.");
const version = await fetch(`${url}/version`)
.then((r) => r.json() as Promise<VersionResponse>)
.catch(() => undefined);
if (!version)
throw new Error(`Could not connect to ${server.title}.`);
if (version.id !== server.id)
throw new Error(
`The URL you have given (${url}) does not point to the ${server.title}.`
);
}
setSuccess(true);
} catch (e) {
setError((e as Error).message);
}
}}
>
Test connection
</Button>
<Button
disabled={isLoggedIn}
variant="errorSecondary"
onClick={async () => {
if (isLoggedIn) return;
useStore.getState().setServerUrls();
await TaskManager.startTask({
type: "modal",
title: "App will reload in 5 seconds",
subtitle:
"Your changes have been saved and will be reflected after the app has refreshed.",
action() {
return new Promise((resolve) => {
setTimeout(() => {
window.location.reload();
resolve(undefined);
}, 5000);
});
}
});
}}
>
Reset
</Button>
</Flex>
</Flex>
</>
);
}

View File

@@ -36,6 +36,7 @@ import {
PasswordAndAuth,
Privacy,
Pro,
Servers,
ShieldLock,
Sync
} from "../../components/icons";
@@ -74,6 +75,7 @@ import { SubscriptionSettings } from "./subscription-settings";
import { ScopedThemeProvider } from "../../components/theme-provider";
import { AppLockSettings } from "./app-lock-settings";
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
import { ServersSettings } from "./servers-settings";
type SettingsDialogProps = BaseDialogProps<false>;
@@ -116,7 +118,8 @@ const sectionGroups: SectionGroup[] = [
icon: Desktop,
isHidden: () => !IS_DESKTOP_APP
},
{ key: "notifications", title: "Notifications", icon: Notification }
{ key: "notifications", title: "Notifications", icon: Notification },
{ key: "servers", title: "Servers", icon: Servers }
]
},
{
@@ -164,7 +167,8 @@ const SettingsGroups = [
...LegalSettings,
...SupportSettings,
...AboutSettings,
...SubscriptionSettings
...SubscriptionSettings,
...ServersSettings
];
// Thoughts:

View File

@@ -0,0 +1,41 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ServersConfiguration } from "./components/servers-configuration";
import { SettingsGroup } from "./types";
export const ServersSettings: SettingsGroup[] = [
{
header: "Servers configuration",
key: "servers",
section: "servers",
settings: [
{
key: "config",
title: "",
components: [
{
type: "custom",
component: ServersConfiguration
}
]
}
]
}
];

View File

@@ -28,6 +28,7 @@ export type SectionKeys =
| "behaviour"
| "desktop"
| "notifications"
| "servers"
| "editor"
| "backup-export"
| "export"

View File

@@ -29,6 +29,8 @@ import { setDocumentTitle } from "../utils/dom";
import { TimeFormat } from "@notesnook/core/dist/utils/date";
import { Profile, TrashCleanupInterval } from "@notesnook/core";
export const HostIds = ["API_HOST", "AUTH_HOST", "SSE_HOST"] as const;
export type HostId = (typeof HostIds)[number];
class SettingStore extends BaseStore<SettingStore> {
encryptBackups = Config.get("encryptBackups", false);
backupReminderOffset = Config.get("backupReminderOffset", 0);
@@ -41,6 +43,7 @@ class SettingStore extends BaseStore<SettingStore> {
markdownShortcuts = Config.get("markdownShortcuts", true);
notificationsSettings = Config.get("notifications", { reminder: true });
isFullOfflineMode = Config.get("fullOfflineMode", false);
serverUrls: Partial<Record<HostId, string>> = Config.get("serverUrls", {});
zoomFactor = 1.0;
privacyMode = false;
@@ -217,6 +220,17 @@ class SettingStore extends BaseStore<SettingStore> {
if (isFullOfflineMode) db.fs().cancel("offline-mode");
else db.attachments.cacheAttachments();
};
setServerUrls = (urls?: Partial<Record<HostId, string>>) => {
if (!urls) {
Config.set("serverUrls", {});
this.set({ serverUrls: {} });
return;
}
const serverUrls = this.get().serverUrls;
this.set({ serverUrls: { ...serverUrls, ...urls } });
Config.set("serverUrls", { ...serverUrls, ...urls });
};
}
const [useStore, store] = createStore<SettingStore>(