Merge pull request #1073 from kanru/encryption

feat: encryption
pull/1358/head
Tienson Qin 2021-02-23 14:39:24 +08:00 committed by GitHub
commit e760318bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 946 additions and 504 deletions

View File

@ -56,6 +56,7 @@
"cljs:build-electron": "clojure -A:cljs compile app electron"
},
"dependencies": {
"@kanru/rage-wasm": "^0.2.1",
"chokidar": "^3.5.1",
"chrono-node": "^2.2.1",
"codemirror": "^5.58.1",

View File

@ -9,6 +9,9 @@
:modules {:main {:init-fn frontend.core/init}
:code-editor
{:entries [frontend.extensions.code]
:depends-on #{:main}}
:age-encryption
{:entries [frontend.extensions.age-encryption]
:depends-on #{:main}}}
:output-dir "./static/js"
@ -52,6 +55,9 @@
:modules {:main {:init-fn frontend.publishing/init}
:code-editor
{:entries [frontend.extensions.code]
:depends-on #{:main}}
:age-encryption
{:entries [frontend.extensions.age-encryption]
:depends-on #{:main}}}
:output-dir "./static/js/publishing"

View File

@ -0,0 +1,177 @@
(ns frontend.components.encryption
(:require [rum.core :as rum]
[promesa.core :as p]
[frontend.encrypt :as e]
[frontend.util :as util :refer-macros [profile]]
[frontend.context.i18n :as i18n]
[frontend.db.utils :as db-utils]
[clojure.string :as string]
[frontend.state :as state]
[frontend.handler.metadata :as metadata-handler]
[frontend.ui :as ui]
[frontend.handler.notification :as notification]))
(rum/defcs encryption-dialog-inner <
(rum/local false ::reveal-secret-phrase?)
[state repo-url close-fn]
(let [reveal-secret-phrase? (get state ::reveal-secret-phrase?)
secret-phrase (e/get-key-pair repo-url)
public-key (e/get-public-key repo-url)
private-key (e/get-secret-key repo-url)]
(rum/with-context [[t] i18n/*tongue-context*]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
"This graph is encrypted"]]]
[:div.mt-1
[:div.max-w-2xl.rounded-md.shadow-sm.sm:max-w-xl
[:div.cursor-pointer.block.w-full.rounded-sm.p-2.text-gray-900
{:on-click (fn []
(when (not @reveal-secret-phrase?)
(reset! reveal-secret-phrase? true)))}
[:div.font-medium.text-gray-900 "Public Key:"]
[:div public-key]
(if @reveal-secret-phrase?
[:div
[:div.mt-1.font-medium.text-gray-900 "Private Key:"]
[:div private-key]]
[:div.text-gray-500 "click to view the private key"])]]]
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
[:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
{:type "button"
:on-click close-fn}
(t :close)]]]])))
(defn encryption-dialog
[repo-url]
(fn [close-fn]
(encryption-dialog-inner repo-url close-fn)))
(rum/defcs input-password-inner <
(rum/local "" ::password)
(rum/local "" ::password-confirm)
[state repo-url close-fn]
(rum/with-context [[t] i18n/*tongue-context*]
(let [password (get state ::password)
password-confirm (get state ::password-confirm)]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900.font-bold
"Enter a password"]]]
(ui/admonition
:warning
[:div.text-gray-700
"Choose a strong and hard to guess password.\nIf you lose your password, all the data can't be decrypted!! Please make sure you remember the password you have set, or you can keep a secure backup of the password."])
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:type "password"
:placeholder "Password"
:auto-focus true
:style {:color "#000"}
:on-change (fn [e]
(reset! password (util/evalue e)))}]
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:type "password"
:placeholder "Re-enter the password"
:auto-focus true
:style {:color "#000"}
:on-change (fn [e]
(reset! password-confirm (util/evalue e)))}]
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
[:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
{:type "button"
:on-click (fn []
(let [value @password]
(cond
(string/blank? value)
nil
(not= @password @password-confirm)
(notification/show! "The passwords are not matched." :error)
:else
(p/let [keys (e/generate-key-pair-and-save! repo-url)
db-encrypted-secret (e/encrypt-with-passphrase value keys)]
(metadata-handler/set-db-encrypted-secret! db-encrypted-secret)
(close-fn true)))))}
"Submit"]]]])))
(defn input-password
[repo-url close-fn]
(fn [_close-fn]
(input-password-inner repo-url close-fn)))
(rum/defcs encryption-setup-dialog-inner
[state repo-url close-fn]
(rum/with-context [[t] i18n/*tongue-context*]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
"Do you want to create an encrypted graph?"]]]
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
[:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
{:type "button"
:on-click (fn []
(state/set-modal! (input-password repo-url close-fn)))}
(t :yes)]]
[:span.mt-3.flex.w-full.rounded-md.shadow-sm.sm:mt-0.sm:w-auto
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
{:type "button"
:on-click (fn [] (close-fn false))}
(t :no)]]]]))
(defn encryption-setup-dialog
[repo-url close-fn]
(fn [close-modal-fn]
(let [close-fn (fn [encrypted?]
(close-fn encrypted?)
(close-modal-fn))]
(encryption-setup-dialog-inner repo-url close-fn))))
(rum/defcs encryption-input-secret-inner <
(rum/local "" ::secret)
[state repo-url db-encrypted-secret close-fn]
(rum/with-context [[t] i18n/*tongue-context*]
(let [secret (get state ::secret)]
[:div
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.text-lg.leading-6.font-medium.text-gray-900
"Enter your password"]]]
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2
{:auto-focus true
:style {:color "#000"}
:on-change (fn [e]
(reset! secret (util/evalue e)))}]
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
[:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
[:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
{:type "button"
:on-click (fn []
(let [value @secret]
(when-not (string/blank? value) ; TODO: length or other checks
(p/let [repo (state/get-current-repo)
keys (e/decrypt-with-passphrase value db-encrypted-secret)]
(e/save-key-pair! repo keys)
(close-fn true)))))}
"Submit"]]]])))
(defn encryption-input-secret-dialog
[repo-url db-encrypted-secret close-fn]
(fn [close-modal-fn]
(let [close-fn (fn [encrypted?]
(close-fn encrypted?)
(close-modal-fn))]
(encryption-input-secret-inner repo-url db-encrypted-secret close-fn))))

View File

@ -4,6 +4,7 @@
[frontend.ui :as ui]
[frontend.state :as state]
[frontend.db :as db]
[frontend.encrypt :as e]
[frontend.handler.repo :as repo-handler]
[frontend.handler.common :as common-handler]
[frontend.handler.route :as route-handler]
@ -16,6 +17,7 @@
[frontend.version :as version]
[frontend.components.commit :as commit]
[frontend.components.svg :as svg]
[frontend.components.encryption :as encryption]
[frontend.context.i18n :as i18n]
[clojure.string :as string]
[clojure.string :as str]))
@ -60,11 +62,17 @@
:href url}
(db/get-repo-path url)])
[:div.controls
[:a.control {:title (if local?
"Sync with the local directory"
"Clone again and re-index the db")
:on-click (fn []
(repo-handler/re-index! nfs-handler/rebuild-index!))}
(when (e/encrypted-db? url)
[:a.control {:title "Show encryption information about this graph"
:on-click (fn []
(state/set-modal! (encryption/encryption-dialog url)))}
"🔐"])
[:a.control.ml-4 {:title (if local?
"Sync with the local directory"
"Clone again and re-index the db")
:on-click (fn []
(repo-handler/re-index! nfs-handler/rebuild-index!)
)}
"Re-index"]
[:a.control.ml-4 {:title "Clone again and re-index the db"
:on-click (fn []

View File

@ -138,6 +138,7 @@
enable-timetracking? (state/enable-timetracking?)
current-repo (state/get-current-repo)
enable-journals? (state/enable-journals? current-repo)
enable-encryption? (state/enable-encryption? current-repo)
enable-git-auto-push? (state/enable-git-auto-push? current-repo)
enable-block-time? (state/enable-block-time?)
show-brackets? (state/show-brackets?)
@ -292,6 +293,13 @@
:else
(notification/show! "Please make sure the page exists!" :warning))))}]]]])
(toggle "enable_encryption"
(t :settings-page/enable-encryption)
enable-encryption?
(fn []
(let [value (not enable-encryption?)]
(config-handler/set-config! :feature/enable-encryption? value))))
(when (string/starts-with? current-repo "https://")
(toggle "enable_git_auto_push"
"Enable Git auto push"
@ -300,6 +308,25 @@
(let [value (not enable-git-auto-push?)]
(config-handler/set-config! :git-auto-push value))))) [:hr]
[:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
[:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
(t :settings-page/current-version)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:p version]
(if (util/electron?) (app-updater))]]
[:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
[:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
{:for "developer_mode"}
(t :settings-page/developer-mode)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
(ui/button (if developer-mode? (t :settings-page/disable-developer-mode) (t :settings-page/enable-developer-mode))
:on-click #(state/set-developer-mode! (not developer-mode?)))]]]
[:br]
(t :settings-page/developer-mode-desc)
(when logged?
[:div
(ui/admonition
@ -325,39 +352,16 @@
(if (= "Enter" k)
(when-let [server (util/evalue event)]
(user-handler/set-cors! server)
(notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
[:hr]])
[:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
[:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
(t :settings-page/current-version)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:p version]
(if (util/electron?) (app-updater))]]
[:hr]
[:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
[:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70
{:for "developer_mode"}
(t :settings-page/developer-mode)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
(ui/button (if developer-mode? (t :settings-page/disable-developer-mode) (t :settings-page/enable-developer-mode))
:on-click #(state/set-developer-mode! (not developer-mode?)))]]]
[:br]
(t :settings-page/developer-mode-desc)
[:hr]
(notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]])
(when logged?
[:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
[:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70.text-red-600
{:for "delete account"}
(t :user/delete-account)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
(ui/button (t :user/delete-your-account)
:on-click #(state/set-modal! delete-account-confirm))]]])]]])))
[:div
[:hr]
[:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start.sm:pt-5
[:label.block.text-sm.font-medium.leading-5.sm:mt-px.sm:pt-2.opacity-70.text-red-600
{:for "delete account"}
(t :user/delete-account)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:div.max-w-lg.rounded-md.shadow-sm.sm:max-w-xs
(ui/button (t :user/delete-your-account)
:on-click #(state/set-modal! delete-account-confirm))]]]])]]])))

View File

@ -346,6 +346,13 @@
(when repo
(get-file-path repo (str app-name "/" config-file)))))
(defn get-metadata-path
([]
(get-metadata-path (state/get-current-repo)))
([repo]
(when repo
(get-file-path repo (str app-name "/" metadata-file)))))
(defn get-custom-css-path
([]
(get-custom-css-path (state/get-current-repo)))

View File

@ -67,10 +67,12 @@
(defn persist! [repo]
(let [file-key (datascript-files-db repo)
non-file-key (datascript-db repo)
file-db (d/db (get-files-conn repo))
non-file-db (d/db (get-conn repo false))
file-db-str (db->string file-db)
non-file-db-str (db->string non-file-db)]
files-conn (get-files-conn repo)
file-db (when files-conn (d/db files-conn))
non-file-conn (get-conn repo false)
non-file-db (d/db non-file-conn)
file-db-str (if file-db (db->string file-db) "")
non-file-db-str (if non-file-db (db->string non-file-db) "")]
(p/let [_ (idb/set-batch! [{:key file-key :value file-db-str}
{:key non-file-key :value non-file-db-str}])]
(state/set-last-persist-transact-id! repo true (get-max-tx-id file-db))

View File

@ -15,7 +15,8 @@
{:schema/version {}
:db/type {}
:db/ident {:db/unique :db.unique/identity}
:db/encrypted? {}
:db/encryption-keys {}
;; user
:me/name {}
:me/email {}

View File

@ -285,6 +285,7 @@ title: How to take dummy notes?
:settings-page/preferred-workflow "Preferred workflow"
:settings-page/enable-timetracking "Enable timetracking"
:settings-page/enable-journals "Enable journals"
:settings-page/enable-encryption "Enable encryption feature"
:settings-page/home-default-page "Set the default home page"
:settings-page/enable-block-time "Enable block timestamps"
:settings-page/dont-use-other-peoples-proxy-servers "Don't use other people's proxy servers. It's very dangerous, which could make your token and notes stolen. Logseq will not be responsible for this loss if you use other people's proxy servers. You can deploy it yourself, check "
@ -300,8 +301,10 @@ title: How to take dummy notes?
:more-options "More options"
:to "to"
:yes "Yes"
:no "No"
:submit "Submit"
:cancel "Cancel"
:close "Close"
:re-index "Re-index"
:export-json "Export as JSON"
:unlink "unlink"
@ -1003,6 +1006,7 @@ title: How to take dummy notes?
:settings-page/preferred-workflow "首选工作流"
:settings-page/enable-timetracking "开启 timetracking"
:settings-page/enable-journals "开启日记"
:settings-page/enable-encryption "激活加密功能"
:settings-page/home-default-page "设置首页默认页面"
:settings-page/enable-block-time "记录 block 创建/修改时间"
:settings-page/dont-use-other-peoples-proxy-servers "不要使用其他人的代理服务器。这非常危险,可能会使您的令牌和笔记被盗。 如果您使用其他人的代理服务器Logseq 将不会对此损失负责。您可以自己部署它,请查阅 "

View File

@ -0,0 +1,102 @@
(ns frontend.encrypt
(:require [frontend.utf8 :as utf8]
[frontend.db.utils :as db-utils]
[frontend.db :as db]
[promesa.core :as p]
[frontend.state :as state]
[clojure.string :as str]
[cljs.reader :as reader]
[shadow.loader :as loader]
[lambdaisland.glogi :as log]))
(defonce age-pem-header-line "-----BEGIN AGE ENCRYPTED FILE-----")
(defonce age-version-line "age-encryption.org/v1")
(defn content-encrypted?
[content]
(or (str/starts-with? content age-pem-header-line)
(str/starts-with? content age-version-line)))
(defn encrypted-db?
[repo-url]
(db-utils/get-key-value repo-url :db/encrypted?))
(defn get-key-pair
[repo-url]
(db-utils/get-key-value repo-url :db/encryption-keys))
(defn save-key-pair!
[repo-url keys]
(let [keys (if (string? keys) (reader/read-string keys) keys)]
(db/set-key-value repo-url :db/encryption-keys keys)
(db/set-key-value repo-url :db/encrypted? true)))
(defn generate-key-pair
[]
(p/let [_ (loader/load :age-encryption)
lazy-keygen (resolve 'frontend.extensions.age-encryption/keygen)]
(lazy-keygen)))
(defn generate-key-pair-and-save!
[repo-url]
(when-not (get-key-pair repo-url)
(p/let [keys (generate-key-pair)]
(save-key-pair! repo-url keys)
(pr-str keys))))
(defn get-public-key
[repo-url]
(second (get-key-pair repo-url)))
(defn get-secret-key
[repo-url]
(first (get-key-pair repo-url)))
(defn encrypt
([content]
(encrypt (state/get-current-repo) content))
([repo-url content]
(cond
(encrypted-db? repo-url)
(p/let [_ (loader/load :age-encryption)
lazy-encrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/encrypt-with-x25519)
content (utf8/encode content)
public-key (get-public-key repo-url)
encrypted (lazy-encrypt-with-x25519 public-key content true)]
(utf8/decode encrypted))
:else
(p/resolved content))))
(defn decrypt
([content]
(decrypt (state/get-current-repo) content))
([repo-url content]
(cond
(and (encrypted-db? repo-url)
(content-encrypted? content))
(let [content (utf8/encode content)]
(if-let [secret-key (get-secret-key repo-url)]
(p/let [_ (loader/load :age-encryption)
lazy-decrypt-with-x25519 (resolve 'frontend.extensions.age-encryption/decrypt-with-x25519)
decrypted (lazy-decrypt-with-x25519 secret-key content)]
(utf8/decode decrypted))
(log/error :encrypt/empty-secret-key (str "Can't find the secret key for repo: " repo-url))))
:else
(p/resolved content))))
(defn encrypt-with-passphrase
[passphrase content]
(p/let [_ (loader/load :age-encryption)
lazy-encrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/encrypt-with-user-passphrase)
content (utf8/encode content)
encrypted (@lazy-encrypt-with-user-passphrase passphrase content true)]
(utf8/decode encrypted)))
;; ;; TODO: What if decryption failed
(defn decrypt-with-passphrase
[passphrase content]
(p/let [_ (loader/load :age-encryption)
lazy-decrypt-with-user-passphrase (resolve 'frontend.extensions.age-encryption/decrypt-with-user-passphrase)
content (utf8/encode content)
decrypted (lazy-decrypt-with-user-passphrase passphrase content)]
(utf8/decode decrypted)))

View File

@ -0,0 +1,23 @@
(ns frontend.extensions.age-encryption
(:require ["regenerator-runtime/runtime"] ;; required for async npm module
["@kanru/rage-wasm" :as rage]))
(defn keygen
[]
(rage/keygen))
(defn encrypt-with-x25519
[public-key content armor]
(rage/encrypt_with_x25519 public-key content armor))
(defn decrypt-with-x25519
[secret-key content]
(rage/decrypt_with_x25519 secret-key content))
(defn encrypt-with-user-passphrase
[passphrase content armor]
(rage/encrypt_with_user_passphrase passphrase content armor))
(defn decrypt-with-user-passphrase
[passphrase content]
(rage/decrypt_with_user_passphrase passphrase content))

View File

@ -10,7 +10,8 @@
[frontend.fs.node :as node]
[frontend.db :as db]
[cljs-bean.core :as bean]
[frontend.state :as state]))
[frontend.state :as state]
[frontend.encrypt :as encrypt]))
(defonce nfs-record (nfs/->Nfs))
(defonce bfs-record (bfs/->Bfs))
@ -56,6 +57,25 @@
[dir]
(protocol/rmdir! (get-fs dir) dir))
(defn write-file!
[repo dir path content opts]
(when content
(let [fs-record (get-fs dir)]
(p/let [metadata-or-css? (or (string/ends-with? path config/metadata-file)
(string/ends-with? path config/custom-css-file))
content (if metadata-or-css? content (encrypt/encrypt content))]
(->
(p/let [_ (protocol/write-file! (get-fs dir) repo dir path content opts)]
(when-not (= fs-record nfs-record)
(db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.))))
(p/catch (fn [error]
(log/error :file/write-failed? {:dir dir
:path path
:error error})
;; Disable this temporarily
;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
)))))))
(defn read-file
([dir path]
(let [fs (get-fs dir)
@ -64,22 +84,8 @@
{})]
(read-file dir path options)))
([dir path options]
(protocol/read-file (get-fs dir) dir path options)))
(defn write-file!
[repo dir path content opts]
(let [fs-record (get-fs dir)]
(->
(p/let [_ (protocol/write-file! fs-record repo dir path content opts)]
(when-not (= fs-record nfs-record)
(db/set-file-last-modified-at! repo (config/get-file-path repo path) (js/Date.))))
(p/catch (fn [error]
(log/error :file/write-failed? {:dir dir
:path path
:error error})
;; Disable this temporarily
;; (js/alert "Current file can't be saved! Please copy its content to your local file system and click the refresh button.")
)))))
(p/chain (protocol/read-file (get-fs dir) dir path options)
encrypt/decrypt)))
(defn rename!
[repo old-path new-path]

View File

@ -7,6 +7,7 @@
[frontend.text :as text]
[frontend.git :as git]
[frontend.db :as db]
[frontend.encrypt :as e]
[lambdaisland.glogi :as log]
[cljs.reader :as reader]
[frontend.spec :as spec]
@ -94,6 +95,15 @@
(state/set-config! repo-url config)
config)))
(defn read-metadata!
[repo-url content]
(try
(reader/read-string content)
(catch js/Error e
(println "Parsing metadata file failed: ")
(js/console.dir e)
{})))
(defn request-app-tokens!
[ok-handler error-handler]
(let [repos (state/get-repos)

View File

@ -1,6 +1,6 @@
(ns frontend.handler.config
(:require [frontend.state :as state]
[frontend.handler.repo :as repo-handler]
[frontend.handler.file :as file-handler]
[borkdude.rewrite-edn :as rewrite]
[frontend.config :as config]
[frontend.db :as db]
@ -21,7 +21,7 @@
new-config (rewrite/assoc-in config ks v)]
(state/set-config! repo new-config)
(let [new-content (str new-config)]
(repo-handler/set-config-content! repo path new-content)))))))
(file-handler/set-file-content! repo path new-content)))))))
(defn toggle-ui-show-brackets! []
(let [show-brackets? (state/show-brackets?)]

View File

@ -194,6 +194,11 @@
(println "Write file failed, path: " path ", content: " content)
(log/error :write/failed error)))))
(defn set-file-content!
[repo path new-content]
(alter-file repo path new-content {:reset? false
:re-render-root? false}))
(defn create!
([path]
(create! path ""))
@ -320,3 +325,15 @@
directories (map (fn [repo] (config/get-repo-dir (:url repo))) repos)]
(doseq [dir directories]
(fs/watch-dir! dir)))))
(defn create-metadata-file
[repo-url encrypted?]
(let [repo-dir (config/get-repo-dir repo-url)
path (str config/app-name "/" config/metadata-file)
file-path (str "/" path)
default-content (if encrypted? "{:db/encrypted? true}" "{}")]
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
file-exists? (fs/create-if-not-exists repo-url repo-dir file-path default-content)]
(when-not file-exists?
(reset-file! repo-url path default-content)
(git-handler/git-add repo-url path)))))

View File

@ -0,0 +1,37 @@
(ns frontend.handler.metadata
(:require [frontend.state :as state]
[frontend.handler.file :as file-handler]
[cljs.reader :as reader]
[frontend.config :as config]
[frontend.db :as db]
[clojure.string :as string]
[promesa.core :as p]))
(def default-metadata-str "{}")
(defn set-metadata!
[k v]
(when-let [repo (state/get-current-repo)]
(let [encrypted? (= k :db/encrypted-secret)
path (config/get-metadata-path)
file-content (db/get-file-no-sub path)]
(p/let [_ (file-handler/create-metadata-file repo false)]
(let [metadata-str (or file-content default-metadata-str)
metadata (try
(reader/read-string metadata-str)
(catch js/Error e
(println "Parsing metadata.edn failed: ")
(js/console.dir e)
{}))
ks (if (vector? k) k [k])
new-metadata (assoc-in metadata ks v)
new-metadata (if encrypted?
(assoc new-metadata :db/encrypted? true)
new-metadata)
new-content (pr-str new-metadata)]
(file-handler/set-file-content! repo path new-content))))))
(defn set-db-encrypted-secret!
[encrypted-secret]
(when-not (string/blank? encrypted-secret)
(set-metadata! :db/encrypted-secret encrypted-secret)))

View File

@ -25,8 +25,11 @@
[clojure.string :as string]
[frontend.dicts :as dicts]
[frontend.spec :as spec]
[frontend.encrypt :as encrypt]
[goog.dom :as gdom]
[goog.object :as gobj]))
[goog.object :as gobj]
;; TODO: remove component dependency from handlers, we can use a core.async channel
[frontend.components.encryption :as encryption]))
;; Project settings should be checked in two situations:
;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
@ -161,12 +164,16 @@
(create-today-journal-if-not-exists repo))))))
(defn create-default-files!
[repo-url]
(spec/validate :repos/url repo-url)
(create-config-file-if-not-exists repo-url)
(create-today-journal-if-not-exists repo-url)
(create-contents-file repo-url)
(create-custom-theme repo-url))
([repo-url]
(create-default-files! repo-url false))
([repo-url encrypted?]
(spec/validate :repos/url repo-url)
(file-handler/create-metadata-file repo-url encrypted?)
;; TODO: move to frontend.handler.file
(create-config-file-if-not-exists repo-url)
(create-today-journal-if-not-exists repo-url)
(create-contents-file repo-url)
(create-custom-theme repo-url)))
(defn- reset-contents-and-blocks!
[repo-url files blocks-pages delete-files delete-blocks]
@ -176,13 +183,9 @@
(util/remove-nils))]
(db/transact! repo-url all-data)))
(defn parse-files-and-load-to-db!
[repo-url files {:keys [first-clone? delete-files delete-blocks re-render? re-render-opts] :as opts
:or {re-render? true}}]
(state/set-loading-files! false)
(state/set-importing-to-db! true)
(let [file-paths (map :file/path files)
parsed-files (filter
(defn- parse-files-and-create-default-files-inner!
[repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts]
(let [parsed-files (filter
(fn [file]
(let [format (format/get-format (:file/path file))]
(contains? config/mldoc-support-formats format)))
@ -196,11 +199,49 @@
(when-let [content (some #(when (= (:file/path %) config-file)
(:file/content %)) files)]
(file-handler/restore-config! repo-url content true))))
(when first-clone? (create-default-files! repo-url))
(when first-clone?
(if (and (not db-encrypted?) (state/enable-encryption? repo-url))
(state/set-modal!
(encryption/encryption-setup-dialog
repo-url
#(create-default-files! repo-url %)))
(create-default-files! repo-url db-encrypted?)))
(when re-render?
(ui-handler/re-render-root! re-render-opts))
(state/set-importing-to-db! false)))
(defn- parse-files-and-create-default-files!
[repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts]
(if db-encrypted?
(p/let [files (p/all
(map (fn [file]
(p/let [content (encrypt/decrypt (:file/content file))]
(assoc file :file/content content)))
files))]
(parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts))
(parse-files-and-create-default-files-inner! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts)))
(defn parse-files-and-load-to-db!
[repo-url files {:keys [first-clone? delete-files delete-blocks re-render? re-render-opts] :as opts
:or {re-render? true}}]
(state/set-loading-files! false)
(state/set-importing-to-db! true)
(let [file-paths (map :file/path files)]
(let [metadata-file (config/get-metadata-path)
metadata-content (some #(when (= (:file/path %) metadata-file)
(:file/content %)) files)
metadata (when metadata-content
(common-handler/read-metadata! repo-url metadata-content))
db-encrypted? (:db/encrypted? metadata)
db-encrypted-secret (if db-encrypted? (:db/encrypted-secret metadata) nil)]
(if db-encrypted?
(state/set-modal!
(encryption/encryption-input-secret-dialog
repo-url
db-encrypted-secret
#(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts)))
(parse-files-and-create-default-files! repo-url files delete-files delete-blocks file-paths first-clone? db-encrypted? re-render? re-render-opts)))))
(defn load-repo-to-db!
[repo-url {:keys [first-clone? diffs nfs-files]
:as opts}]
@ -531,14 +572,9 @@
(git-handler/set-git-error! repo-url e)
(show-install-error! repo-url (util/format "Failed to clone %s." repo-url)))))))
(defn set-config-content!
[repo path new-content]
(file-handler/alter-file repo path new-content {:reset? false
:re-render-root? false}))
(defn remove-repo!
[{:keys [id url] :as repo}]
(spec/validate :repos/repo repo)
;; (spec/validate :repos/repo repo)
(let [delete-db-f (fn []
(db/remove-conn! url)
(db/remove-db! url)

View File

@ -19,7 +19,8 @@
[frontend.db :as db]
[frontend.db.model :as db-model]
[frontend.config :as config]
[lambdaisland.glogi :as log]))
[lambdaisland.glogi :as log]
[frontend.encrypt :as encrypt]))
(defn remove-ignore-files
[files]
@ -144,7 +145,8 @@
(-> (p/all (map (fn [file]
(p/let [content (if nfs?
(.text (:file/file file))
(:file/content file))]
(:file/content file))
content (encrypt/decrypt content)]
(assoc file :file/content content))) markup-files))
(p/then (fn [result]
(let [files (map #(dissoc % :file/file) result)]
@ -239,7 +241,8 @@
(when-let [file (get-file-f path new-files)]
(p/let [content (if nfs?
(.text (:file/file file))
(:file/content file))]
(:file/content file))
content (encrypt/decrypt content)]
(assoc file :file/content content)))) added-or-modified))
(p/then (fn [result]
(let [files (map #(dissoc % :file/file :file/handle) result)

View File

@ -68,8 +68,8 @@
(defn reset-indice!
[repo]
(swap! indices assoc repo {:pages #js []
:blocks #js []}))
(swap! indices assoc repo {:pages nil
:blocks nil}))
;; Copied from https://gist.github.com/vaughnd/5099299
(defn str-len-distance

View File

@ -193,6 +193,11 @@
(not (false? (:feature/enable-journals?
(get (sub-config) repo)))))
(defn enable-encryption?
[repo]
(:feature/enable-encryption?
(get (sub-config) repo)))
(defn enable-git-auto-push?
[repo]
(not (false? (:git-auto-push

View File

@ -4,22 +4,24 @@
[datascript.core :as d]
[frontend.db-schema :as schema]
[frontend.handler.repo :as repo-handler]
[promesa.core :as p]
[cljs.test :refer [deftest is are testing use-fixtures]]))
(deftest test-page-alias-with-multiple-alias
[]
(let [files [{:file/path "a.md"
:file/content "---\ntitle: a\nalias: b, c\n---"}
{:file/path "b.md"
:file/content "---\ntitle: b\nalias: a, d\n---"}
{:file/path "e.md"
:file/content "---\ntitle: e\n---\n## ref to [[b]]"}]
_ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false})
a-aliases (model/page-alias-set test-db "a")
b-aliases (model/page-alias-set test-db "b")
alias-names (model/get-page-alias-names test-db "a")
b-ref-blocks (model/get-page-referenced-blocks test-db "b")
a-ref-blocks (model/get-page-referenced-blocks test-db "a")]
(p/let [files [{:file/path "a.md"
:file/content "---\ntitle: a\nalias: b, c\n---"}
{:file/path "b.md"
:file/content "---\ntitle: b\nalias: a, d\n---"}
{:file/path "e.md"
:file/content "---\ntitle: e\n---\n## ref to [[b]]"}]
_ (-> (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false})
(p/catch (fn [] "ignore indexedDB error")))
a-aliases (model/page-alias-set test-db "a")
b-aliases (model/page-alias-set test-db "b")
alias-names (model/get-page-alias-names test-db "a")
b-ref-blocks (model/get-page-referenced-blocks test-db "b")
a-ref-blocks (model/get-page-referenced-blocks test-db "a")]
(are [x y] (= x y)
4 (count a-aliases)
4 (count b-aliases)
@ -29,43 +31,45 @@
(deftest test-page-alias-set
[]
(let [files [{:file/path "a.md"
:file/content "---\ntitle: a\nalias: [[b]]\n---"}
{:file/path "b.md"
:file/content "---\ntitle: b\nalias: [[c]]\n---"}
{:file/path "d.md"
:file/content "---\ntitle: d\n---\n## ref to [[b]]"}]
_ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false})
a-aliases (model/page-alias-set test-db "a")
b-aliases (model/page-alias-set test-db "b")
alias-names (model/get-page-alias-names test-db "a")
b-ref-blocks (model/get-page-referenced-blocks test-db "b")
a-ref-blocks (model/get-page-referenced-blocks test-db "a")]
(p/let [files [{:file/path "a.md"
:file/content "---\ntitle: a\nalias: [[b]]\n---"}
{:file/path "b.md"
:file/content "---\ntitle: b\nalias: [[c]]\n---"}
{:file/path "d.md"
:file/content "---\ntitle: d\n---\n## ref to [[b]]"}]
_ (-> (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false})
(p/catch (fn [] "ignore indexedDB error")))
a-aliases (model/page-alias-set test-db "a")
b-aliases (model/page-alias-set test-db "b")
alias-names (model/get-page-alias-names test-db "a")
b-ref-blocks (model/get-page-referenced-blocks test-db "b")
a-ref-blocks (model/get-page-referenced-blocks test-db "a")]
(are [x y] (= x y)
3 (count a-aliases)
1 (count b-ref-blocks)
1 (count a-ref-blocks)
["b" "c"] alias-names)))
(set ["b" "c"]) (set alias-names))))
(deftest test-page-alias-without-brackets
[]
(let [files [{:file/path "a.md"
:file/content "---\ntitle: a\nalias: b\n---"}
{:file/path "b.md"
:file/content "---\ntitle: b\nalias: c\n---"}
{:file/path "d.md"
:file/content "---\ntitle: d\n---\n## ref to [[b]]"}]
_ (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false})
a-aliases (model/page-alias-set test-db "a")
b-aliases (model/page-alias-set test-db "b")
alias-names (model/get-page-alias-names test-db "a")
b-ref-blocks (model/get-page-referenced-blocks test-db "b")
a-ref-blocks (model/get-page-referenced-blocks test-db "a")]
(p/let [files [{:file/path "a.md"
:file/content "---\ntitle: a\nalias: b\n---"}
{:file/path "b.md"
:file/content "---\ntitle: b\nalias: c\n---"}
{:file/path "d.md"
:file/content "---\ntitle: d\n---\n## ref to [[b]]"}]
_ (-> (repo-handler/parse-files-and-load-to-db! test-db files {:re-render? false})
(p/catch (fn [] "ignore indexedDB error")))
a-aliases (model/page-alias-set test-db "a")
b-aliases (model/page-alias-set test-db "b")
alias-names (model/get-page-alias-names test-db "a")
b-ref-blocks (model/get-page-referenced-blocks test-db "b")
a-ref-blocks (model/get-page-referenced-blocks test-db "a")]
(are [x y] (= x y)
3 (count a-aliases)
1 (count b-ref-blocks)
1 (count a-ref-blocks)
["b" "c"] alias-names)))
(set ["b" "c"]) (set alias-names))))
(use-fixtures :each
{:before config/start-test-db!

View File

@ -6,6 +6,7 @@
[datascript.core :as d]
[frontend.db-schema :as schema]
[frontend.handler.repo :as repo-handler]
[promesa.core :as p]
[cljs.test :refer [deftest is are testing use-fixtures]]))
;; TODO: quickcheck

742
yarn.lock

File diff suppressed because it is too large Load Diff