Skip to content

Conversation

fm3
Copy link
Member

@fm3 fm3 commented Aug 12, 2025

URL of deployed dev instance (used for testing):

Steps to test:

  • Log into webknossos with the same account in two incognito tabs.
  • Go to account settings > security and log out everywhere
  • Open the other tab. Upon the next request, you should see at least one error toast, pointing out that you need to refresh the page. Upon the refresh, you will be logged out.
  • Check that this toast does not appear too often. Do this e.g. by sharing a publicly available dataset and opening it without being logged in.

TODOs:

  • Backend
    • Introduce new column to indicate loggedOutEverywhereTime
    • Route to set the value
    • Cookie validation/retreival should respect this value
  • Frontend
    • Button to trigger this (probably somewhere in the Account Settings pages? probably with confirmation modal?) navigate to /auth/login afterwards, just like in normal logout

Issues:


  • Added changelog entry (create a $PR_NUMBER.md file in unreleased_changes or use ./tools/create-changelog-entry.py)
  • Added migration guide entry if applicable (edit the same file as for the changelog)
  • Considered common edge cases

@fm3 fm3 self-assigned this Aug 12, 2025
Copy link
Contributor

coderabbitai bot commented Aug 12, 2025

📝 Walkthrough

Walkthrough

Adds a logout-everywhere feature: new controller endpoint and route, DAO and model additions with DB migration, token deletion and cookie invalidation logic, frontend API/UI to trigger it, routing updates, centralized request error handling, and a minor ShortLink action auth change.

Changes

