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
+
Log in
+
+
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)));
})