Skip to content

Conversation

@dkildar
Copy link
Collaborator

@dkildar dkildar commented Oct 20, 2025

Summary by CodeRabbit

  • New Features

    • Added password change functionality allowing users to generate and validate new keys for account security.
  • Improvements

    • Reorganized permissions UI with tabbed interface separating key management and password change workflows.
    • Updated permissions section layout for better component alignment.
  • Deprecations

    • Marked older password change method as deprecated.

✏️ Tip: You can customize this high-level summary in your review settings.

@dkildar dkildar requested a review from feruzm October 20, 2025 14:50
@dkildar dkildar self-assigned this Oct 20, 2025
@dkildar dkildar added the enhancement New feature or request label Oct 20, 2025
@coderabbitai

This comment was marked as resolved.

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

🤖 Fix all issues with AI agents
In @apps/web/src/api/mutations/change-password.ts:
- Around line 60-67: In prepareAuth, the code pushes the new key into
auth.key_auths but mistakenly discards the result of filter so the old key
remains; change the line that calls filter to assign its result back to
auth.key_auths and invert the predicate to exclude the old key (use !== with
currentKeys[keyName]) so the old public key is removed and only the new one is
kept along with any other non-matching entries.
- Around line 69-79: The updateAccount call is leaving memo_key as
accountData.memo_key so the memo key isn't rotated; compute the new memo public
key from the new password/key (the same place you derive owner/active/posting
keys) and replace accountData.memo_key with that new memo key in the arguments
to CONFIG.hiveClient.broadcast.updateAccount; look for the prepareAuth helper
and where new keys are derived (e.g., functions that produce
owner/active/posting keys) and use that same derivation to produce the new
memo_key passed to updateAccount along with owner/active/posting and currentKey.

In
@apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-change-password.tsx:
- Around line 46-51: The newKeys object uses the wrong key name "memo_key";
change it to "memo" so it matches the Payload/Keys expected by the mutation and
prepareAuth calls—replace the property memo_key with memo (using
PrivateKey.fromString(keys.memo)) in the newKeys literal and ensure any callers
using prepareAuth("owner"/"active"/"posting"/"memo") and the Payload type in
change-password.ts align with this name.
- Around line 22-28: activeUser?.username is being non-null asserted when
calling useQuery(getAccountFullQueryOptions), useHiveKeysQuery and
useAccountChangePassword which can blow up if activeUser is undefined; instead
derive a safe username const (e.g. const username = activeUser?.username) and
keep hooks called unconditionally but pass an enabled flag or guarded query
options so they do nothing until username is truthy: update
getAccountFullQueryOptions call and useQuery to include enabled: !!username (or
return early from select if username missing), call useHiveKeysQuery(username)
with an options param or internal enabled check so it waits for username, and
ensure useAccountChangePassword is invoked in a way that defers mutate creation
until username exists (via hook options or by returning a no-op mutate) rather
than using the non-null assertion on activeUser?.username!.
- Line 67: Replace the hardcoded placeholder "Current key" in the
ManageKeysChangePassword component's input (the placeholder prop on the input
element in manage-keys-change-password.tsx) with a call to the project's i18n
translation helper (e.g., useTranslations()/t() or your existing i18n hook),
e.g., const t = useTranslations('profile.permissions'); then use t('currentKey')
(or the appropriate key) for the placeholder; add the new translation key to the
locale files.
- Around line 43-59: handleChangePassword currently awaits changePassword but
has no error handling, so failures give no user feedback; wrap the async call in
a try/catch inside handleChangePassword (or use the mutation's onError/state)
and on error call the UI error handler (e.g., error(i18next.t(...)) or surface
the mutation error message) while ensuring finally logic (setShowSeed(false),
setCurrentKey(undefined)) still runs only on success or is handled
appropriately; update the dependency usage around rawKey and currentKey as
needed.

In
@apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys.tsx:
- Around line 21-38: The tab elements are non-interactive divs and lack keyboard
accessibility; update the tab UI (the elements that read tab, use
setTab("password") and setTab("key")) to be keyboard-focusable and activatable
by converting them to semantic <button> elements or adding role="button",
tabIndex={0}, and an onKeyDown handler that calls setTab when Enter or Space is
pressed; ensure the active styling logic (tab === "password"/"key") and the
i18next.t labels remain unchanged so the visual state and localization are
preserved.
🧹 Nitpick comments (6)
apps/web/src/features/ui/input/key-input.tsx (1)

65-89: Minor inconsistent indentation.

Line 79 has 6-space indentation which is inconsistent with the surrounding code that uses 2 or 4 spaces.

Suggested fix
          } else {
-              privateKey = PrivateKey.from(key);
+            privateKey = PrivateKey.from(key);
          }
apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys.tsx (3)

13-14: Misleading state variable name.

The showChangePassword state controls the "Add Keys" modal (line 79: ManageKeysAddKeys), not a password change dialog. Consider renaming to showAddKeysModal for clarity.

Suggested fix
-  const [showChangePassword, setShowChangePassword] = useState(false);
+  const [showAddKeysModal, setShowAddKeysModal] = useState(false);

Then update usages at lines 57, 73-74, and 79.


40-48: Low XSS risk with i18n strings, but consider sanitization for defense-in-depth.

The static analysis tools flag this as an XSS vector. Since these are i18next translation strings (developer-controlled), the risk is minimal. However, if translations ever source from external CMS or user input, this becomes exploitable. If the hint text doesn't require HTML formatting, prefer plain text rendering.

Alternative using plain text if HTML isn't needed
-          <div
-            className="text-sm opacity-75"
-            dangerouslySetInnerHTML={{
-              __html:
-                tab === "password"
-                  ? i18next.t("permissions.change-password.hint")
-                  : i18next.t("permissions.keys.hint")
-            }}
-          />
+          <div className="text-sm opacity-75">
+            {tab === "password"
+              ? i18next.t("permissions.change-password.hint")
+              : i18next.t("permissions.keys.hint")}
+          </div>

20-38: Consider extracting tab button as a reusable component to reduce duplication.

The tab buttons share identical structure with only tab value and translation key differing. This could be extracted for cleaner code.

Example refactor
const TabButton = ({ value, label }: { value: "password" | "key"; label: string }) => (
  <div
    className={clsx(
      "cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
      tab === value && "bg-gray-100 dark:bg-gray-100/10"
    )}
    onClick={() => setTab(value)}
    role="tab"
    aria-selected={tab === value}
  >
    {label}
  </div>
);
apps/web/src/api/mutations/change-password.ts (1)

21-21: Misleading mutation key.

The mutation key uses "revoke-key" but this hook is for changing passwords. Consider using a more descriptive key.

-    mutationKey: ["accounts", "revoke-key", accountData?.name],
+    mutationKey: ["accounts", "change-password", accountData?.name],
apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-change-password.tsx (1)

59-59: Unnecessary dependency in useCallback.

activeUser is included in the dependency array but is not used inside the callback.

-  }, [keys, currentKey, activeUser, rawKey]);
+  }, [keys, currentKey, rawKey, changePassword]);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ce1b16 and fee1f47.

📒 Files selected for processing (8)
  • apps/web/src/api/mutations/change-password.ts
  • apps/web/src/api/mutations/index.ts
  • apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-change-password.tsx
  • apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys.tsx
  • apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/profile-permissions.tsx
  • apps/web/src/features/i18n/locales/en-US.json
  • apps/web/src/features/ui/input/key-input.tsx
  • packages/sdk/src/modules/accounts/mutations/use-account-update-password.ts
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/src/features/ui/input/key-input.tsx (1)
packages/wallets/src/modules/wallets/utils/detect-hive-key-derivation.ts (1)
  • detectHiveKeyDerivation (7-39)
apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/profile-permissions.tsx (1)
apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys.tsx (1)
  • ManageKeys (12-86)
🪛 ast-grep (0.40.4)
apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys.tsx

[warning] 41-41: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys.tsx

[error] 42-42: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ 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). (1)
  • GitHub Check: build (20.x)
🔇 Additional comments (6)
apps/web/src/features/i18n/locales/en-US.json (1)

3087-3093: LGTM!

The new localization strings for the password change feature are well-structured and follow existing patterns. The hint appropriately warns users about losing access to external tokens when changing the seed phrase.

packages/sdk/src/modules/accounts/mutations/use-account-update-password.ts (1)

21-25: LGTM!

The deprecation annotation clearly communicates the limitations of this hook and guides developers toward the new implementation. The explanation about seed-based vs native Hive passwords is helpful.

apps/web/src/features/ui/input/key-input.tsx (1)

35-43: LGTM!

These are purely formatting/whitespace adjustments with no functional changes.

apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/profile-permissions.tsx (1)

28-31: Verify grid nesting behavior for ManageKeys component.

The layout change itself appears sound—moving ManageKeys and AccountRecovery into a md:grid-cols-2 grid with items-start prevents unnecessary equal-height stretching. However, the concern about ManageKeys using an internal md:col-span-2 layout while being nested as a child of the parent 2-column grid warrants verification. If ManageKeys internally uses grid spanning that expects a wider context, it may render narrower than intended. Inspect the ManageKeys component to confirm its internal layout structure and verify it renders correctly within this parent grid configuration.

apps/web/src/api/mutations/index.ts (1)

32-32: LGTM!

The new export follows the existing barrel file pattern and successfully re-exports the useAccountChangePassword hook from the change-password module.

apps/web/src/api/mutations/change-password.ts (1)

22-25: [Waiting for review comment to be provided]

Comment on lines +60 to +67
const prepareAuth = (keyName: keyof Keys) => {
const auth: AuthorityType = R.clone(accountData[keyName]);

auth.key_auths.push([newKeys[keyName].createPublic().toString(), 1]);
auth.key_auths.filter(([key]) => currentKeys[keyName] === key);

return auth;
};
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 | 🔴 Critical

Critical bug: filter() return value is discarded, old keys are never removed.

Array.prototype.filter() returns a new array and does not mutate the original. The current code pushes the new key but never actually removes the old key since the filter result is thrown away.

Proposed fix
       const prepareAuth = (keyName: keyof Keys) => {
         const auth: AuthorityType = R.clone(accountData[keyName]);

         auth.key_auths.push([newKeys[keyName].createPublic().toString(), 1]);
-        auth.key_auths.filter(([key]) => currentKeys[keyName] === key);
+        auth.key_auths = auth.key_auths.filter(([key]) => key !== currentKeys[keyName]);

         return auth;
       };

Note: Also corrected the filter logic—you want to exclude the old key (!==), not keep only the old key (===).

📝 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
const prepareAuth = (keyName: keyof Keys) => {
const auth: AuthorityType = R.clone(accountData[keyName]);
auth.key_auths.push([newKeys[keyName].createPublic().toString(), 1]);
auth.key_auths.filter(([key]) => currentKeys[keyName] === key);
return auth;
};
const prepareAuth = (keyName: keyof Keys) => {
const auth: AuthorityType = R.clone(accountData[keyName]);
auth.key_auths.push([newKeys[keyName].createPublic().toString(), 1]);
auth.key_auths = auth.key_auths.filter(([key]) => key !== currentKeys[keyName]);
return auth;
};
🤖 Prompt for AI Agents
In @apps/web/src/api/mutations/change-password.ts around lines 60 - 67, In
prepareAuth, the code pushes the new key into auth.key_auths but mistakenly
discards the result of filter so the old key remains; change the line that calls
filter to assign its result back to auth.key_auths and invert the predicate to
exclude the old key (use !== with currentKeys[keyName]) so the old public key is
removed and only the new one is kept along with any other non-matching entries.

