diff --git a/config/runtime.exs b/config/runtime.exs index ed2a275..9e97417 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,5 +1,11 @@ import Config +config :jenot, host: System.fetch_env!("HOST") + +config :jenot, secret_key_base: System.fetch_env!("SECRET_KEY_BASE") + +config :jenot, secure_cookie: System.fetch_env!("SECURE_COOKIE") == "true" + config :web_push_elixir, vapid_public_key: System.fetch_env!("VAPID_PUBLIC_KEY"), vapid_private_key: System.fetch_env!("VAPID_PRIVATE_KEY"), diff --git a/config/test.exs b/config/test.exs index 1e38a63..26e2ac5 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,3 +1,5 @@ import Config config :jenot, Jenot.Repo, database: :memory + +config :bcrypt_elixir, :log_rounds, 4 diff --git a/lib/jenot.ex b/lib/jenot.ex index 9e3b7aa..1957267 100644 --- a/lib/jenot.ex +++ b/lib/jenot.ex @@ -1,18 +1,9 @@ defmodule Jenot do @moduledoc """ - Documentation for `Jenot`. + Note taking app. Spelled ye-not. """ - @doc """ - Hello world. - - ## Examples - - iex> Jenot.hello() - :world - - """ - def hello do - :world + def host() do + Application.fetch_env!(:jenot, :host) end end diff --git a/lib/jenot/account.ex b/lib/jenot/account.ex index 9a409e1..4755fef 100644 --- a/lib/jenot/account.ex +++ b/lib/jenot/account.ex @@ -3,14 +3,45 @@ defmodule Jenot.Account do import Ecto.Changeset + @code_length 10 + @primary_key {:id, :binary_id, autogenerate: true} schema "accounts" do + field :name, :string + field :code_digest, :string + timestamps(type: :utc_datetime_usec) end - def new() do + def new(code) do + id = Ecto.UUID.generate() + %__MODULE__{} |> change() - |> put_change(:id, Ecto.UUID.generate()) + |> put_change(:id, id) + |> put_change(:name, Hahash.name(id)) + |> put_change(:code_digest, hash(code)) + end + + def match?(account, input_code) do + Bcrypt.verify_pass(input_code, account.code_digest) + end + + @safe_disambiguations %{ + "O" => "8", + "I" => "7" + } + + def generate_code() do + :crypto.strong_rand_bytes(6) + |> Base.encode32(padding: false) + |> String.replace( + Map.keys(@safe_disambiguations), + &Map.fetch!(@safe_disambiguations, &1) + ) + end + + defp hash(code) when byte_size(code) == @code_length do + Bcrypt.hash_pwd_salt(code) end end diff --git a/lib/jenot/accounts.ex b/lib/jenot/accounts.ex index a6b141f..41b97fa 100644 --- a/lib/jenot/accounts.ex +++ b/lib/jenot/accounts.ex @@ -2,7 +2,100 @@ defmodule Jenot.Accounts do alias Jenot.Account alias Jenot.Repo - def get() do - Repo.one(Account) + @cookie_name "jenot" + @cookie_seconds 30 * 24 * 60 * 60 + + def get_by_cookie(conn) do + with {:ok, %{account_id: id}} <- get_cookie(conn) do + get_by_id(id) + end + end + + def authenticate(name, code) do + with {:ok, account} <- get_by_name(name), + :ok <- validate_code(account, code) do + {:ok, account} + end + end + + def new() do + code = Account.generate_code() + account = Account.new(code) + + case Repo.insert(account) do + {:ok, account} -> {:ok, %{code: code, account: account}} + {:error, _} -> {:error, :account_creation_failed} + end + end + + def set_cookie(conn, account_id) do + conn + |> Plug.Conn.put_resp_cookie(@cookie_name, %{account_id: account_id}, + domain: Jenot.host(), + secure: secure_cookie(), + encrypt: true, + max_age: @cookie_seconds, + same_site: "Strict" + ) + |> Plug.Conn.put_resp_cookie("#{@cookie_name}_pub", "true", + domain: Jenot.host(), + http_only: false, + max_age: @cookie_seconds, + same_site: "Strict" + ) + end + + def clear_cookie(conn) do + Plug.Conn.delete_resp_cookie(conn, @cookie_name, + domain: Jenot.host(), + secure: secure_cookie(), + encrypt: true, + max_age: @cookie_seconds, + same_site: "Strict" + ) + |> Plug.Conn.delete_resp_cookie("#{@cookie_name}_pub", + domain: Jenot.host(), + http_only: false, + max_age: @cookie_seconds, + same_site: "Strict" + ) + end + + defp get_by_name(name) do + case Repo.get_by(Account, name: name) do + nil -> + # dummy calculation to prevent timing attacks + Bcrypt.no_user_verify() + {:error, :account_not_found} + + account -> + {:ok, account} + end + end + + defp validate_code(account, code) do + if Account.match?(account, code) do + :ok + else + {:error, :account_not_found} + end + end + + defp get_cookie(conn) do + conn + |> Plug.Conn.fetch_cookies(encrypted: [@cookie_name]) + |> Map.fetch!(:cookies) + |> Map.fetch(@cookie_name) + end + + defp get_by_id(id) do + case Repo.get(Account, id) do + nil -> {:error, :account_not_found} + account -> {:ok, account} + end + end + + def secure_cookie() do + Application.fetch_env!(:jenot, :secure_cookie) end end diff --git a/lib/jenot/templates/account_authenticate.html.heex b/lib/jenot/templates/account_authenticate.html.heex new file mode 100644 index 0000000..acaff38 --- /dev/null +++ b/lib/jenot/templates/account_authenticate.html.heex @@ -0,0 +1,48 @@ + + + + + + Jenot + + + + + + +
+