Cohort / File(s) Summary
Backend controller & routes
app/controllers/AuthenticationController.scala, conf/webknossos.latest.routes
Added logoutEverywhere action and POST route; changed /auth/logout to POST.
User model & DAO
app/models/user/User.scala
Added loggedOutEverywhereTime field; insertOne persists it; added findIdsByMultiUserId and logOutEverywhereByMultiUserId; removed countIdentitiesForMultiUser.
User service cache
app/models/user/UserService.scala
removeUserFromCache visibility changed from private to public.
Authenticator logic
app/security/CombinedAuthenticatorService.scala
Cookie validation now checks cookie timestamp vs. user loggedOutEverywhereTime and falls back to token auth when invalidated.
Token DAO
app/security/Token.scala
Added deleteDataStoreTokensForMultiUser to soft-delete DataStore tokens for a multi-user.
DB migrations & schema
conf/evolutions/139-logout-everywhere.sql, conf/evolutions/reversions/139-logout-everywhere.sql, tools/postgres/schema.sql
Added loggedOutEverywhereTime column, updated/recreated dependent views, bumped schema version to 139, and provided revert script.
Frontend API
frontend/javascripts/admin/rest_api.ts
logoutUser now POSTs; added logoutUserEverywhere() which POSTs /api/auth/logoutEverywhere.
Account security UI & routing
frontend/javascripts/admin/account/account_security_view.tsx, frontend/javascripts/admin/account/account_settings_view.tsx, frontend/javascripts/router/router.tsx
Replaced Password view with Security view, added “Log out everywhere” UI and flows, updated route keys and redirects (password → security) and added multiple replace-based navigations.
Email flows
frontend/javascripts/admin/auth/change_email_view.tsx, frontend/javascripts/admin/auth/verify_email_view.tsx
After email change, call logoutUserEverywhere, dispatch logout, and navigate to login; updated a type import path.
Request/error handling & toasts
frontend/javascripts/libs/request.ts, frontend/javascripts/libs/handle_request_error_helper.tsx, frontend/javascripts/libs/handle_http_status.ts, frontend/javascripts/libs/toast.tsx, frontend/javascripts/types/api_types.ts
Extracted centralized handleError helper, removed ServerErrorMessage from request.ts and added it to types/api_types.ts, added showToastOnce debounce helper, small formatting edits.
Short links
app/controllers/ShortLinkController.scala
create action changed from SecuredAction to UserAwareAction (auth context change); signature unchanged.
Changelog
unreleased_changes/8850.md
Added unreleased note for logout-everywhere and referenced migration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement server-side functionality to log out all sessions of a user [#8846]
Expose an endpoint to trigger logout-everywhere [#8846]
Ensure existing sessions/cookies are invalidated after global logout [#8846]
Provide a frontend action to trigger logout-everywhere [#8846]

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Change ShortLink create action to UserAwareAction (app/controllers/ShortLinkController.scala) Alters auth wrapper for short-link creation; unrelated to logout-everywhere objectives.
Router: publication and keyboard-shortcuts loader-based redirects (frontend/javascripts/router/router.tsx) Routing redirect refactors unrelated to logout-everywhere feature.
Centralized request error handling and toast throttle (frontend/javascripts/libs/request.ts, frontend/javascripts/libs/handle_request_error_helper.tsx, frontend/javascripts/libs/toast.tsx) Broad error-handling refactor that is not required by logout-everywhere objective.
Type import relocation in verify_email_view (frontend/javascripts/admin/auth/verify_email_view.tsx) Minor type import change not related to logout-everywhere.

Possibly related PRs

Suggested reviewers

  • MichaelBuessemeyer

Poem

I hop from cache to token burrow,
Snipping sessions, none to follow.
A whisker twitch, the cookies fade,
Across all warrens, logout's made.
I thump once — safe paths cleared away. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch logout-everywhere

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@knollengewaechs knollengewaechs marked this pull request as ready for review August 21, 2025 15:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/javascripts/router/router.tsx (1)

378-378: Fix broken backward-compat redirect for change password

The target “/account/password” no longer exists (renamed to “/account/security”). This redirect currently produces a 404.

Apply this diff:

-    <Route path="/auth/changePassword" element={<Navigate to="/account/password" />} />
+    <Route path="/auth/changePassword" element={<Navigate to="/account/security" />} />
frontend/javascripts/admin/account/account_security_view.tsx (1)

25-37: Chain mixes then/await; error message conflates failures.

If logoutUserEverywhere fails, the catch labels it “Password change failed,” which is misleading. Rewrite onFinish with async/await and separate handling. Also, rely on centralized Request toasts; avoid double error toasts here.

-  function onFinish(formValues: Record<string, any>) {
-    changePassword(formValues)
-      .then(async () => {
-        Toast.success(messages["auth.reset_pw_confirmation"]);
-        await logoutUserEverywhere();
-        Store.dispatch(logoutUserAction());
-        navigate("/auth/login");
-      })
-      .catch((error) => {
-        console.error("Password change failed:", error);
-        Toast.error("Failed to change password. Please try again.");
-      });
-  }
+  async function onFinish(formValues: Record<string, any>) {
+    try {
+      await changePassword(formValues);
+      Toast.success(messages["auth.reset_pw_confirmation"]);
+      try {
+        await logoutUserEverywhere();
+      } catch (e) {
+        // Centralized Request handler already showed a toast; keep UX simple
+        console.warn("Logout-everywhere after password change failed:", e);
+      }
+      Store.dispatch(logoutUserAction());
+      navigate("/auth/login");
+    } catch (error) {
+      console.error("Password change failed:", error);
+      // Centralized Request handler will have shown a toast; optionally keep a generic one:
+      Toast.error("Failed to change password. Please try again.");
+    }
+  }
🧹 Nitpick comments (18)
app/models/user/UserService.scala (1)

220-221: Public removeUserFromCache is fine; tighten predicate and consider a batched variant for “logout everywhere.”

  • Visibility widening looks intentional for the new flow. Minor readability: match on the tuple to avoid magic indices.
  • For performance, when invalidating many user IDs (multi-user), provide a single-shot batch clear to avoid multiple cache scans.

Apply this small readability diff:

 def removeUserFromCache(userId: ObjectId): Unit =
-  userCache.clear(idAndAccessContextString => idAndAccessContextString._1 == userId)
+  userCache.clear { case (id, _) => id == userId }

Optionally add a batched helper (outside the changed lines):

def removeUsersFromCache(userIds: Iterable[ObjectId]): Unit = {
  val ids = userIds.toSet
  userCache.clear { case (id, _) => ids.contains(id) }
}

If AuthenticationController.logoutEverywhere currently iterates one-by-one, switching to the batched helper would reduce cache traversal work under high user counts.

frontend/javascripts/libs/toast.tsx (2)

3-3: Avoid importing all of lodash; import only debounce to cut bundle size.

Switching to a focused import prevents pulling in the full lodash build.

Apply:

-import _ from "lodash";
+import debounce from "lodash/debounce";

248-259: Global debounce will suppress unrelated toasts; add a keyed variant to throttle per-message.

The exported showToastOnce debounces all toasts across the entire app for 60s, which can hide different, legitimate toasts fired within that window. Keep showToastOnce for trivial cases, but add a keyed version that throttles by a semantic key (ideally the toast’s own config.key).

Minimal change within these lines (align with focused import above):

-export const showToastOnce = _.debounce(
+export const showToastOnce = debounce(
   (
     type: ToastStyle,
     message: React.ReactNode,
     config: ToastConfig = {},
     details?: string | undefined,
   ) => {
     Toast[type](message, config, details);
   },
   60000,
   { leading: true },
 );

Then add a keyed helper (outside the changed lines):

// Throttle per logical key (defaults to config.key, falling back to type+stringified message)
export const showToastOnceByKey = (() => {
  const debouncers = new Map<string, ReturnType<typeof debounce>>();
  return (
    type: ToastStyle,
    message: React.ReactNode,
    config: ToastConfig = {},
    details?: string | undefined,
  ) => {
    const fallbackMessageKey =
      typeof message === "string" ? message : JSON.stringify({ t: type, m: "node" });
    const key = config.key ?? `${type}:${fallbackMessageKey}`;
    let d = debouncers.get(key);
    if (!d) {
      d = debounce(
        (t: ToastStyle, m: React.ReactNode, c: ToastConfig, det?: string) => {
          Toast[t](m, { ...c, key }, det);
          // Clear debouncer so a new window starts after 60s
          debouncers.delete(key);
        },
        60000,
        { leading: true },
      );
      debouncers.set(key, d);
    }
    d(type, message, config, details);
  };
})();

Call sites that already provide unique config.key can use showToastOnceByKey to prevent spam without globally silencing other toasts.

app/models/user/User.scala (2)

168-170: Scope/ACL of findIdsByMultiUserId verified – safe to keep public

I ran the grep search for findIdsByMultiUserId and confirmed only two hits:

  • Its declaration in app/models/user/User.scala
  • Its single invocation in AuthenticationController.logoutEverywhere (app/controllers/AuthenticationController.scala:963)
    No other call sites exist, so there’s no unguarded bulk-ID exposure beyond your intended logout-everywhere flow.

Next steps:

  • If you leave it public (so the controller in a different package can call it), add a Scaladoc comment clarifying it’s only for cache eviction on logout-everywhere.
  • Optional refactor: move this into a small service or companion in the app.models.user package and mark it private[user] (or private[models.user]); you’d then update the controller to use that service.

590-595: Suggestion: Restrict logout‐everywhere updates to active users & capture row count

I checked the cookieUnlessSignedOutEverywhere logic in CombinedAuthenticatorService.scala – it already rejects cookies whose last‐used time is on or before the user’s loggedOutEverywhereTime, defaulting to Instant.ZERO when unset. That confirms the cookie path is gated correctly by the new column.

Next, I located where the token authenticator is wired up (using tokenAuthenticatorService.retrieve). It does not perform the same timestamp check, so bearer‐token sessions would remain valid across a global logout. We should mirror the cookie check for tokens as well.

• In User.scala (lines 590–595), modify the UPDATE:

-  _ <- run(
-        q"""UPDATE webknossos.users SET loggedOutEverywhereTime = ${Instant.now} WHERE _multiUser = $multiUserId""".asUpdate)
+  updated <- run(
+        q"""UPDATE webknossos.users
+            SET loggedOutEverywhereTime = ${Instant.now}
+            WHERE _multiUser = $multiUserId
+              AND NOT isDeleted""".asUpdate)
+  logger.info(s"logoutEverywhere: updated $updated rows for multiUser=$multiUserId")

• In the token‐validation path (e.g. wherever tokenAuthenticatorService.retrieve is accepted), add the same “greater‐than” comparison against loggedOutEverywhereTime, analogous to cookieUnlessSignedOutEverywhere.

That ensures both cookies and tokens are invalidated when a user logs out everywhere.

conf/evolutions/139-logout-everywhere.sql (1)

1-24: Migration is sound and transactional; add small resilience improvements

  • The migration wraps all changes in a transaction and asserts the prior schema version, preventing partial application.
  • Views are dropped and recreated in the correct order around the new TIMESTAMPTZ column.
  • An index on users(_multiUser) was already created in evolution 060-multiusers.sql, so no additional index DDL is needed here.
  • To ease local/dev rollbacks, use IF EXISTS on the DROP VIEW statements:
-DROP VIEW webknossos.userInfos;
-DROP VIEW webknossos.users_;
+DROP VIEW IF EXISTS webknossos.userInfos;
+DROP VIEW IF EXISTS webknossos.users_;
unreleased_changes/8850.md (1)

1-6: Grammar nit: improve changelog phrasing

Consider “log out of all devices” and clarify it’s accessible from Account Security.

-### Added
-- Added the option to log out from all devices.
+### Added
+- Added an option to log out of all devices (Account Settings → Security).
frontend/javascripts/admin/rest_api.ts (1)

2330-2330: Suppressing error toast for createShortLink — confirm UX intent

Silencing toasts can avoid duplicate messaging, but ensure failures are still surfaced (e.g., inline form error or console).

If you want to keep global silence but log for troubleshooting:

-      showErrorToast: false,
+      showErrorToast: false,
+      // on failure, Request.* helpers still reject the promise; callers should handle and log if needed
frontend/javascripts/admin/auth/change_email_view.tsx (1)

39-42: Make navigation resilient and update browser history after logout

Ensure the user is redirected to login even if the logoutEverywhere request fails (network flake) and prevent “Back” from re-entering the app with a stale page.

-        await logoutUserEverywhere();
-        Store.dispatch(logoutUserAction());
-        navigate("/auth/login");
+        try {
+          await logoutUserEverywhere();
+        } finally {
+          Store.dispatch(logoutUserAction());
+          navigate("/auth/login", { replace: true });
+        }

Optional: If logoutEverywhere fails, show a secondary toast (“Couldn’t complete ‘logout everywhere’; you’ve been logged out of this device.”) instead of the generic “changing the email address” error.

frontend/javascripts/libs/request.ts (1)

262-264: Type flow: catch handler now narrows the promise to Promise (from the helper).

Because handleError is declared to return Promise, fetchPromise’s inferred type after .catch(...) becomes Promise. It still rejects correctly, but you lose value typing for callers. Consider adjusting the helper to return Promise to reflect that it always rejects (see suggested diff in handle_request_error_helper.tsx).

conf/evolutions/reversions/139-logout-everywhere.sql (2)

5-7: Make drops idempotent and robust.

Using IF EXISTS helps when environments drift or when partial rollbacks happen. Consider adding CASCADE only if you expect dependent objects that can be safely recreated below.

-DROP VIEW webknossos.userInfos;
-DROP VIEW webknossos.users_;
+DROP VIEW IF EXISTS webknossos.userInfos;
+DROP VIEW IF EXISTS webknossos.users_;

10-20: Prefer explicit column lists in views.

SELECT * in views makes them fragile to table shape changes. Listing columns for webknossos.users_ and userInfos improves forward/backward compatibility during rolling deploys.

frontend/javascripts/libs/handle_request_error_helper.tsx (3)

5-10: This helper always rejects; type it as Promise (not Promise).

The function never resolves; it only rejects. Returning Promise preserves downstream typing (see Request.triggerRequest catch). Minor but useful for type safety.

-export const handleError = async (
+export const handleError = async (
   requestedUrl: string,
   showErrorToast: boolean,
   doInvestigate: boolean,
   error: Response | Error,
-): Promise<void> => {
+): Promise<never> => {
@@
-  return Promise.reject(error);
+  return Promise.reject(error);

Also applies to: 84-85


17-29: 401 UX: consider redirecting to login (optional).

For global logout and expired sessions, showing a sticky “Refresh” toast is okay. Given the “logout everywhere” feature, you may want to also route to /auth/login (or dispatch logout) on 401 to reduce confusion. If you keep the toast, that’s fine; just a UX suggestion.


78-82: Minor: ensure error is an Error before mutating message.

The instanceof check ensures it’s not a Response, but extremely rarely non-Error throwables can occur. Narrow with instanceof Error for safety; otherwise ignore.

-  if (!(error instanceof Response)) {
+  if (!(error instanceof Response) && error instanceof Error) {
     error.message += ` - Url: ${requestedUrl}`;
   }
frontend/javascripts/admin/account/account_security_view.tsx (3)

185-195: Simplify handleLogout with async/await; close modal; avoid duplicate toasts.

Use await/try-catch, close the modal on success, and let Request’s centralized handler show errors instead of adding another toast.

-  async function handleLogout() {
-    logoutUserEverywhere()
-      .then(() => {
-        Store.dispatch(logoutUserAction());
-        navigate("/login");
-      })
-      .catch((error) => {
-        Toast.error("Failed to log out. See console for more details");
-        console.error("Logout failed:", error);
-      });
-  }
+  async function handleLogout() {
+    try {
+      await logoutUserEverywhere();
+      setShowConfirmLogoutModal(false);
+      Store.dispatch(logoutUserAction());
+      navigate("/auth/login");
+    } catch (error) {
+      console.error("Logout failed:", error);
+      // Centralized Request handler already surfaced the error
+    }
+  }

172-178: UX nit: signal destructive action.

Mark the “Log out on all devices” button as danger to convey impact.

-        <Button type="default" onClick={() => setShowConfirmLogoutModal(true)}>
+        <Button type="default" danger onClick={() => setShowConfirmLogoutModal(true)}>
           Log out on all devices
         </Button>

206-209: Keying by title can collide with i18n.

Using item.title as React key risks collisions when labels change. Consider adding a stable id to SettingsCardProps and key by that. Optional.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between de553c2 and 0607eee.

⛔ Files ignored due to path filters (1)
  • test/db/users.csv is excluded by !**/*.csv
📒 Files selected for processing (19)
  • app/controllers/AuthenticationController.scala (1 hunks)
  • app/models/user/User.scala (5 hunks)
  • app/models/user/UserService.scala (1 hunks)
  • app/security/CombinedAuthenticatorService.scala (2 hunks)
  • conf/evolutions/139-logout-everywhere.sql (1 hunks)
  • conf/evolutions/reversions/139-logout-everywhere.sql (1 hunks)
  • conf/webknossos.latest.routes (1 hunks)
  • frontend/javascripts/admin/account/account_security_view.tsx (3 hunks)
  • frontend/javascripts/admin/account/account_settings_view.tsx (2 hunks)
  • frontend/javascripts/admin/auth/change_email_view.tsx (3 hunks)
  • frontend/javascripts/admin/auth/verify_email_view.tsx (1 hunks)
  • frontend/javascripts/admin/rest_api.ts (2 hunks)
  • frontend/javascripts/libs/handle_http_status.ts (1 hunks)
  • frontend/javascripts/libs/handle_request_error_helper.tsx (1 hunks)
  • frontend/javascripts/libs/request.ts (2 hunks)
  • frontend/javascripts/libs/toast.tsx (2 hunks)
  • frontend/javascripts/router/router.tsx (2 hunks)
  • tools/postgres/schema.sql (2 hunks)
  • unreleased_changes/8850.md (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-09T07:33:45.681Z
Learnt from: robert-oleynik
PR: scalableminds/webknossos#8393
File: frontend/javascripts/admin/auth/passkeys_view.tsx:47-51
Timestamp: 2025-07-09T07:33:45.681Z
Learning: In the webknossos project, the Request module has comprehensive built-in error handling. The triggerRequest method defaults to showErrorToast: true, automatically catches errors with .catch(), and displays user notifications via Toast.error() or Toast.messages() in the handleError method. Functions using Request methods (like doWebAuthnRegistration and listWebAuthnKeys) do not need additional error handling for user notifications.

Applied to files:

  • frontend/javascripts/libs/handle_request_error_helper.tsx
  • frontend/javascripts/libs/request.ts
🧬 Code graph analysis (7)
app/controllers/AuthenticationController.scala (2)
app/models/user/User.scala (2)
  • logOutEverywhereByMultiUserId (590-595)
  • findIdsByMultiUserId (168-171)
app/models/user/UserService.scala (1)
  • removeUserFromCache (220-223)
app/security/CombinedAuthenticatorService.scala (2)
util/src/main/scala/com/scalableminds/util/time/Instant.scala (3)
  • Instant (14-45)
  • Instant (47-103)
  • zero (52-52)
app/models/user/UserService.scala (1)
  • retrieve (272-275)
frontend/javascripts/admin/auth/change_email_view.tsx (1)
frontend/javascripts/admin/rest_api.ts (1)
  • logoutUserEverywhere (157-159)
frontend/javascripts/libs/handle_request_error_helper.tsx (3)
frontend/javascripts/admin/datastore_health_check.ts (1)
  • pingMentionedDataStores (105-107)
frontend/javascripts/libs/toast.tsx (1)
  • showToastOnce (248-259)
frontend/javascripts/libs/handle_http_status.ts (1)
  • ServerErrorMessage (1-3)
app/models/user/User.scala (4)
util/src/main/scala/com/scalableminds/util/time/Instant.scala (4)
  • Instant (14-45)
  • Instant (47-103)
  • fromSql (58-58)
  • now (48-48)
app/utils/sql/SimpleSQLDAO.scala (1)
  • run (28-48)
app/utils/sql/SqlInterpolation.scala (3)
  • q (19-38)
  • as (53-73)
  • asUpdate (73-73)
app/utils/sql/SecuredSQLDAO.scala (1)
  • existingCollectionName (16-16)
frontend/javascripts/libs/request.ts (1)
frontend/javascripts/libs/handle_request_error_helper.tsx (1)
  • handleError (5-85)
frontend/javascripts/admin/account/account_security_view.tsx (3)
frontend/javascripts/admin/rest_api.ts (1)
  • logoutUserEverywhere (157-159)
frontend/javascripts/admin/account/helpers/settings_card.tsx (2)
  • SettingsCardProps (4-10)
  • SettingsCard (12-38)
frontend/javascripts/admin/account/helpers/settings_title.tsx (1)
  • SettingsTitle (5-21)
🪛 LanguageTool
unreleased_changes/8850.md

[grammar] ~1-~1: There might be a mistake here.
Context: ### Added - Added the option to log out from all dev...

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (16)
frontend/javascripts/libs/handle_http_status.ts (1)

1-3: No stale imports of ServerErrorMessage detected

I searched the codebase for any remaining imports of ServerErrorMessage from libs/request and found none—every consumer now pulls it from handle_http_status.ts. This centralization is safe to merge.

• Optional: if you’d like to preserve backward-compatibility, you can re-export the type in libs/request/index.ts:

// libs/request/index.ts
export type { ServerErrorMessage } from "libs/handle_http_status";
frontend/javascripts/admin/auth/verify_email_view.tsx (1)

3-3: Import path update looks correct.

Type-only import is appropriate and keeps bundling lean. No further action from this change.

frontend/javascripts/admin/account/account_settings_view.tsx (1)

10-11: Breadcrumb label rename to “Security” is consistent with the new page.

Looks good and aligns with the route/name changes elsewhere.

app/models/user/User.scala (2)

122-123: Row → model mapping for loggedOutEverywhereTime is accurate

Mapping from r.loggedouteverywheretime to Instant.fromSql is consistent with other timestamp fields.


43-45: No direct JSON serialization of User – loggedOutEverywhereTime is not exposed

Our searches did not find any implicit Writes/Format or macro-based JSON serializers for the User case class, nor any direct Json.toJson(user) calls in controllers or services. All API endpoints invoke custom serializers (e.g. userService.publicWrites, userService.compactWrites, or write against DTOs such as UserCompactInfo or UserAccessAnswer), none of which include the new loggedOutEverywhereTime field.

• No implicit val/def for Writes[User], Format[User], etc., in app/models/user/User.scala or elsewhere.
• No direct Json.toJson(user) invocations in controllers—only DTO-based serialization.
• Existing DTOs (UserCompactInfo, UserAccessAnswer, etc.) and their companion serializers won’t include the new timestamp.

Since there’s no accidental macro serialization of the full User model, loggedOutEverywhereTime will not be sent in any public JSON response. If at any point you add a generic Writes[User], remember to explicitly omit this field in your public‐facing DTOs or write definitions.

tools/postgres/schema.sql (1)

24-24: Schema bump and users.loggedOutEverywhereTime mirror the migration — LGTM

The schemaVersion = 139 and the new TIMESTAMPTZ column align with the evolution. The preexisting index on users(_multiUser) covers the UPDATE predicate.

Also applies to: 413-414

frontend/javascripts/admin/rest_api.ts (2)

154-155: Switching logout to POST matches backend route — good

Explicit POST avoids relying on defaults and aligns with typical CSRF defenses.


157-159: New API logoutUserEverywhere — good minimal surface

Straightforward wrapper to the new endpoint.

frontend/javascripts/router/router.tsx (1)

468-470: Route swap looks good

Importing AccountSecurityView and wiring /account/security is consistent with the UI rename. No issues.

conf/webknossos.latest.routes (1)

35-36: No GET /auth/logout usage detected; routes update is safe

  • Ripgrep search confirmed no client-side anchors or GET requests targeting /auth/logout outside of the routes file.
  • Frontend’s logoutUser() (in frontend/javascripts/admin/rest_api.ts) issues a POST to /api/auth/logout as intended.
  • There are no <Link>s, router entries, or other code paths invoking GET /auth/logout.
app/security/CombinedAuthenticatorService.scala (1)

83-93: No JodaTime usage in Silhouette 10.0.3 – Java Date API in use

I’ve confirmed that the project is using Silhouette 10.0.3 (defined in project/Dependencies.scala) and, per the Silhouette changelog, JodaTime was removed in version 9.0.0 and replaced with the Java Date API (github.com). As a result, cookie.lastUsedDateTime is a java.time type, so calling toInstant.toEpochMilli is correct and no change is needed here.

frontend/javascripts/libs/request.ts (2)

9-9: Good move: centralize error handling via helper.

Importing handleError from a dedicated helper keeps Request lean and avoids circular deps. No further concerns here.


255-256: No-op functional change; fine to keep.

Wrapping handleStatus in an arrow is semantically identical to then(handleStatus). If this was to aid debugging or typings, LGTM.

frontend/javascripts/libs/handle_request_error_helper.tsx (2)

1-86: Centralization of HTTP error handling looks good.

Dynamic import to avoid cycles, datastore health ping, and consolidated toasts align with previous Request behavior. Nice separation of concerns.


1-86: No stale ServerErrorMessage imports found
Ran a ripgrep search for any ServerErrorMessage imports from libs/request and found zero occurrences. Everything is correctly pointing to libs/handle_http_status.

frontend/javascripts/admin/account/account_security_view.tsx (1)

1-236: Overall: solid introduction of Security view and logout-everywhere flow.

The modal, cards, and action wiring look clean. Address the route inconsistency and small flow issues above and this should be ready.

Comment on lines +30 to +75
return error.text().then(
(text) => {
try {
const json = JSON.parse(text);

// Propagate HTTP status code for further processing down the road
if (error.status != null) {
json.status = error.status;
}

const messages = json.messages.map((message: ServerErrorMessage[]) => ({
...message,
key: json.status.toString(),
}));
if (showErrorToast) {
Toast.messages(messages); // Note: Toast.error internally logs to console
} else {
console.error(messages);
}
// Check whether the error chain mentions an url which belongs
// to a datastore. Then, ping the datastore
pingMentionedDataStores(text);

/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({ ...json, url: requestedUrl });
} catch (_jsonError) {
if (showErrorToast) {
Toast.error(text); // Note: Toast.error internally logs to console
} else {
console.error(`Request failed for ${requestedUrl}:`, text);
}

/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({
errors: [text],
status: error.status != null ? error.status : -1,
url: requestedUrl,
});
}
},
(textError) => {
Toast.error(textError.toString());
return Promise.reject(textError);
},
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: incorrect typing/mapping of json.messages; add null guards.

json.messages.map((message: ServerErrorMessage[]) => ...) treats each message as an array. Spreading an array into an object produces numeric keys, which is incorrect for Toast.messages. Also, json.messages may be absent or not an array. Fix the annotation and add a safe fallback.

-          try {
-            const json = JSON.parse(text);
+          try {
+            const json = JSON.parse(text);
@@
-            const messages = json.messages.map((message: ServerErrorMessage[]) => ({
-              ...message,
-              key: json.status.toString(),
-            }));
+            const rawMessages = Array.isArray(json.messages) ? json.messages : [];
+            const messages = rawMessages.map((message: ServerErrorMessage) => ({
+              ...message,
+              key: String(json.status),
+            }));
+            // If server did not provide messages, fall back to a single generic error
+            const messagesOrFallback =
+              messages.length > 0 ? messages : [{ error: json.error ?? text, key: String(json.status) }];
-            if (showErrorToast) {
-              Toast.messages(messages); // Note: Toast.error internally logs to console
+            if (showErrorToast) {
+              Toast.messages(messagesOrFallback); // Note: Toast.error internally logs to console
             } else {
-              console.error(messages);
+              console.error(messagesOrFallback);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return error.text().then(
(text) => {
try {
const json = JSON.parse(text);
// Propagate HTTP status code for further processing down the road
if (error.status != null) {
json.status = error.status;
}
const messages = json.messages.map((message: ServerErrorMessage[]) => ({
...message,
key: json.status.toString(),
}));
if (showErrorToast) {
Toast.messages(messages); // Note: Toast.error internally logs to console
} else {
console.error(messages);
}
// Check whether the error chain mentions an url which belongs
// to a datastore. Then, ping the datastore
pingMentionedDataStores(text);
/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({ ...json, url: requestedUrl });
} catch (_jsonError) {
if (showErrorToast) {
Toast.error(text); // Note: Toast.error internally logs to console
} else {
console.error(`Request failed for ${requestedUrl}:`, text);
}
/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({
errors: [text],
status: error.status != null ? error.status : -1,
url: requestedUrl,
});
}
},
(textError) => {
Toast.error(textError.toString());
return Promise.reject(textError);
},
);
}
return error.text().then(
(text) => {
try {
const json = JSON.parse(text);
// Propagate HTTP status code for further processing down the road
if (error.status != null) {
json.status = error.status;
}
const rawMessages = Array.isArray(json.messages) ? json.messages : [];
const messages = rawMessages.map((message: ServerErrorMessage) => ({
...message,
key: String(json.status),
}));
// If server did not provide messages, fall back to a single generic error
const messagesOrFallback =
messages.length > 0
? messages
: [{ error: json.error ?? text, key: String(json.status) }];
if (showErrorToast) {
Toast.messages(messagesOrFallback); // Note: Toast.error internally logs to console
} else {
console.error(messagesOrFallback);
}
// Check whether the error chain mentions an url which belongs
// to a datastore. Then, ping the datastore
pingMentionedDataStores(text);
/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({ ...json, url: requestedUrl });
} catch (_jsonError) {
if (showErrorToast) {
Toast.error(text); // Note: Toast.error internally logs to console
} else {
console.error(`Request failed for ${requestedUrl}:`, text);
}
/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({
errors: [text],
status: error.status != null ? error.status : -1,
url: requestedUrl,
});
}
},
(textError) => {
Toast.error(textError.toString());
return Promise.reject(textError);
},
);
🤖 Prompt for AI Agents
In frontend/javascripts/libs/handle_request_error_helper.tsx around lines 30 to
75, json.messages is incorrectly typed/mapped as ServerErrorMessage[] and may be
missing or not an array; change the mapping to treat each item as a single
ServerErrorMessage (not an array), add a guard using
Array.isArray(json.messages) (falling back to an empty array) before mapping,
and ensure each mapped message is an object (spread the message only when it is
an object) and assign a stable key (e.g. json.status?.toString() or a fallback)
so Toast.messages always receives a proper array of objects; update downstream
rejects to use the safe messages fallback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
frontend/javascripts/router/router.tsx (1)

378-379: Use replace on Navigate to avoid polluting browser history for compat redirects.

These are pure compatibility redirects; adding replace prevents a back-button loop to the legacy URL.

Apply:

-<Route path="/auth/changePassword" element={<Navigate to="/account/security" />} />
-<Route path="/account/password" element={<Navigate to="/account/security" />} />
+<Route path="/auth/changePassword" element={<Navigate to="/account/security" replace />} />
+<Route path="/account/password" element={<Navigate to="/account/security" replace />} />
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0607eee and 85dc08b.

📒 Files selected for processing (6)
  • frontend/javascripts/admin/account/account_security_view.tsx (3 hunks)
  • frontend/javascripts/admin/auth/verify_email_view.tsx (1 hunks)
  • frontend/javascripts/libs/handle_http_status.ts (0 hunks)
  • frontend/javascripts/libs/handle_request_error_helper.tsx (1 hunks)
  • frontend/javascripts/router/router.tsx (3 hunks)
  • frontend/javascripts/types/api_types.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/javascripts/libs/handle_http_status.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/javascripts/admin/auth/verify_email_view.tsx
  • frontend/javascripts/libs/handle_request_error_helper.tsx
  • frontend/javascripts/admin/account/account_security_view.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (4)
frontend/javascripts/router/router.tsx (2)

50-50: Verification Complete: No stale AccountPasswordView imports; legacy routes are intentionally redirected

I’ve confirmed that:

  • There are no remaining imports or usages of AccountPasswordView across .ts/.tsx files.
  • No files matching account_password_view.* exist in the repo.
  • The old front-end routes (/auth/changePassword, /account/password) still appear in router.tsx but correctly redirect to /account/security for backward compatibility.
  • The call to /api/auth/changePassword in admin/rest_api.ts is the live backend endpoint (not a view route) and does not require removal.

No further cleanup is needed here.


467-471: Verified legacy redirects; optional default‐to‐security remains safe

  • In frontend/javascripts/router/router.tsx lines 378–379, we already have backwards-compatibility routes:
    <Route path="/auth/changePassword" element={<Navigate to="/account/security" />} />
    <Route path="/account/password"    element={<Navigate to="/account/security" />} />
    
  • No other hard-coded links to /account/password or /auth/changePassword were found in TS/TSX files or elsewhere in the repo.
  • If you’d like “Security” to be the default tab under /account, you can apply the optional refactor:
    -<Route index element={<Navigate to="profile" />} />
    +<Route index element={<Navigate to="security" replace />} />
  • Please manually confirm that any menu items or command-palette entries pointing to the old password view now use /account/security.
frontend/javascripts/types/api_types.ts (2)

1303-1306: Centralizing ServerErrorMessage here looks good

Moving the error payload type into api_types.ts improves discoverability and reuse across the frontend.


1303-1306: Add JSDoc to ServerErrorMessage and confirm no lingering imports

• TSConfig’s paths setting maps "*""./frontend/javascripts/*", so the canonical import for this type is

import type { ServerErrorMessage } from 'types/api_types';

No explicit types/* alias is defined, but the wildcard covers it.

• Verified with ripgrep that there are no remaining imports of ServerErrorMessage from:

  • libs/request
  • the literal path frontend/javascripts/types/api_types.ts
  • any other source
    (i.e., no call sites require updating)

Apply this diff to add documentation:

-export type ServerErrorMessage = {
-  error: string;
-};
+/**
+ * JSON error body returned by backend endpoints.
+ * Example: HTTP 4xx/5xx with payload { "error": "<human-readable message>" }.
+ * Only use where the server guarantees this shape.
+ */
+export type ServerErrorMessage = {
+  error: string;
+};

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
frontend/javascripts/router/router.tsx (3)

50-50: Switch to AccountSecurityView import — consider lazy-loading for parity and bundle size.

The direct import works, but this view is only needed on the account settings route. Consider lazy-loading it (you already use loadable elsewhere) to keep the router bundle lean.

Apply this diff within the selected range to drop the static import:

-import AccountSecurityView from "admin/account/account_security_view";
+// (lazy-load AccountSecurityView below to reduce initial bundle size)

Add this near your other loadables (e.g., after the existing AsyncWorkflow declarations):

const AccountSecurityView = loadable<EmptyObject>(() =>
  import("admin/account/account_security_view"),
);

376-380: Back-compat redirects look good; minor consistency nit: add replace to /login.

Using replace prevents polluting history. You already use it for other back-compat redirects; mirror that on /login for consistency.

-    <Route path="/login" element={<Navigate to="/auth/login" />} />
+    <Route path="/login" element={<Navigate to="/auth/login" replace />} />

Note: If any of the legacy URLs carried query params (e.g., ?redirect=...), those are dropped by static declarations. If preserving search params is important, we can convert these to small wrapper components that read location.search and forward it.


386-390: Legacy signup/register references audit — ripgrep results & next steps

The provided ripgrep search returned only intentional references in the JS/TS codebase:

  • API calls in frontend/javascripts/admin
    /api/auth/register in registration_form_generic.tsx
    /api/auth/changePassword in rest_api.ts
    /api/auth/webauthn/register/... in webauthn.ts
  • UI components now correctly linking to /auth/signup
    <Button href="https://github.com/auth/signup"…> in welcome_toast.tsx
    <Link to="/auth/signup"> in login_form.tsx
  • Asset references in stylesheets
    signup.svg in _drawings.less
  • Server route definitions
    • POST /auth/register, /auth/changePassword, etc., in conf/webknossos.latest.routes
  • Changelog entries noting the explicit /signup route

No unexpected or stale deep‐linkable routes (/signup, /register, /auth/register) were found in the JS/TS code.

Please extend this verification to cover:

  • Documentation files (e.g. *.md)
  • Email templates or view files (e.g. under app/views/email* or similar)

If any remaining links to the old paths are intentional, consider annotating them with a clarifying comment or updating them to /auth/signup to avoid user‐facing 404s when opening deep links.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 85dc08b and f4164ed.

📒 Files selected for processing (1)
  • frontend/javascripts/router/router.tsx (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (1)
frontend/javascripts/router/router.tsx (1)

469-469: New “/account/security” nested route under SecuredRoute — LGTM.

Matches the rename and aligns with the new AccountSecurityView. The back-compat redirect from /account/password covers legacy deep links.

@hotzenklotz
Copy link
Member

🧹 Nitpick comments (1)

frontend/javascripts/router/router.tsx (1)> 378-379: Use replace on Navigate to avoid polluting browser history for compat redirects.

These are pure compatibility redirects; adding replace prevents a back-button loop to the legacy URL.
Apply:

-<Route path="/auth/changePassword" element={<Navigate to="/account/security" />} />
-<Route path="/account/password" element={<Navigate to="/account/security" />} />
+<Route path="/auth/changePassword" element={<Navigate to="/account/security" replace />} />
+<Route path="/account/password" element={<Navigate to="/account/security" replace />} />

@knollengewaechs This CodeRabit comment might be a good idea. Otherwise, one when trying to navigate back, one is stuck in a endless "back" <-> "redirect" loop.

Copy link
Member

@hotzenklotz hotzenklotz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontend parts look good to me. I did not test this yet, though.

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left 2 Backend comments

Testing work well 👍

@fm3 fm3 requested a review from MichaelBuessemeyer August 25, 2025 12:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/controllers/AuthenticationController.scala (1)

961-968: Discard the current authenticator and avoid map-for-side-effects in logoutEverywhere

Return an authenticator-discard result so the browser removes the cookie immediately, and use foreach for side effects. This aligns with existing patterns in changePassword and prevents stale cookies on the initiating tab. Also logs are useful for auditability.

 def logoutEverywhere: Action[AnyContent] = sil.SecuredAction.async { implicit request =>
   for {
     _ <- userDAO.logOutEverywhereByMultiUserId(request.identity._multiUser)
     userIds <- userDAO.findIdsByMultiUserId(request.identity._multiUser)
-    _ = userIds.map(userService.removeUserFromCache)
+    _ = userIds.foreach(userService.removeUserFromCache)
     _ <- tokenDAO.deleteDataStoreTokensForMultiUser(request.identity._multiUser)
-  } yield Ok
+    _ = logger.info(s"Multiuser ${request.identity._multiUser} issued logout-everywhere.")
+    authenticatorResult <- Fox.fromFuture(combinedAuthenticatorService.discard(request.authenticator, Ok))
+  } yield authenticatorResult
 }

Follow-up thought: If your threat model requires it, consider whether non-DataStore bearer tokens (e.g., ResetPassword) should also be invalidated for this multi-user. Happy to sketch a DAO variant if desired.

🧹 Nitpick comments (1)
app/security/Token.scala (1)

137-148: Bulk soft-delete for DataStore tokens — add NOT isDeleted; providerKey confirmed to be user.id; users reference optional

Verified findings (short):

  • DataStore tokens are created from user.loginInfo (createAndInitDataStoreTokenForUser -> createAndInit(user.loginInfo, TokenType.DataStore)), and code expects providerKey to be an ObjectId — providerKey = user._id.
  • tokens schema exposes a view webknossos.tokens_ (SELECT * FROM webknossos.tokens WHERE NOT isDeleted), so skipping already-deleted rows is safe and avoids redundant writes.
  • The hard-coded webknossos.users_ subquery is brittle; consider using the User DAO's existingCollectionName or the stable view webknossos.users if you want to future-proof it.

Locations to update / review:

  • app/security/Token.scala — deleteDataStoreTokensForMultiUser (around lines 137–148).
  • Evidence: app/security/WebknossosBearerTokenAuthenticatorService.scala (createAndInitDataStoreTokenForUser/createAndInit), app/models/user/UserService.scala (createLoginInfo uses ObjectId), tools/postgres/schema.sql (tokens_ view).

Apply this minimal diff to avoid redundant updates:

-                   WHERE tokenType = ${TokenType.DataStore}
+                   WHERE tokenType = ${TokenType.DataStore}
+                   AND NOT isDeleted

Optional: replace the hard-coded "FROM webknossos.users_" subquery with the user DAO's existingCollectionName or "webknossos.users" view to reduce brittleness.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f4164ed and d1fa0f8.

📒 Files selected for processing (2)
  • app/controllers/AuthenticationController.scala (2 hunks)
  • app/security/Token.scala (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/security/Token.scala (2)
app/utils/sql/SqlInterpolation.scala (2)
  • q (19-38)
  • asUpdate (73-73)
app/security/TokenType.scala (1)
  • TokenType (5-8)
app/controllers/AuthenticationController.scala (3)
app/models/user/User.scala (2)
  • logOutEverywhereByMultiUserId (590-595)
  • findIdsByMultiUserId (168-171)
app/models/user/UserService.scala (1)
  • removeUserFromCache (220-223)
app/security/Token.scala (1)
  • deleteDataStoreTokensForMultiUser (137-149)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (1)
app/controllers/AuthenticationController.scala (1)

204-204: DI addition of TokenDAO looks good

Wiring TokenDAO here is appropriate for the logout-everywhere flow and matches its usage below.

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks. Backend testing worked well, the tokens seem to expire upon logout everywhere :)

@knollengewaechs
Copy link
Contributor

@hotzenklotz there were quite some changes after your review. I added replace props to all components in the router. and I updated the modal. fyi in case you want to have another short look before merging

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
frontend/javascripts/router/router.tsx (2)

376-378: External docs redirect: consider a tiny ExternalRedirect component.
Using Navigate to an absolute URL is okay, but a dedicated component makes intent explicit and avoids any router nuances.

Example:

// add once in this file (or a shared utils file)
import { useEffect } from "react";

function ExternalRedirect({ href }: { href: string }) {
  useEffect(() => {
    window.location.replace(href);
  }, [href]);
  return null;
}

// usage
<Route
  path="/help/keyboardshortcuts"
  element={
    <ExternalRedirect href="https://docs.webknossos.org/webknossos/ui/keyboard_shortcuts.html" />
  }
/>

381-395: Nit: also use replace on the /login compat redirect for consistency.
All other compat redirects here use replace — mirror it for /login to avoid history pollution.

-    <Route path="/login" element={<Navigate to="/auth/login" />} />
+    <Route path="/login" element={<Navigate to="/auth/login" replace />} />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 86661fb and fe47435.

📒 Files selected for processing (1)
  • frontend/javascripts/router/router.tsx (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/javascripts/router/router.tsx (5)
frontend/javascripts/admin/organization/organization_overview_view.tsx (1)
  • OrganizationOverviewView (25-210)
frontend/javascripts/admin/organization/organization_notifications_view.tsx (1)
  • OrganizationNotificationsView (21-122)
frontend/javascripts/admin/organization/organization_danger_zone_view.tsx (1)
  • OrganizationDangerZoneView (10-70)
frontend/javascripts/admin/auth/accept_invite_view.tsx (1)
  • AcceptInviteView (15-117)
frontend/javascripts/admin/auth/verify_email_view.tsx (1)
  • VerifyEmailView (47-92)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (7)
frontend/javascripts/router/router.tsx (7)

50-50: Switched to AccountSecurityView import — good alignment with new /account/security route.


172-172: Compat redirect now uses replace — prevents back-button loops.


278-278: Index redirect for dataset settings uses replace — nice touch.


357-360: Compat redirect /organizations/:organizationId → /organization with replace — looks good.


369-369: Organization index redirect uses replace — LGTM.


472-472: Account index redirect uses replace — good.


474-474: New /account/security route wired to AccountSecurityView — matches feature intent.

@hotzenklotz
Copy link
Member

Frontend changes LGTM

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/javascripts/router/router.tsx (1)

386-386: Nit: also add replace to /login → /auth/login for consistency.

Keeps browser history clean like other compat redirects.

-    <Route path="/login" element={<Navigate to="/auth/login" />} />
+    <Route path="/login" element={<Navigate to="/auth/login" replace />} />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fe47435 and f622925.

📒 Files selected for processing (2)
  • conf/application.conf (1 hunks)
  • frontend/javascripts/router/router.tsx (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/javascripts/router/router.tsx (5)
frontend/javascripts/admin/organization/organization_overview_view.tsx (1)
  • OrganizationOverviewView (25-210)
frontend/javascripts/admin/organization/organization_notifications_view.tsx (1)
  • OrganizationNotificationsView (21-122)
frontend/javascripts/admin/organization/organization_danger_zone_view.tsx (1)
  • OrganizationDangerZoneView (10-70)
frontend/javascripts/admin/auth/accept_invite_view.tsx (1)
  • AcceptInviteView (15-117)
frontend/javascripts/admin/auth/verify_email_view.tsx (1)
  • VerifyEmailView (47-92)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (10)
conf/application.conf (1)

149-149: Confirm intent: isWkorgInstance set to true affects routing and onboarding semantics.

If this was only for the temporary dev instance, consider reverting in the default config and overriding via environment- or profile-specific config to avoid surprising local/prod defaults.

frontend/javascripts/router/router.tsx (9)

35-36: Good: import redirect for data-router redirects.

Enables loader-based redirects like the publication route below.


51-51: Good: swap to AccountSecurityView.

Matches the new canonical /account/security route.


173-173: LGTM: replace on compatibility redirect.

Prevents back-button loops for /reports/openTasks.


279-279: LGTM: index redirect uses replace.

Avoids extra history entries in dataset settings.


358-361: LGTM: org id → /organization with replace.

Cleaner history for legacy org URLs.


370-370: LGTM: organization index → overview with replace.


382-396: LGTM: auth/password/register compat redirects now use replace.

Consistent with the “don’t pollute history” goal.


436-439: Good fix: loader-based param redirect for /publication/:id.

Avoids literal “:id” path; aligns with RR data-router best practices.


476-478: LGTM: /account index → profile (replace) and security route wiring.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/controllers/ShortLinkController.scala (1)

26-27: Host check via startsWith is unsafe (open-redirect/phishing risk)

String prefix checks allow crafted domains (e.g., https://example.org.attacker.com) and userinfo tricks. Validate scheme/host via URI parsing or, at minimum, enforce a trailing “/” boundary.

Robust fix:

+import java.net.URI
+import scala.util.Try
@@
-      _ <- Fox.fromBool(longLink.startsWith(wkConf.Http.uri)) ?~> "Could not generate short link: URI does not match"
+      // Only allow short links to our own host (scheme + host match). Treat invalid URIs as mismatch.
+      val baseUri   = new URI(wkConf.Http.uri)
+      val targetOk  = Try(new URI(longLink)).toOption.exists { u =>
+        u.isAbsolute &&
+        Option(u.getScheme).contains(baseUri.getScheme) &&
+        Option(u.getHost).contains(baseUri.getHost)
+      }
+      _ <- Fox.fromBool(targetOk) ?~> "Could not generate short link: URI host/scheme do not match"

Minimal mitigation (if avoiding URI parse now):

-      _ <- Fox.fromBool(longLink.startsWith(wkConf.Http.uri)) ?~> "Could not generate short link: URI does not match"
+      _ <- Fox.fromBool(longLink.startsWith(wkConf.Http.uri.stripSuffix(\"/\") + \"/\")) ?~> "Could not generate short link: URI does not match"

Consider adding rate limiting if anonymous access is ever intended.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f622925 and 8d5df9b.

📒 Files selected for processing (1)
  • app/controllers/ShortLinkController.scala (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/ShortLinkController.scala (2)
app/controllers/AnnotationPrivateLinkController.scala (1)
  • create (86-99)
util/src/main/scala/com/scalableminds/util/mvc/ExtendedController.scala (1)
  • validateJson (204-209)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: frontend-tests
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
frontend/javascripts/router/router.tsx (3)

173-173: Compat redirect uses replace — consider loader-based redirect for consistency.
Works as-is; using a loader avoids a render hop and aligns with other redirects in this file.

Apply if you want parity with other routes:

-    <Route path="/reports/openTasks" element={<Navigate to="/reports/availableTasks" replace />} />
+    <Route path="/reports/openTasks" loader={() => redirect("/reports/availableTasks")} />

358-361: Organizations legacy redirect drops the organizationId.
If users deep-link to a specific org, silently ignoring the id might be confusing. Optional: switch to the requested org before redirecting.

Example component-based approach:

// near other helpers
import { useEffect } from "react";
import { useParams, Navigate } from "react-router-dom";
import { switchToOrganization } from "admin/organization/organization_utils";

function OrganizationLegacyRedirect() {
  const { organizationId = "" } = useParams();
  useEffect(() => {
    if (organizationId) switchToOrganization(organizationId);
  }, [organizationId]);
  return <Navigate to="/organization" replace />;
}

// route:
// <Route path="/organizations/:organizationId" element={<OrganizationLegacyRedirect />} />

380-385: Add replace to the /login redirect
Ensure the <Navigate> for /login uses replace to match other compat redirects and prevent back-button loops.

-    <Route path="/login" element={<Navigate to="/auth/login" />} />
+    <Route path="/login" element={<Navigate to="/auth/login" replace />} />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8d5df9b and ef2e01e.

📒 Files selected for processing (1)
  • frontend/javascripts/router/router.tsx (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/javascripts/router/router.tsx (5)
frontend/javascripts/admin/organization/organization_overview_view.tsx (1)
  • OrganizationOverviewView (25-210)
frontend/javascripts/admin/organization/organization_notifications_view.tsx (1)
  • OrganizationNotificationsView (21-122)
frontend/javascripts/admin/organization/organization_danger_zone_view.tsx (1)
  • OrganizationDangerZoneView (10-70)
frontend/javascripts/admin/auth/accept_invite_view.tsx (1)
  • AcceptInviteView (15-117)
frontend/javascripts/admin/auth/verify_email_view.tsx (1)
  • VerifyEmailView (47-92)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (8)
frontend/javascripts/router/router.tsx (8)

35-36: Importing redirect is correct for data-router redirects.
The usage below looks consistent.


51-51: Switch to AccountSecurityView import looks good.
Matches the new “Security” route below.


279-285: Index redirect to “data” with replace is correct.
No issues spotted.


370-374: Index redirect to organization/overview with replace is correct.
All good.


376-378: External docs redirect via loader — nice fix.
Addresses prior concern with absolute-URL .


390-395: Signup/register compat redirects now use replace — good.
Prevents polluting history.


434-437: Param-preserving publication redirect via loader — correct.
Fixes the earlier “:id not interpolated” bug.


474-478: Account index redirect and Security route wiring look correct.
Matches the new “Logout everywhere” UI location.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/javascripts/router/router.tsx (1)

376-378: Fixed: external redirect now via loader.

This addresses prior feedback about to absolute URLs. ✅

🧹 Nitpick comments (3)
frontend/javascripts/router/router.tsx (3)

279-279: Nested index redirect OK; optional: use loader-based redirect.

Current Navigate works; consider a loader for parity with other redirects.

-      <Route index element={<Navigate to="data" replace />} />
+      <Route index loader={() => redirect("data")} />

358-361: Minor: avoid double-hop by redirecting straight to overview.

Right now it goes /organizations/:id → /organization → /organization/overview. Jump directly.

-    <Route
-      path="/organizations/:organizationId"
-      element={<Navigate to="/organization" replace />}
-    />
+    <Route
+      path="/organizations/:organizationId"
+      element={<Navigate to="/organization/overview" replace />}
+    />

435-437: Param redirect looks correct; encode the id (nit).

Guard against odd chars in ids.

-      loader={({ params }) => redirect(`/publications/${params.id}`)}
+      loader={({ params }) => redirect(`/publications/${encodeURIComponent(params.id ?? "")}`)}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ef2e01e and 4cd0f7a.

📒 Files selected for processing (1)
  • frontend/javascripts/router/router.tsx (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/javascripts/router/router.tsx (3)
frontend/javascripts/admin/organization/organization_overview_view.tsx (1)
  • OrganizationOverviewView (25-210)
frontend/javascripts/admin/organization/organization_notifications_view.tsx (1)
  • OrganizationNotificationsView (21-122)
frontend/javascripts/admin/organization/organization_danger_zone_view.tsx (1)
  • OrganizationDangerZoneView (10-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (8)
frontend/javascripts/router/router.tsx (8)

35-35: Good: switched to data-router redirect.

Import looks correct for loader-based redirects.


173-173: Compat redirect uses replace — prevents back-button loops.

Looks good.


370-370: Good: canonicalizes /organization to overview with replace.

Prevents history bloat.


380-380: Good: auth-token compat redirect with replace.

No concerns.


382-384: Good: legacy auth routes consolidated to Account Security/Login with replace.

Matches the new security page.


390-394: Good: signup/register legacy routes use replace.

Keeps history clean; consistent with other compat redirects.


474-474: Good: /account index → profile with replace.

Consistent with other sections.


476-476: Security route wiring LGTM.

New AccountSecurityView is mounted at the canonical path.

@knollengewaechs
Copy link
Contributor

from my side this is ready to go! 🚀

@fm3 fm3 enabled auto-merge (squash) September 1, 2025 08:50
@fm3 fm3 merged commit 34e37ed into master Sep 1, 2025
5 checks passed
@fm3 fm3 deleted the logout-everywhere branch September 1, 2025 08:51
knollengewaechs added a commit that referenced this pull request Sep 4, 2025
### Steps to test:
- Go to tree tab and enter long name or make tab really narrow. The
circle next to the tree should stay round.

### TODOs:

- [x] fix tree dot
- [x] fix browsers back button for DS settings page (other PR: #8850)


------
- [x] Considered [common edge
cases](../blob/master/.github/common_edge_cases.md)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Logout everywhere
5 participants