Comment on lines +69 to +79
return CONFIG.hiveClient.broadcast.updateAccount(
{
account: accountData.name,
json_metadata: accountData.json_metadata,
owner: prepareAuth("owner"),
active: prepareAuth("active"),
posting: prepareAuth("posting"),
memo_key: accountData.memo_key
},
currentKey
);
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 | 🟠 Major

memo_key is not updated with the new key.

The mutation updates owner, active, and posting authorities with new keys, but memo_key still uses accountData.memo_key. This leaves the memo key unchanged after a password change, which may not be the intended behavior.

Proposed fix if memo_key should also be updated
         return CONFIG.hiveClient.broadcast.updateAccount(
           {
             account: accountData.name,
             json_metadata: accountData.json_metadata,
             owner: prepareAuth("owner"),
             active: prepareAuth("active"),
             posting: prepareAuth("posting"),
-            memo_key: accountData.memo_key
+            memo_key: newKeys.memo_key.createPublic().toString()
           },
           currentKey
         );
🤖 Prompt for AI Agents
In @apps/web/src/api/mutations/change-password.ts around lines 69 - 79, The
updateAccount call is leaving memo_key as accountData.memo_key so the memo key
isn't rotated; compute the new memo public key from the new password/key (the
same place you derive owner/active/posting keys) and replace
accountData.memo_key with that new memo key in the arguments to
CONFIG.hiveClient.broadcast.updateAccount; look for the prepareAuth helper and
where new keys are derived (e.g., functions that produce owner/active/posting
keys) and use that same derivation to produce the new memo_key passed to
updateAccount along with owner/active/posting and currentKey.

Comment on lines +22 to +28
const { data: accountOwnerPublicKeys } = useQuery({
...getAccountFullQueryOptions(activeUser?.username!),
select: (resp) => resp.owner.key_auths as [string, number][]
});
const { data: keys } = useHiveKeysQuery(activeUser?.username!);

const { mutateAsync: changePassword } = useAccountChangePassword(activeUser?.username!);
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 | 🟡 Minor

Non-null assertions on potentially undefined username could cause runtime errors.

If activeUser is undefined or username is missing, these queries will be called with undefined!, which could lead to unexpected behavior or API errors. Consider adding early returns or guards.

Suggested approach
+  if (!activeUser?.username) {
+    return null; // or a loading/login prompt
+  }
+
   const { data: accountOwnerPublicKeys } = useQuery({
-    ...getAccountFullQueryOptions(activeUser?.username!),
+    ...getAccountFullQueryOptions(activeUser.username),
     select: (resp) => resp.owner.key_auths as [string, number][]
   });
-  const { data: keys } = useHiveKeysQuery(activeUser?.username!);
+  const { data: keys } = useHiveKeysQuery(activeUser.username);

-  const { mutateAsync: changePassword } = useAccountChangePassword(activeUser?.username!);
+  const { mutateAsync: changePassword } = useAccountChangePassword(activeUser.username);
🤖 Prompt for AI Agents
In
@apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-change-password.tsx
around lines 22 - 28, activeUser?.username is being non-null asserted when
calling useQuery(getAccountFullQueryOptions), useHiveKeysQuery and
useAccountChangePassword which can blow up if activeUser is undefined; instead
derive a safe username const (e.g. const username = activeUser?.username) and
keep hooks called unconditionally but pass an enabled flag or guarded query
options so they do nothing until username is truthy: update
getAccountFullQueryOptions call and useQuery to include enabled: !!username (or
return early from select if username missing), call useHiveKeysQuery(username)
with an options param or internal enabled check so it waits for username, and
ensure useAccountChangePassword is invoked in a way that defers mutate creation
until username exists (via hook options or by returning a no-op mutate) rather
than using the non-null assertion on activeUser?.username!.

