diff --git a/src/main/frontend/components/repo.cljs b/src/main/frontend/components/repo.cljs index 6f3b20007d..5854e5366a 100644 --- a/src/main/frontend/components/repo.cljs +++ b/src/main/frontend/components/repo.cljs @@ -451,8 +451,8 @@ (-> (p/do! (state/ (push-sync-config-to-worker!) + (p/then #(notification/show! (t :settings-page/sync-server-url-cleared) :success)) + (p/catch #(notification/show! (str "Failed to update worker: " %) :error))))] + [:div.cp__settings-sync-server-cnt + [:h1.mb-2.text-2xl.font-bold (t :settings-page/sync-server-url)] + [:div.p-2 + [:p.text-sm.opacity-70.mb-4 (t :settings-page/sync-server-url-desc)] + [:p + [:label + [:strong "URL"] + [:input.form-input.is-small + {:value url + :placeholder config/default-db-sync-http-base + :style {:width "100%"} + :on-change #(set-url! (util/evalue %))}]]] + [:p.pt-2.flex.gap-2 + (shui/button + {:size :sm + :on-click (fn [] + (let [trimmed (string/trim url)] + (if (string/blank? trimmed) + (reset-url!) + (if-not (config/valid-sync-server-url? trimmed) + (notification/show! "URL must start with https:// or http://" :error) + (do + (config/set-custom-sync-server-url! trimmed) + (-> (push-sync-config-to-worker!) + (p/then #(notification/show! (t :settings-page/sync-server-url-saved) :success)) + (p/catch #(notification/show! (str "Failed to update worker: " %) :error))))))))} + (t :save)) + (when (seq url) + (shui/button + {:size :sm + :variant :outline + :on-click (fn [] (reset-url!))} + (t :settings-page/sync-server-url-reset)))]]])) + +(rum/defc sync-server-url-button + [] + (let [current-url (config/get-custom-sync-server-url)] + (ui/button [:span.flex.items-center + [:span.pr-1 + (if (seq current-url) + current-url + (t :settings-page/sync-server-url-default))] + (ui/icon "edit")] + :class "text-sm" + :on-click #(state/pub-event! [:go/sync-server-settings])))) + +(defn sync-server-url-row [] + (row-with-button-action + {:left-label (t :settings-page/sync-server-url) + :action (sync-server-url-button)})) + (rum/defc user-proxy-settings [{:keys [type protocol host port] :as agent-opts}] (ui/button [:span.flex.items-center @@ -661,6 +731,7 @@ (when (and (or util/mac? util/win32?) (util/electron?)) (app-auto-update-row t)) (usage-diagnostics-row t instrument-disabled?) (when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?)) + (sync-server-url-row) (when (util/electron?) (https-user-agent-row https-agent-opts)) (when (util/electron?) (auto-chmod-row t)) ;; (clear-cache-row t) diff --git a/src/main/frontend/config.cljs b/src/main/frontend/config.cljs index 34447b19b3..45e07c0a59 100644 --- a/src/main/frontend/config.cljs +++ b/src/main/frontend/config.cljs @@ -50,19 +50,70 @@ (goog-define ENABLE-DB-SYNC-LOCAL false) (defonce db-sync-local? ENABLE-DB-SYNC-LOCAL) -(defonce db-sync-ws-url +(defonce default-db-sync-ws-url (if db-sync-local? "ws://127.0.0.1:8787/sync/%s" ;; "wss://api-staging.logseq.io/sync/%s" "wss://api.logseq.io/sync/%s")) -(defonce db-sync-http-base +(defonce default-db-sync-http-base (if db-sync-local? "http://127.0.0.1:8787" ;; "https://api-staging.logseq.io" "https://api.logseq.io" )) +(defn get-custom-sync-server-url + "Read the user-configured custom sync server URL from localStorage. + Returns nil when not set or empty." + [] + (when-not util/node-test? + (let [v (.getItem js/localStorage "sync-server-url")] + (when (and (string? v) (not (string/blank? v))) + v)))) + +(defn set-custom-sync-server-url! + "Persist the custom sync server URL to localStorage. Pass nil or empty string to clear." + [url] + (when-not util/node-test? + (if (or (nil? url) (string/blank? url)) + (.removeItem js/localStorage "sync-server-url") + (.setItem js/localStorage "sync-server-url" (string/trim url))))) + +(defn valid-sync-server-url? + "Return true when `url` looks like a valid HTTP(S) base URL." + [url] + (and (string? url) + (re-find #"^https?://" url))) + +(defn custom-url->ws-url + "Derive a WebSocket sync URL from a custom HTTP base URL. Pure function." + [custom-url] + (let [scheme (if (string/starts-with? custom-url "https") "wss" "ws") + base (-> custom-url + (string/replace #"^https?://" "") + (string/replace #"/+$" ""))] + (str scheme "://" base "/sync/%s"))) + +(defn custom-url->http-base + "Normalize a custom HTTP base URL by stripping trailing slashes. Pure function." + [custom-url] + (string/replace custom-url #"/+$" "")) + +(defn db-sync-ws-url + "Return the WebSocket sync URL. Uses custom server when configured, otherwise the default." + [] + (if-let [custom (get-custom-sync-server-url)] + (custom-url->ws-url custom) + default-db-sync-ws-url)) + +(defn db-sync-http-base + "Return the HTTP base URL for sync. Uses custom server when configured, otherwise the default." + [] + (if-let [custom (get-custom-sync-server-url)] + (custom-url->http-base custom) + default-db-sync-http-base)) + ;; Feature flags ;; ============= diff --git a/src/main/frontend/handler/db_based/sync.cljs b/src/main/frontend/handler/db_based/sync.cljs index b089dcd5e2..2b8959f642 100644 --- a/src/main/frontend/handler/db_based/sync.cljs +++ b/src/main/frontend/handler/db_based/sync.cljs @@ -28,8 +28,8 @@ base))) (defn http-base [] - (or config/db-sync-http-base - (ws->http-base config/db-sync-ws-url))) + (or (config/db-sync-http-base) + (ws->http-base (config/db-sync-ws-url)))) (defn- auth-headers [] (when-let [token (state/get-auth-id-token)] diff --git a/src/main/frontend/handler/events/ui.cljs b/src/main/frontend/handler/events/ui.cljs index 7e09902d6c..eca6c85d17 100644 --- a/src/main/frontend/handler/events/ui.cljs +++ b/src/main/frontend/handler/events/ui.cljs @@ -81,6 +81,11 @@ (plugin/user-proxy-settings-container agent-opts) {:id :https-proxy-panel :center? true :class "lg:max-w-2xl"})) +(defmethod events/handle :go/sync-server-settings [[_]] + (shui/dialog-open! + (settings/sync-server-url-settings-container) + {:id :sync-server-panel :center? true :class "lg:max-w-2xl"})) + (defmethod events/handle :redirect-to-home [_] (page-handler/create-today-journal!) (when (util/capacitor?) @@ -330,8 +335,8 @@ (state/pub-event! [:rtc/sync-app-state]) (state/ (p/let [_ (state/ws-url-test + (testing "https URL becomes wss" + (is (= "wss://my-server.example.com/sync/%s" + (config/custom-url->ws-url "https://my-server.example.com")))) + + (testing "http URL becomes ws" + (is (= "ws://localhost:8787/sync/%s" + (config/custom-url->ws-url "http://localhost:8787")))) + + (testing "trailing slashes are stripped" + (is (= "wss://my-server.example.com/sync/%s" + (config/custom-url->ws-url "https://my-server.example.com/"))) + (is (= "wss://my-server.example.com/sync/%s" + (config/custom-url->ws-url "https://my-server.example.com///")))) + + (testing "preserves port in URL" + (is (= "wss://example.com:3000/sync/%s" + (config/custom-url->ws-url "https://example.com:3000")))) + + (testing "preserves subpath in host" + ;; Users should only provide a base URL, but verify trailing path doesn't break things + (is (= "wss://example.com/api/sync/%s" + (config/custom-url->ws-url "https://example.com/api"))))) + +(deftest custom-url->http-base-test + (testing "returns URL as-is when no trailing slash" + (is (= "https://my-server.example.com" + (config/custom-url->http-base "https://my-server.example.com")))) + + (testing "strips trailing slashes" + (is (= "https://my-server.example.com" + (config/custom-url->http-base "https://my-server.example.com/"))) + (is (= "https://my-server.example.com" + (config/custom-url->http-base "https://my-server.example.com///")))) + + (testing "preserves http scheme" + (is (= "http://localhost:8787" + (config/custom-url->http-base "http://localhost:8787")))) + + (testing "preserves port" + (is (= "https://example.com:3000" + (config/custom-url->http-base "https://example.com:3000/"))))) + +(deftest default-urls-are-returned-when-no-custom-url + (testing "db-sync-ws-url returns default when no custom URL is set" + ;; In test environment, node-test? is true so get-custom-sync-server-url + ;; always returns nil, meaning we always get the default + (is (string? (config/db-sync-ws-url))) + (is (= config/default-db-sync-ws-url (config/db-sync-ws-url)))) + + (testing "db-sync-http-base returns default when no custom URL is set" + (is (string? (config/db-sync-http-base))) + (is (= config/default-db-sync-http-base (config/db-sync-http-base))))) + +(deftest valid-sync-server-url?-test + (testing "accepts http and https URLs" + (is (config/valid-sync-server-url? "https://my-server.example.com")) + (is (config/valid-sync-server-url? "http://localhost:8787"))) + + (testing "rejects non-URL strings" + (is (not (config/valid-sync-server-url? "not a url"))) + (is (not (config/valid-sync-server-url? ""))) + (is (not (config/valid-sync-server-url? nil)))))