From 8ccfa5c3fb35be628b0f4e6b8375b8d2f4c7acb6 Mon Sep 17 00:00:00 2001 From: nimbleghost <132819643+nimbleghost@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:00:51 +0200 Subject: [PATCH 1/2] Fix session replica behaviour (merge with session) The harder-to-refactor parts are the places where exists/username/token are called within a React component. However, `resetAndRedirect` and `store` are already called from async contexts, so adding an `await` is simple. This thus merges the logic, keeping localStorage for the components to call, but making sure reset/store behaviour works correctly for the replica. --- web/src/app/AccountApi.js | 2 +- web/src/app/Session.js | 29 ++++++++++------- web/src/app/SessionReplica.js | 41 ------------------------ web/src/app/db.js | 3 +- web/src/components/Account.jsx | 18 +++++------ web/src/components/ActionBar.jsx | 2 +- web/src/components/Login.jsx | 2 +- web/src/components/Preferences.jsx | 2 +- web/src/components/PublishDialog.jsx | 2 +- web/src/components/ReserveDialogs.jsx | 6 ++-- web/src/components/Signup.jsx | 2 +- web/src/components/SubscribeDialog.jsx | 4 +-- web/src/components/SubscriptionPopup.jsx | 4 +-- web/src/components/UpgradeDialog.jsx | 2 +- web/src/components/hooks.js | 2 +- 15 files changed, 43 insertions(+), 78 deletions(-) delete mode 100644 web/src/app/SessionReplica.js diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index 572764fe..d9380438 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -367,7 +367,7 @@ class AccountApi { } catch (e) { console.log(`[AccountApi] Error fetching account`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } return undefined; } diff --git a/web/src/app/Session.js b/web/src/app/Session.js index bc50864b..82249b06 100644 --- a/web/src/app/Session.js +++ b/web/src/app/Session.js @@ -1,29 +1,36 @@ -import sessionReplica from "./SessionReplica"; +import Dexie from "dexie"; /** * Manages the logged-in user's session and access token. * The session replica is stored in IndexedDB so that the service worker can access it. */ class Session { - constructor(replica) { - this.replica = replica; + constructor() { + const db = new Dexie("session-replica"); + db.version(1).stores({ + kv: "&key", + }); + this.db = db; } - store(username, token) { + async store(username, token) { + await this.db.kv.bulkPut([ + { key: "user", value: username }, + { key: "token", value: token }, + ]); localStorage.setItem("user", username); localStorage.setItem("token", token); - this.replica.store(username, token); } - reset() { + async resetAndRedirect(url) { + await this.db.delete(); localStorage.removeItem("user"); localStorage.removeItem("token"); - this.replica.reset(); + window.location.href = url; } - resetAndRedirect(url) { - this.reset(); - window.location.href = url; + async usernameAsync() { + return (await this.db.kv.get({ key: "user" }))?.value; } exists() { @@ -39,5 +46,5 @@ class Session { } } -const session = new Session(sessionReplica); +const session = new Session(); export default session; diff --git a/web/src/app/SessionReplica.js b/web/src/app/SessionReplica.js deleted file mode 100644 index a68d4c70..00000000 --- a/web/src/app/SessionReplica.js +++ /dev/null @@ -1,41 +0,0 @@ -import Dexie from "dexie"; - -/** - * Replica of the session in IndexedDB. This is used by the service - * worker to access the session. This is a bit of a hack. - */ -class SessionReplica { - constructor() { - const db = new Dexie("session-replica"); - db.version(1).stores({ - kv: "&key", - }); - this.db = db; - } - - async store(username, token) { - try { - await this.db.kv.bulkPut([ - { key: "user", value: username }, - { key: "token", value: token }, - ]); - } catch (e) { - console.error("[Session] Error replicating session to IndexedDB", e); - } - } - - async reset() { - try { - await this.db.delete(); - } catch (e) { - console.error("[Session] Error resetting session on IndexedDB", e); - } - } - - async username() { - return (await this.db.kv.get({ key: "user" }))?.value; - } -} - -const sessionReplica = new SessionReplica(); -export default sessionReplica; diff --git a/web/src/app/db.js b/web/src/app/db.js index 6a192011..357f4e96 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -1,6 +1,5 @@ import Dexie from "dexie"; import session from "./Session"; -import sessionReplica from "./SessionReplica"; // Uses Dexie.js // https://dexie.org/docs/API-Reference#quick-reference @@ -23,7 +22,7 @@ const createDatabase = (username) => { }; export const dbAsync = async () => { - const username = await sessionReplica.username(); + const username = await session.usernameAsync(); return createDatabase(username); }; diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx index 47449515..a6a624b8 100644 --- a/web/src/components/Account.jsx +++ b/web/src/components/Account.jsx @@ -164,7 +164,7 @@ const ChangePasswordDialog = (props) => { if (e instanceof IncorrectPasswordError) { setError(t("account_basics_password_dialog_current_password_incorrect")); } else if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); } @@ -245,7 +245,7 @@ const AccountType = () => { } catch (e) { console.log(`[Account] Error opening billing portal`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setShowPortalError(true); } @@ -371,7 +371,7 @@ const PhoneNumbers = () => { } catch (e) { console.log(`[Account] Error deleting phone number`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } } }; @@ -447,7 +447,7 @@ const AddPhoneNumberDialog = (props) => { } catch (e) { console.log(`[Account] Error sending verification`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); } @@ -464,7 +464,7 @@ const AddPhoneNumberDialog = (props) => { } catch (e) { console.log(`[Account] Error confirming verification`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); } @@ -946,7 +946,7 @@ const TokenDialog = (props) => { } catch (e) { console.log(`[Account] Error creating token`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); } @@ -1003,7 +1003,7 @@ const TokenDeleteDialog = (props) => { } catch (e) { console.log(`[Account] Error deleting token`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); } @@ -1080,13 +1080,13 @@ const DeleteAccountDialog = (props) => { await accountApi.delete(password); await db().delete(); console.debug(`[Account] Account deleted`); - session.resetAndRedirect(routes.app); + await session.resetAndRedirect(routes.app); } catch (e) { console.log(`[Account] Error deleting account`, e); if (e instanceof IncorrectPasswordError) { setError(t("account_basics_password_dialog_current_password_incorrect")); } else if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); } diff --git a/web/src/components/ActionBar.jsx b/web/src/components/ActionBar.jsx index a8cb18ce..a16022e4 100644 --- a/web/src/components/ActionBar.jsx +++ b/web/src/components/ActionBar.jsx @@ -123,7 +123,7 @@ const ProfileIcon = () => { await accountApi.logout(); await db().delete(); } finally { - session.resetAndRedirect(routes.app); + await session.resetAndRedirect(routes.app); } }; diff --git a/web/src/components/Login.jsx b/web/src/components/Login.jsx index 489eee0f..4efec255 100644 --- a/web/src/components/Login.jsx +++ b/web/src/components/Login.jsx @@ -24,7 +24,7 @@ const Login = () => { try { const token = await accountApi.login(user); console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`); - session.store(user.username, token); + await session.store(user.username, token); window.location.href = routes.app; } catch (e) { console.log(`[Login] User auth for user ${user.username} failed`, e); diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx index 5cac0c5a..5a68a8ed 100644 --- a/web/src/components/Preferences.jsx +++ b/web/src/components/Preferences.jsx @@ -59,7 +59,7 @@ const maybeUpdateAccountSettings = async (payload) => { } catch (e) { console.log(`[Preferences] Error updating account settings`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } } }; diff --git a/web/src/components/PublishDialog.jsx b/web/src/components/PublishDialog.jsx index eb0af0dd..0929a5e9 100644 --- a/web/src/components/PublishDialog.jsx +++ b/web/src/components/PublishDialog.jsx @@ -211,7 +211,7 @@ const PublishDialog = (props) => { } catch (e) { console.log(`[PublishDialog] Retrieving attachment limits failed`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setAttachFileError(""); // Reset error (rely on server-side checking) } diff --git a/web/src/components/ReserveDialogs.jsx b/web/src/components/ReserveDialogs.jsx index 3dc370e6..e413657a 100644 --- a/web/src/components/ReserveDialogs.jsx +++ b/web/src/components/ReserveDialogs.jsx @@ -43,7 +43,7 @@ export const ReserveAddDialog = (props) => { } catch (e) { console.log(`[ReserveAddDialog] Error adding topic reservation.`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else if (e instanceof TopicReservedError) { setError(t("subscribe_dialog_error_topic_already_reserved")); return; @@ -99,7 +99,7 @@ export const ReserveEditDialog = (props) => { } catch (e) { console.log(`[ReserveEditDialog] Error updating topic reservation.`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); return; @@ -136,7 +136,7 @@ export const ReserveDeleteDialog = (props) => { } catch (e) { console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); return; diff --git a/web/src/components/Signup.jsx b/web/src/components/Signup.jsx index 3b82cd61..2e97956f 100644 --- a/web/src/components/Signup.jsx +++ b/web/src/components/Signup.jsx @@ -27,7 +27,7 @@ const Signup = () => { await accountApi.create(user.username, user.password); const token = await accountApi.login(user); console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`); - session.store(user.username, token); + await session.store(user.username, token); window.location.href = routes.app; } catch (e) { console.log(`[Signup] Signup for user ${user.username} failed`, e); diff --git a/web/src/components/SubscribeDialog.jsx b/web/src/components/SubscribeDialog.jsx index fedccc39..e777d873 100644 --- a/web/src/components/SubscribeDialog.jsx +++ b/web/src/components/SubscribeDialog.jsx @@ -39,7 +39,7 @@ export const subscribeTopic = async (baseUrl, topic, opts) => { } catch (e) { console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } } } @@ -124,7 +124,7 @@ const SubscribePage = (props) => { } catch (e) { console.log(`[SubscribeDialog] Error reserving topic`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else if (e instanceof TopicReservedError) { setError(t("subscribe_dialog_error_topic_already_reserved")); return; diff --git a/web/src/components/SubscriptionPopup.jsx b/web/src/components/SubscriptionPopup.jsx index 74438f9a..7d154ef5 100644 --- a/web/src/components/SubscriptionPopup.jsx +++ b/web/src/components/SubscriptionPopup.jsx @@ -155,7 +155,7 @@ export const SubscriptionPopup = (props) => { } catch (e) { console.log(`[SubscriptionPopup] Error unsubscribing`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } } } @@ -298,7 +298,7 @@ const DisplayNameDialog = (props) => { } catch (e) { console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); return; diff --git a/web/src/components/UpgradeDialog.jsx b/web/src/components/UpgradeDialog.jsx index a554f1f8..6d569fa2 100644 --- a/web/src/components/UpgradeDialog.jsx +++ b/web/src/components/UpgradeDialog.jsx @@ -140,7 +140,7 @@ const UpgradeDialog = (props) => { } catch (e) { console.log(`[UpgradeDialog] Error changing billing subscription`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } else { setError(e.message); } diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 8da8fdf0..792a1126 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -114,7 +114,7 @@ export const useAutoSubscribe = (subscriptions, selected) => { } catch (e) { console.log(`[Hooks] Auto-subscribing failed`, e); if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); + await session.resetAndRedirect(routes.login); } } } From 390d42c6076934a25205d5e3a98f1fd74de35ecd Mon Sep 17 00:00:00 2001 From: nimbleghost <132819643+nimbleghost@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:02:54 +0200 Subject: [PATCH 2/2] Format & fix lint --- web/src/app/Api.js | 3 +-- web/src/app/Prefs.js | 4 ++-- web/src/app/SubscriptionManager.js | 5 ++--- web/src/app/UserManager.js | 4 ++-- web/src/app/db.js | 2 +- web/src/components/Preferences.jsx | 6 +++++- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/web/src/app/Api.js b/web/src/app/Api.js index 8b7fc79b..afe59c7c 100644 --- a/web/src/app/Api.js +++ b/web/src/app/Api.js @@ -132,7 +132,6 @@ class Api { }); } - async deleteWebPush(pushSubscription) { const user = await userManager.get(config.base_url); const url = accountWebPushUrl(config.base_url); @@ -141,7 +140,7 @@ class Api { method: "DELETE", headers: maybeWithAuth({}, user), body: JSON.stringify({ - endpoint: pushSubscription.endpoint + endpoint: pushSubscription.endpoint, }), }); } diff --git a/web/src/app/Prefs.js b/web/src/app/Prefs.js index b4cef0ac..4826e061 100644 --- a/web/src/app/Prefs.js +++ b/web/src/app/Prefs.js @@ -1,8 +1,8 @@ import db from "./db"; class Prefs { - constructor(db) { - this.db = db; + constructor(dbImpl) { + this.db = dbImpl; } async setSound(sound) { diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index b9e5d083..5b876ae1 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -5,8 +5,8 @@ import db from "./db"; import { topicUrl } from "./utils"; class SubscriptionManager { - constructor(db) { - this.db = db; + constructor(dbImpl) { + this.db = dbImpl; } /** All subscriptions, including "new count"; this is a JOIN, see https://dexie.org/docs/API-Reference#joining */ @@ -124,7 +124,6 @@ class SubscriptionManager { } else { await api.deleteWebPush(browserSubscription); } - } async updateState(subscriptionId, state) { diff --git a/web/src/app/UserManager.js b/web/src/app/UserManager.js index 412e41da..b53b1da8 100644 --- a/web/src/app/UserManager.js +++ b/web/src/app/UserManager.js @@ -2,8 +2,8 @@ import db from "./db"; import session from "./Session"; class UserManager { - constructor(db) { - this.db = db; + constructor(dbImpl) { + this.db = dbImpl; } async all() { diff --git a/web/src/app/db.js b/web/src/app/db.js index 357f4e96..77ac2562 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -26,6 +26,6 @@ export const dbAsync = async () => { return createDatabase(username); }; -export const db = () => createDatabase(session.username()); +const db = () => createDatabase(session.username()); export default db; diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx index 5a68a8ed..eeb6ee0c 100644 --- a/web/src/components/Preferences.jsx +++ b/web/src/components/Preferences.jsx @@ -247,7 +247,11 @@ const WebPushEnabled = () => { } return ( - +