Comment on lines +43 to +59
const handleChangePassword = useCallback(async () => {
if (keys && currentKey) {
await changePassword({
newKeys: {
owner: PrivateKey.fromString(keys.owner),
active: PrivateKey.fromString(keys.active),
posting: PrivateKey.fromString(keys.posting),
memo_key: PrivateKey.fromString(keys.memo)
},
currentPassword: rawKey!
});

setShowSeed(false);
setCurrentKey(undefined);
success(i18next.t("permissions.change-password.success"));
}
}, [keys, currentKey, activeUser, rawKey]);
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 | 🟡 Minor

Missing error handling for changePassword mutation.

If the mutation fails, the user receives no feedback. Consider wrapping in try/catch or using the mutation's error state.

Suggested approach
   const handleChangePassword = useCallback(async () => {
     if (keys && currentKey) {
+      try {
         await changePassword({
           newKeys: {
             owner: PrivateKey.fromString(keys.owner),
             active: PrivateKey.fromString(keys.active),
             posting: PrivateKey.fromString(keys.posting),
-            memo_key: PrivateKey.fromString(keys.memo)
+            memo: PrivateKey.fromString(keys.memo)
           },
           currentPassword: rawKey!
         });

         setShowSeed(false);
         setCurrentKey(undefined);
         success(i18next.t("permissions.change-password.success"));
+      } catch (e) {
+        error(i18next.t("permissions.change-password.error"));
+      }
     }
   }, [keys, currentKey, activeUser, rawKey]);
🤖 Prompt for AI Agents
In
@apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-change-password.tsx
around lines 43 - 59, handleChangePassword currently awaits changePassword but
has no error handling, so failures give no user feedback; wrap the async call in
a try/catch inside handleChangePassword (or use the mutation's onError/state)
and on error call the UI error handler (e.g., error(i18next.t(...)) or surface
the mutation error message) while ensuring finally logic (setShowSeed(false),
setCurrentKey(undefined)) still runs only on success or is handled
appropriately; update the dependency usage around rawKey and currentKey as
needed.

Comment on lines +46 to +51
newKeys: {
owner: PrivateKey.fromString(keys.owner),
active: PrivateKey.fromString(keys.active),
posting: PrivateKey.fromString(keys.posting),
memo_key: PrivateKey.fromString(keys.memo)
},
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 | 🟠 Major

Key name mismatch: memo_key vs memo.

The mutation's Payload interface in change-password.ts expects newKeys to be a Record<string, PrivateKey> and prepareAuth is called with "owner", "active", "posting" as keyof Keys. However, here you're passing memo_key while the mutation likely expects memo to match the authority key names.

Proposed fix
         newKeys: {
           owner: PrivateKey.fromString(keys.owner),
           active: PrivateKey.fromString(keys.active),
           posting: PrivateKey.fromString(keys.posting),
-          memo_key: PrivateKey.fromString(keys.memo)
+          memo: PrivateKey.fromString(keys.memo)
         },
📝 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
newKeys: {
owner: PrivateKey.fromString(keys.owner),
active: PrivateKey.fromString(keys.active),
posting: PrivateKey.fromString(keys.posting),
memo_key: PrivateKey.fromString(keys.memo)
},
newKeys: {
owner: PrivateKey.fromString(keys.owner),
active: PrivateKey.fromString(keys.active),
posting: PrivateKey.fromString(keys.posting),
memo: PrivateKey.fromString(keys.memo)
},
🤖 Prompt for AI Agents
In
@apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-change-password.tsx
around lines 46 - 51, The newKeys object uses the wrong key name "memo_key";
change it to "memo" so it matches the Payload/Keys expected by the mutation and
prepareAuth calls—replace the property memo_key with memo (using
PrivateKey.fromString(keys.memo)) in the newKeys literal and ensure any callers
using prepareAuth("owner"/"active"/"posting"/"memo") and the Payload type in
change-password.ts align with this name.

<div className="flex flex-col items-start gap-4 w-full">
<KeyInput
keyType="owner"
placeholder="Current key"
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 | 🟡 Minor

Hardcoded English string should use i18n.

The placeholder "Current key" is not internationalized.

           <KeyInput
             keyType="owner"
-            placeholder="Current key"
+            placeholder={i18next.t("permissions.change-password.current-key-placeholder")}
             className="w-full"
             ref={keyInputRef}
           />
📝 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
placeholder="Current key"
<KeyInput
keyType="owner"
placeholder={i18next.t("permissions.change-password.current-key-placeholder")}
className="w-full"
ref={keyInputRef}
/>
🤖 Prompt for AI Agents
In
@apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-change-password.tsx
at line 67, Replace the hardcoded placeholder "Current key" in the
ManageKeysChangePassword component's input (the placeholder prop on the input
element in manage-keys-change-password.tsx) with a call to the project's i18n
translation helper (e.g., useTranslations()/t() or your existing i18n hook),
e.g., const t = useTranslations('profile.permissions'); then use t('currentKey')
(or the appropriate key) for the placeholder; add the new translation key to the
locale files.