Jenot

+ + <%= if error do %> +

<%= error %>

+ <% end %> +
+
+ + +
+
+ + +
+ + +
+ Back to Jenot +
+ + diff --git a/lib/jenot/templates/new_account.html.heex b/lib/jenot/templates/new_account.html.heex new file mode 100644 index 0000000..13b8a32 --- /dev/null +++ b/lib/jenot/templates/new_account.html.heex @@ -0,0 +1,43 @@ + + + + + + Jenot + + + + + + +
+

Jenot

+ +
+
+ + +
+
+ + +
+
+ Open Jenot +
+ + diff --git a/lib/jenot/web.ex b/lib/jenot/web.ex index d086aea..dd2aa30 100644 --- a/lib/jenot/web.ex +++ b/lib/jenot/web.ex @@ -8,6 +8,18 @@ defmodule Jenot.Web do alias Jenot.Accounts alias Jenot.Notes + @template_dir "lib/jenot/templates" + + @templates @template_dir + |> File.ls!() + |> Enum.map(fn file -> + { + String.replace_suffix(file, ".html.heex", ".html"), + File.read!(Path.join(@template_dir, file)) + } + end) + |> Map.new() + if Mix.env() == :dev do plug PlugLiveReload end @@ -26,6 +38,8 @@ defmodule Jenot.Web do pass: ["*/*"], json_decoder: Jason + plug :put_secret_key_base + plug :match plug :dispatch @@ -35,6 +49,47 @@ defmodule Jenot.Web do |> put_resp_header("location", "/index.html") end + post "/account/new" do + case Accounts.new() do + {:ok, %{code: code, account: account}} -> + conn + |> Accounts.set_cookie(account.id) + |> render("new_account.html", name: account.name, code: code) + + {:error, _} -> + conn + |> Accounts.clear_cookie() + |> resp(:found, "") + |> put_resp_header("location", "/index.html") + end + end + + get "/account/authenticate" do + render(conn, "account_authenticate.html", error: nil) + end + + post "/account/authenticate" do + case Accounts.authenticate(conn.params["name"], conn.params["code"]) do + {:ok, account} -> + conn + |> Accounts.set_cookie(account.id) + |> resp(:found, "") + |> put_resp_header("location", "/index.html?reset-meta") + + {:error, _} -> + conn + |> put_status(422) + |> render("account_authenticate.html", error: "Account not found") + end + end + + post "/account/logout" do + conn + |> Accounts.clear_cookie() + |> resp(:found, "") + |> put_resp_header("location", "/index.html?reset-meta") + end + get "/api" do send_resp(conn, 200, """ Jenot API says hi! @@ -42,13 +97,13 @@ defmodule Jenot.Web do end get "/api/latest" do - account = Accounts.get() + {:ok, account} = Accounts.get_by_cookie(conn) send_resp(conn, 200, Jason.encode!(%{notes: Notes.latest_change(account)})) end get "/api/notes" do - account = Accounts.get() + {:ok, account} = Accounts.get_by_cookie(conn) notes = account @@ -59,7 +114,7 @@ defmodule Jenot.Web do end get "/api/notes/:internal_id" do - account = Accounts.get() + {:ok, account} = Accounts.get_by_cookie(conn) case Notes.note_by_internal_id(account, internal_id) do {:ok, note} -> @@ -72,7 +127,7 @@ defmodule Jenot.Web do end post "/api/notes" do - account = Accounts.get() + {:ok, account} = Accounts.get_by_cookie(conn) case Notes.add(account, conn.params) do {:ok, note} -> @@ -84,7 +139,7 @@ defmodule Jenot.Web do end put "/api/notes/:internal_id" do - account = Accounts.get() + {:ok, account} = Accounts.get_by_cookie(conn) case Notes.update(account, internal_id, conn.params) do {:ok, note} -> @@ -99,7 +154,7 @@ defmodule Jenot.Web do end delete "/api/notes/:internal_id" do - account = Accounts.get() + {:ok, account} = Accounts.get_by_cookie(conn) :ok = Notes.delete(account, internal_id) @@ -126,4 +181,17 @@ defmodule Jenot.Web do defp service_worker_header(conn, _opts) do put_resp_header(conn, "service-worker-allowed", "/") end + + defp render(%{status: status} = conn, template, assigns) do + body = + @templates + |> Map.fetch!(template) + |> EEx.eval_string(assigns) + + send_resp(conn, status || 200, body) + end + + def put_secret_key_base(conn, _) do + put_in(conn.secret_key_base, Application.fetch_env!(:jenot, :secret_key_base)) + end end diff --git a/mix.exs b/mix.exs index 4cefaed..a47d451 100644 --- a/mix.exs +++ b/mix.exs @@ -37,6 +37,8 @@ defmodule Jenot.MixProject do [ {:burrito, "~> 1.0"}, {:bandit, "~> 1.0"}, + {:bcrypt_elixir, "~> 3.0"}, + {:hahash, "~> 0.2.0"}, {:web_push_elixir, "~> 0.4.0"}, {:ecto_sqlite3, "~> 0.17"}, {:plug_live_reload, "~> 0.1.0", only: :dev} diff --git a/mix.lock b/mix.lock index 29f4d34..249941e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,10 @@ %{ "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, + "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"}, "burrito": {:hex, :burrito, "1.2.0", "88f973469edcb96bd984498fb639d3fc4dbf01b52baab072b40229f03a396789", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "7e22158023c6558de615795ab135d27f0cbd9a0602834e3e474fe41b448afba9"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, @@ -16,6 +18,7 @@ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hahash": {:hex, :hahash, "0.2.0", "5d21c44b1b65d1f591adfd20ea4720e8a81db3caa377c28b20b434b8afc2115f", [:mix], [], "hexpm", "c39ce3f6163bbcf668e7a0bef88b608a9f37f99fcf181e7a17e4d10d02a5fbbd"}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, diff --git a/priv/repo/migrations/20241117180924_initial_schema.exs b/priv/repo/migrations/20241117180924_initial_schema.exs index ea746d4..5088c6f 100644 --- a/priv/repo/migrations/20241117180924_initial_schema.exs +++ b/priv/repo/migrations/20241117180924_initial_schema.exs @@ -4,6 +4,8 @@ defmodule Jenot.Repo.Migrations.InitialSchema do def change do create table(:accounts, primary_key: false) do add :id, :uuid, primary_key: true + add :name, :text, null: false + add :code_digest, :text, null: false timestamps(type: :datetime_usec) end diff --git a/priv/repo/migrations/20241124160809_add_default_account.exs b/priv/repo/migrations/20241124160809_add_default_account.exs deleted file mode 100644 index 979f0a2..0000000 --- a/priv/repo/migrations/20241124160809_add_default_account.exs +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Jenot.Repo.Migrations.AddDefaultAccount do - use Ecto.Migration - - def change do - id = Ecto.UUID.generate() - now = DateTime.utc_now() |> DateTime.to_iso8601() - - execute """ - INSERT INTO accounts (id, inserted_at, updated_at) - VALUES ('#{id}', '#{now}', '#{now}') - """, - """ - DELETE FROM accounts - """ - end -end diff --git a/priv/static/index.html b/priv/static/index.html index c9f065f..70b6546 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -30,6 +30,13 @@

Jenot

+ + + diff --git a/priv/static/js/jenot.js b/priv/static/js/jenot.js index a6a301c..437910b 100644 --- a/priv/static/js/jenot.js +++ b/priv/static/js/jenot.js @@ -1,6 +1,5 @@ import "./service-worker-init.js"; import { renderText } from "./dom.js"; -import { LocalNoteStore } from "./local-store.js"; import { SyncedNoteStore } from "./synced-store.js"; import { authorizeNotifications, @@ -11,12 +10,25 @@ import "./components.js"; const URL_PARAMS = new URLSearchParams(window.location.search); +// Cookie presence determines login state +const isLoggedIn = !!document.cookie + .split("; ") + .find((row) => row.startsWith("jenot_pub=")); + // Notes storage configuration. -// Currently supports either simple, local storage based implementation -// and a more elaborate one, using a combination of IndexedDB + network sync. -const Notes = URL_PARAMS.has("localStorage") - ? new LocalNoteStore("jenot-app") - : new SyncedNoteStore("jenot-app", "notes", "/"); +// The storage is a combination of IndexedDB + network sync. +// Network sync is only enabled is user is logged in. +const Notes = new SyncedNoteStore("jenot-app", "notes", isLoggedIn && "/"); + +// Reset metadata to force full sync +if (URL_PARAMS.has("reset-meta")) { + history.replaceState( + null, + "", + location.href.replace("&reset-meta", "").replace("?reset-meta", ""), + ); + await Notes.setMeta({ lastSync: null }); +} // Very rudimentary periodic sync. It will be refactored into a more real-time // solution using either websocket of long-polling, so that server can notify about @@ -26,7 +38,22 @@ const sync = async () => { Notes.saveStorage(); }; -setInterval(sync, 5000); +if (isLoggedIn) { + setInterval(sync, 5000); +} + +// New account provisioning and login/logout actions + +const newAccountForm = document.querySelector("#new-account-form"); +const loginLink = document.querySelector("#login-link"); +const logoutForm = document.querySelector("#logout-form"); + +if (!isLoggedIn) { + loginLink.classList.remove("hidden"); + newAccountForm.classList.remove("hidden"); +} else { + logoutForm.classList.remove("hidden"); +} // Notifications API test - to be reused for push notifications later on @@ -58,7 +85,9 @@ Notes.addEventListener("save", render.bind(this)); // Initial notes render and initial sync. render(); -sync(); +if (isLoggedIn) { + sync(); +} // note-form component specific event handlers newNote.addEventListener("addNote", async (e) => { diff --git a/priv/static/js/local-store.js b/priv/static/js/local-store.js deleted file mode 100644 index ca81a03..0000000 --- a/priv/static/js/local-store.js +++ /dev/null @@ -1,71 +0,0 @@ -export class LocalNoteStore extends EventTarget { - localStorageKey; - notes = []; - - /* - Note structure: - - - id - unique note ID - - type - either `note` or `tasklist` - - content - note's content - - created - timestamp - - updated - timestamp - */ - - constructor(localStorageKey) { - super(); - this.localStorageKey = localStorageKey; - - this.#readStorage(); - - // handle notes edited in another window - window.addEventListener( - "storage", - () => { - this.#readStorage(); - this.saveStorage(); - }, - false, - ); - } - - all = () => this.notes; - get = (id) => this.notes.find((note) => note.id === id); - - add(note) { - const now = Date.now(); - - this.notes.unshift({ - id: "id_" + now, - type: note.type, - content: note.content, - created: now, - updated: now, - }); - } - - remove({ id }) { - this.notes = this.notes.filter((note) => note.id !== id); - } - - update(note) { - note.updated = Date.now(); - this.notes = this.notes.map((n) => (n.id === note.id ? note : n)); - } - - sync() {} - - saveStorage() { - window.localStorage.setItem( - this.localStorageKey + "_notes", - JSON.stringify(this.notes), - ); - this.dispatchEvent(new CustomEvent("save")); - } - - #readStorage() { - this.notes = JSON.parse( - window.localStorage.getItem(this.localStorageKey + "_notes") || "[]", - ); - } -} diff --git a/priv/static/js/synced-store.js b/priv/static/js/synced-store.js index 0e04e3d..f065c13 100644 --- a/priv/static/js/synced-store.js +++ b/priv/static/js/synced-store.js @@ -123,7 +123,7 @@ export class SyncedNoteStore extends EventTarget { async setMeta(meta) { meta.id = "meta"; - this.update(meta, true); + return this.update(meta, true); } async add(note) { @@ -193,7 +193,7 @@ export class SyncedNoteStore extends EventTarget { const lastSync = meta?.lastSync; const currentSync = Date.now(); - this.all(lastSync, true) + return this.all(lastSync, true) .then((notes) => { return Promise.all(notes.map((n) => that.webStore.add(n))); })