Comment on lines +21 to +38
<div
className={clsx(
"cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
tab === "password" && "bg-gray-100 dark:bg-gray-100/10 "
)}
onClick={() => setTab("password")}
>
{i18next.t("permissions.change-password.title")}
</div>
<div
className={clsx(
"cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
tab === "key" && "bg-gray-100 dark:bg-gray-100/10 "
)}
onClick={() => setTab("key")}
>
{i18next.t("permissions.keys.title")}
</div>
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 | 🟡 Minor

Missing keyboard accessibility for tab navigation.

The tab elements are clickable divs without keyboard support. Users navigating via keyboard cannot activate these tabs.

Add keyboard accessibility
             <div
               className={clsx(
                 "cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
                 tab === "password" && "bg-gray-100 dark:bg-gray-100/10 "
               )}
               onClick={() => setTab("password")}
+              role="tab"
+              tabIndex={0}
+              onKeyDown={(e) => e.key === "Enter" && setTab("password")}
+              aria-selected={tab === "password"}
             >

Apply similarly to the "key" tab button.

📝 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
<div
className={clsx(
"cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
tab === "password" && "bg-gray-100 dark:bg-gray-100/10 "
)}
onClick={() => setTab("password")}
>
{i18next.t("permissions.change-password.title")}
</div>
<div
className={clsx(
"cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
tab === "key" && "bg-gray-100 dark:bg-gray-100/10 "
)}
onClick={() => setTab("key")}
>
{i18next.t("permissions.keys.title")}
</div>
<div
className={clsx(
"cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
tab === "password" && "bg-gray-100 dark:bg-gray-100/10 "
)}
onClick={() => setTab("password")}
role="tab"
tabIndex={0}
onKeyDown={(e) => e.key === "Enter" && setTab("password")}
aria-selected={tab === "password"}
>
{i18next.t("permissions.change-password.title")}
</div>
<div
className={clsx(
"cursor-pointer text-sm font-semibold rounded-full px-2 py-1",
tab === "key" && "bg-gray-100 dark:bg-gray-100/10 "
)}
onClick={() => setTab("key")}
role="tab"
tabIndex={0}
onKeyDown={(e) => e.key === "Enter" && setTab("key")}
aria-selected={tab === "key"}
>
{i18next.t("permissions.keys.title")}
</div>
🤖 Prompt for AI Agents
In
@apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys.tsx
around lines 21 - 38, The tab elements are non-interactive divs and lack
keyboard accessibility; update the tab UI (the elements that read tab, use
setTab("password") and setTab("key")) to be keyboard-focusable and activatable
by converting them to semantic <button> elements or adding role="button",
tabIndex={0}, and an onKeyDown handler that calls setTab when Enter or Space is
pressed; ensure the active styling logic (tab === "password"/"key") and the
i18next.t labels remain unchanged so the visual state and localization are
preserved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants