-
Notifications
You must be signed in to change notification settings - Fork 30
web-next: migrate passkey feature #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Warning Rate limit exceeded@Perlmint has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 16 minutes and 31 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (5)
WalkthroughAdds end-to-end WebAuthn (passkey) support: GraphQL types and mutations for registration/authentication/revocation; passkey UI and flows in settings and sign-in pages; an AlertDialog UI primitive; and localized passkey strings across multiple locales. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Web as Web App (Settings)
participant GQL as GraphQL API
participant Service as Passkey Service
participant Auth as Browser WebAuthn
User->>Web: Click "Register" (name)
Web->>GQL: getPasskeyRegistrationOptions(accountId)
GQL->>Service: getRegistrationOptions(kv, origin, account)
Service-->>GQL: options (JSON)
GQL-->>Web: options
Web->>Auth: startRegistration(options)
Auth-->>Web: registrationResponse
Web->>GQL: verifyPasskeyRegistration(accountId, name, registrationResponse)
GQL->>Service: verifyRegistration(db, kv, origin, account, name, response)
Service-->>GQL: { verified, passkey? }
GQL-->>Web: { verified, passkey? }
Web-->>User: show success / refresh
sequenceDiagram
autonumber
actor User
participant Web as Web App (Sign-in)
participant GQL as GraphQL API
participant Service as Passkey Service
participant Auth as Browser WebAuthn
participant Sess as Session Store
User->>Web: Click "Sign in with passkey"
Web->>Web: gen sessionId (UUID)
Web->>GQL: getPasskeyAuthenticationOptions(sessionId)
GQL->>Service: getAuthenticationOptions(kv, origin, sessionId)
Service-->>GQL: options (JSON)
GQL-->>Web: options
Web->>Auth: startAuthentication(options)
Auth-->>Web: authenticationResponse
Web->>GQL: loginByPasskey(sessionId, authenticationResponse)
GQL->>Service: verifyAuthentication(db, kv, origin, sessionId, response)
Service-->>GQL: { accountId } or null
alt success
GQL->>Sess: createSession(accountId, ip, ua)
Sess-->>GQL: SessionRef
GQL-->>Web: SessionRef
Web-->>User: set cookie & redirect
else failed
GQL-->>Web: null
Web-->>User: show error / stay
end
sequenceDiagram
autonumber
actor User
participant Web as Web App (Settings)
participant GQL as GraphQL API
participant DB as DB
User->>Web: Confirm "Revoke" on passkey
Web->>GQL: revokePasskey(passkeyId)
GQL->>DB: validate ownership and delete
DB-->>GQL: ok / not found / unauthorized
alt deleted
GQL-->>Web: success (ID)
Web-->>User: show success / refresh
else error
GQL-->>Web: error/null
Web-->>User: show error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of Changes
Hello @Perlmint, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request introduces comprehensive support for passkeys, a modern, secure, and user-friendly authentication method. It enables users to register, manage, and log in to their accounts using passkeys, significantly enhancing the overall security posture and user experience by moving towards a passwordless future. The changes span both the backend GraphQL API and the frontend web application, providing a complete end-to-end solution for passkey integration.
Highlights
- Passkey Registration: Users can now register new passkeys directly from their account settings, providing a secure and convenient alternative to traditional password-based authentication.
- Passkey Login Integration: The sign-in page has been enhanced to support logging in with registered passkeys, streamlining the authentication process for users.
- Passkey Management UI: A dedicated section in user settings allows for the management of registered passkeys, including the ability to view details and revoke access for specific keys.
- GraphQL API Expansion: New GraphQL API endpoints have been introduced to support all passkey operations, ensuring a robust and scalable backend for the feature.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command>
or @gemini-code-assist <command>
. Below is a summary of the supported commands.
Feature | Command | Description |
---|---|---|
Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/
folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request effectively migrates the passkey management and login features. The backend changes introduce the necessary GraphQL mutations and types for passkeys, while the frontend adds a new settings page for managing them and integrates passkey login into the sign-in flow. The implementation is solid, with good use of modern authentication patterns. My review includes suggestions to improve code maintainability by refactoring duplicated logic in GraphQL resolvers, and to enhance the user experience by replacing full-page reloads with proper data refetching and providing clearer error feedback on the sign-in page.
// TODO: Fix Relay query refreshing - currently loadQuery doesn't update the UI | ||
// The loadQuery call fetches data but doesn't update the createPreloadedQuery | ||
// Need to investigate proper solid-relay patterns for refreshing queries | ||
window.location.reload(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using window.location.reload()
to refresh data provides a jarring user experience. While the TODO
comment indicates you're aware of this, it's a significant UX issue that should be prioritized.
Instead of a full-page reload, please investigate the data refetching capabilities of solid-relay
or the router's query
integration. Typically, query hooks provide a refetch
function. If that's not available, you might need to trigger a re-evaluation of the loadPageQuery
call to get fresh data without reloading the entire application.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@XiNiHa Would be there any better way to refresh here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The
Account.passkeys
should be paginated using@connection
andcreatePaginationFragment()
Mutation.verifyPasskeyRegistration
should be modified to return the registered passkey- The returned passkey should be added back to the connection either using the
@appendNode
directive or the imperative updater
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made it. Thank you.
const session = await ctx.session; | ||
if (session == null) throw new Error("Not authenticated."); | ||
if (session.accountId !== args.accountId.id) { | ||
throw new Error("Not authorized."); | ||
} | ||
const account = await ctx.db.query.accountTable.findFirst({ | ||
where: { id: args.accountId.id }, | ||
with: { passkeys: true }, | ||
}); | ||
if (account == null) throw new Error("Account not found."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's significant code duplication for authentication, authorization, and account fetching logic across getPasskeyRegistrationOptions
, verifyPasskeyRegistration
, and revokePasskey
mutations. This makes the code harder to maintain.
To improve this, you could extract the common logic into a helper function. This function would handle session checks and fetch the authorized account, which can then be used by all three mutations. This would centralize the logic, making it easier to update and test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
web-next/src/locales/zh-CN/messages.po (1)
642-647
: Placeholder order is reversed; this will render incorrectly.The English source is “available on {0} under the {1} license.” Your translation swaps placeholders.
Apply:
- msgid "The source code of this website is available on {0} under the {1} license." - msgstr "可在 {1} 上以 {0} 许可获取该网站的源代码。" + msgid "The source code of this website is available on {0} under the {1} license." + msgstr "可在 {0} 上以 {1} 许可获取该网站的源代码。"web-next/src/routes/(root)/sign/index.tsx (1)
169-176
: Prevent open redirects by validating the “next” parameterAn attacker could supply
?next=https://evil.tld
, causing your app to redirect off-site post-login. Ensure you only honor same-origin, absolute-path values. Update all three occurrences in web-next/src/routes/(root)/sign/index.tsx:• Lines 172–176 (building the email verification URL)
• Lines 265–271 (redirect after OAuth flow)
• Lines 333–337 (redirect after session set)Example replacement (apply at each location):
- const rawNext = searchParams.get("next") ?? "/"; + const rawNext = searchParams.get("next"); + const safeNext = rawNext && rawNext.startsWith("/") && !rawNext.startsWith("//") + ? rawNext + : "/"; // then use encodeURIComponent(safeNext) or window.location.href = safeNext• Lines 172–176:
- const verifyUrl = `${location.origin}/sign/in/{token}?code={code}&next=${ - encodeURIComponent(searchParams.get("next") ?? "/") - }`; + const rawNext = searchParams.get("next"); + const safeNext = rawNext && rawNext.startsWith("/") && !rawNext.startsWith("//") + ? rawNext + : "/"; + const verifyUrl = `${location.origin}/sign/in/{token}?code={code}&next=${encodeURIComponent(safeNext)}`;• Lines 265–271 and 333–337 (redirects):
- window.location.href = searchParams.get("next") ?? "/"; + const rawNext = new URL(location.href).searchParams.get("next"); + const safeNext = rawNext && rawNext.startsWith("/") && !rawNext.startsWith("//") + ? rawNext + : "/"; + window.location.href = safeNext;These changes thwart open-redirect attempts by allowing only internal paths.
🧹 Nitpick comments (24)
web-next/src/locales/en-US/messages.po (1)
472-480
: Confirm capitalization consistency for “Passkeys”.You have both “passkeys” (lowercase) and “Passkeys” (title case). If “passkeys” is a route/slug, keep lowercase; otherwise, prefer title case for UI labels.
Also applies to: 557-563
graphql/schema.graphql (3)
18-24
: WebAuthn GraphQL shape looks good; please verify auth scope, challenge binding, and replay protection.The schema is clean and pragmatic (JSON for WebAuthn payloads is fine). However, several security-critical aspects aren’t visible at the SDL level:
- Account.passkeys: ensure it’s restricted to the account owner (and admins). Consider adding a short field doc comment to communicate ordering and auth expectations.
- getPasskeyAuthenticationOptions/loginByPasskey: confirm the challenge is bound to sessionId, origin (Relying Party ID), and expires promptly; reject replay.
- getPasskeyRegistrationOptions/verifyPasskeyRegistration: enforce per-account name uniqueness (UI has the error text), and ensure only the owner can register/revoke.
- revokePasskey: verify that only the passkey owner (or admin) can revoke, and that revoking invalidates any outstanding challenges for that key.
Optional ergonomics:
- Consider returning the Passkey (or passkeyId) from revokePasskey for simpler client cache updates.
- sessionId might be clearer as challengeId/authSessionId to avoid conflation with Session.id.
If desired, add a field description to clarify ordering and access:
type Account implements Node { @@ - passkeys(after: String, before: String, first: Int, last: Int): AccountPasskeysConnection! + """Account's passkeys (owner-only). Ordered by created desc unless specified.""" + passkeys(after: String, before: String, first: Int, last: Int): AccountPasskeysConnection! }Also applies to: 99-108, 418-485, 555-560
451-455
: Fix typos in docstrings ("variabvles" → "variables").Apply:
- The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. + The RFC 6570-compliant URI Template for the verification link. Available variables: `{token}` and `{code}`.Also applies to: 470-473
475-475
: Consider returning richer data from revokePasskey.Returning Boolean! prevents clients from updating caches optimally. Returning the revoked Passkey (or at least its id) is often more ergonomic:
- revokePasskey(passkeyId: ID!): Boolean! + revokePasskey(passkeyId: ID!): PasskeyNon-breaking alternative: keep Boolean and add a separate node fetch in the client.
web-next/src/locales/ko-KR/messages.po (1)
182-185
: Spacing and ellipsis fixes for natural Korean.
- Use spacing around “중” and the ellipsis glyph.
- Keep colon ASCII consistently or fullwidth consistently across strings.
Apply:
- msgid "Authenticating..." - msgstr "인증중..." + msgid "Authenticating..." + msgstr "인증 중…" - msgid "Registering..." - msgstr "등록중..." + msgid "Registering..." + msgstr "등록 중…" - msgid "Cancel" - msgstr "취소" + msgid "Cancel" + msgstr "취소" # unchanged here; listed to show contextAlso applies to: 534-537, 195-198
web-next/src/locales/zh-CN/messages.po (1)
178-181
: Tone and pronoun consistency.
- The string about “already changed” reads very colloquial. Suggest a neutral tone.
- Consider using 您 consistently around account and settings flows; you already use it elsewhere.
Apply:
- msgid "As you have already changed it {0}, you can't change it again." - msgstr "自打你已经把用户名换成 {0} 了,你再也改不了了。" + msgid "As you have already changed it {0}, you can't change it again." + msgstr "由于你已在 {0} 更改过,因此无法再次更改。" - msgid "The following passkeys are registered to your account. You can use them to sign in to your account." - msgstr "以下通行密钥已注册到你的账户。你可以使用它们登录你的账户。" + msgid "The following passkeys are registered to your account. You can use them to sign in to your account." + msgstr "以下通行密钥已注册到您的账户。您可以使用它们登录。"Also applies to: 630-633
web-next/src/components/ui/alert-dialog.tsx (2)
119-142
: Offer a semantic alias for cancel actions.Most design systems expose AlertDialogCancel explicitly (even when implemented with CloseButton). Exporting a semantic alias improves readability without breaking your current API.
export const AlertDialogClose = <T extends ValidComponent = "button">( props: PolymorphicProps<T, alertDialogCloseProps<T>>, ) => { @@ }; +// Semantic alias for cancel actions; implemented with CloseButton. +export const AlertDialogCancel = AlertDialogClose;
144-155
: Consider exposing a variant prop for AlertDialogAction.Right now, styling can only be overridden via class. If your buttonVariants supports variants (default, destructive, etc.), allowing a variant prop improves DX. Example (if using class-variance-authority):
- export const AlertDialogAction = <T extends ValidComponent = "button">( - props: PolymorphicProps<T, alertDialogCloseProps<T>>, -) => { +import type { VariantProps } from "class-variance-authority"; +type ButtonVariant = VariantProps<typeof buttonVariants>["variant"]; + +export const AlertDialogAction = <T extends ValidComponent = "button">( + props: PolymorphicProps< + T, + alertDialogCloseProps<T> & { variant?: ButtonVariant } + >, +) => { const [local, rest] = splitProps(props as alertDialogCloseProps, ["class"]); return ( <AlertDialogPrimitive.CloseButton - class={cn(buttonVariants(), local.class)} + class={cn(buttonVariants({ variant: (props as any).variant }), local.class)} {...rest} /> ); };graphql/passkey.ts (5)
35-41
: Stabilize cursor pagination by adding a tie-breaker and aligning sort with cursorUsing only created as the cursor can duplicate/skip rows when multiple passkeys share the same timestamp. Also, orderBy should be deterministic across equal created values.
- Encode the cursor as createdMillis:id.
- Sort by created DESC/ASC and id DESC/ASC accordingly.
- Update the where clause to handle the secondary key.
- toCursor: (passkey) => passkey.created.valueOf().toString(), + toCursor: (p) => `${p.created.valueOf()}:${p.id}`,And in the query:
- .orderBy( - inverted ? passkeyTable.created : desc(passkeyTable.created), - ).limit(limit); + .orderBy( + inverted + ? passkeyTable.created + : desc(passkeyTable.created), + inverted ? passkeyTable.id : desc(passkeyTable.id), + ) + .limit(limit);You’ll also need to update the before/after predicates to compare (created, id). See the next comment for parsing changes that make this straightforward. -->
Also applies to: 64-66
43-61
: Harden cursor parsing; ignore invalid cursors and support compound cursor parsingCurrently Number(before)/Number(after) can yield NaN if a malicious/invalid cursor is supplied, which turns into Invalid Date. Prefer defensive parsing; also parse the compound cursor (timestamp:id) if you adopt the tie-breaker.
- const beforeDate = before ? new Date(Number(before)) : undefined; - const afterDate = after ? new Date(Number(after)) : undefined; + const parseCursor = (cur?: string) => { + if (!cur) return undefined as + | { ts: number; id?: string } + | undefined; + const [tsStr, id] = cur.split(":"); + const ts = Number(tsStr); + if (!Number.isFinite(ts)) return undefined; + return { ts, id }; + }; + const beforeCur = parseCursor(before); + const afterCur = parseCursor(after);Follow-up: adjust the where predicates:
- before - ? inverted - ? lt(passkeyTable.created, beforeDate!) - : gt(passkeyTable.created, beforeDate!) + beforeCur + ? inverted + ? or( + lt(passkeyTable.created, new Date(beforeCur.ts)), + and( + eq(passkeyTable.created, new Date(beforeCur.ts)), + lt(passkeyTable.id, beforeCur.id!), + ), + ) + : or( + gt(passkeyTable.created, new Date(beforeCur.ts)), + and( + eq(passkeyTable.created, new Date(beforeCur.ts)), + gt(passkeyTable.id, beforeCur.id!), + ), + ) : undefined, - after - ? inverted - ? gt(passkeyTable.created, afterDate!) - : lt(passkeyTable.created, afterDate!) + afterCur + ? inverted + ? or( + gt(passkeyTable.created, new Date(afterCur.ts)), + and( + eq(passkeyTable.created, new Date(afterCur.ts)), + gt(passkeyTable.id, afterCur.id!), + ), + ) + : or( + lt(passkeyTable.created, new Date(afterCur.ts)), + and( + eq(passkeyTable.created, new Date(afterCur.ts)), + lt(passkeyTable.id, afterCur.id!), + ), + ) : undefined,This makes pagination deterministic and resilient to malformed cursors. -->
89-95
: Add TTL when storing registration options (parity with authentication options)getRegistrationOptions stores challenges in KV without expiration, which can accumulate and linger. Authentication options already use a 5-minute TTL. Mirror that here.
Apply this change in models/passkey.ts (not in this file):
- await kv.set(`${KV_NAMESPACE}/registration/${account.id}`, options); + await kv.set( + `${KV_NAMESPACE}/registration/${account.id}`, + options, + 5 * 60 * 1000, // 5 minutes + );Optional: Consider deleting the registration entry upon successful verification to free KV and prevent any accidental reuse. -->
97-125
: Enforce unique passkey name per account to match UI copy and avoid duplicatesUI/locale says “A passkey with this name already exists.” but the backend doesn’t enforce uniqueness. Add a pre-check to keep UX and data consistent and prevent race duplicates.
async resolve(_, args, ctx) { const session = await ctx.session; if (session == null) throw new Error("Not authenticated."); if (session.accountId !== args.accountId.id) { throw new Error("Not authorized."); } + // Enforce unique name per account (case-insensitive, trimmed) + const trimmed = args.name.trim(); + const dup = await ctx.db.query.passkeyTable.findFirst({ + where: and( + eq(passkeyTable.accountId, args.accountId.id), + // case-insensitive match for name + sql`lower(${passkeyTable.name}) = lower(${trimmed})`, + ), + columns: { id: true }, + }); + if (dup) { + throw new Error("A passkey with this name already exists."); + }If you’d prefer hard guarantees, add a unique index on (account_id, lower(name)) and handle constraint violations. Do you want me to propose the SQL/Drizzle migration? -->
126-145
: Avoid existence leaks in revokePasskey response semanticsReturning false for non-existent IDs while throwing for other users can leak whether a particular passkey exists. Consider making the mutation idempotent and always returning true after authorization on the current account.
- const passkey = await ctx.db.query.passkeyTable.findFirst({ - where: { id: args.passkeyId.id }, - }); - if (passkey == null) return false; - if (passkey.accountId !== session.accountId) { - throw new Error("Not authorized."); - } - await ctx.db.delete(passkeyTable).where( - eq(passkeyTable.id, args.passkeyId.id), - ); - return true; + const passkey = await ctx.db.query.passkeyTable.findFirst({ + where: { id: args.passkeyId.id }, + columns: { id: true, accountId: true }, + }); + if (passkey && passkey.accountId === session.accountId) { + await ctx.db + .delete(passkeyTable) + .where(eq(passkeyTable.id, args.passkeyId.id)); + } + // Always return true for authorized callers on their own account. + return true;If you want to surface “not mine” versus “not found”, keep current semantics. This is just to reduce enumeration risk. -->
web-next/src/components/SettingsTabs.tsx (1)
31-53
: LGTM: new “Passkeys” tab wiring and layout
- grid-cols-3 matches the new third tab.
- Hrefs point to the expected routes.
Optional: if tabs may grow, consider a responsive auto-fit grid to avoid manual col counts.
- <TabsList class="grid max-w-prose mx-auto grid-cols-3"> + <TabsList class="grid max-w-prose mx-auto grid-cols-3 sm:grid-cols-3 md:auto-cols-fr md:grid-flow-col">graphql/login.ts (2)
269-286
: Consider setting explicit WebAuthn options (UX/security knobs)getPasskeyAuthenticationOptions currently relies on defaults. Optionally set:
- userVerification: "preferred" (or "required" if you want stricter),
- timeout: e.g., 60_000,
- allowCredentials: omit (as you do) when you don’t know the account yet.
This is optional and can be tuned later based on UX feedback.
-->
316-323
: DRY up session creation logiccreateSession payload construction is duplicated with completeLoginChallenge. Extract a small helper (e.g., createSessionFromRequest(ctx, accountId)) to centralize remote IP and user-agent capture.
Example helper (outside this file):
export function sessionPayloadFromCtx(ctx: Context, accountId: Uuid) { const remoteAddr = ctx.connectionInfo?.remoteAddr; return { accountId, ipAddress: remoteAddr?.transport === "tcp" ? remoteAddr.hostname : undefined, userAgent: ctx.request.headers.get("User-Agent"), }; }Then:
- const remoteAddr = ctx.connectionInfo?.remoteAddr; - return await createSession(ctx.kv, { - accountId: account.id, - ipAddress: remoteAddr?.transport === "tcp" - ? remoteAddr.hostname - : undefined, - userAgent: ctx.request.headers.get("User-Agent"), - }); + return await createSession(ctx.kv, sessionPayloadFromCtx(ctx, account.id));web-next/src/routes/(root)/sign/index.tsx (2)
343-347
: Surface passkey errors to users (toast) instead of just console.error.Provide actionable feedback on failure.
Apply this diff:
- } catch (error) { - console.error("Passkey authentication failed:", error); - // You could show a toast or error message here + } catch (error) { + console.error("Passkey authentication failed:", error); + // Optional: user-visible error + // showToast({ + // title: t`Sign in with passkey`, + // description: error instanceof Error ? error.message : t`Authentication failed.`, + // variant: "error", + // });And add the import near other UI imports:
+import { showToast } from "~/components/ui/toast.tsx";
413-416
: Nit: unify ellipsis with the rest of the UI.Elsewhere you use … (single Unicode ellipsis). Consider aligning.
Apply this diff:
- {passkeyAuthenticating() - ? t`Authenticating...` - : t`Sign in with passkey`} + {passkeyAuthenticating() + ? t`Authenticating…` + : t`Sign in with passkey`}web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (3)
173-177
: Defensive null-safety when reading edges.Guard against potential nulls in edges/nodes to avoid runtime errors if schema ever relaxes non-null.
Apply this diff:
- const existingNames = account.passkeys.edges.map((edge) => - edge.node.name.toLowerCase() - ); + const existingNames = (account.passkeys.edges ?? []) + .map((edge) => edge?.node?.name?.toLowerCase()) + .filter((n): n is string => !!n);
141-146
: Avoid full-page reload for refresh; prefer Relay store updates or refetch.The TODO notes a full reload. Consider:
- Return the created Passkey node (or edge) from verify mutation and append to the connection via updater.
- On revoke, delete node from store via updater or use @deleteRecord directive if available.
- Alternatively, convert to a loadable query and call loadQuery again with a new key.
392-393
: Nit: consistent ellipsis.Align “Registering...” with the rest of the UI using the Unicode ellipsis.
Apply this diff:
- {registering() ? t`Registering...` : t`Register`} + {registering() ? t`Registering…` : t`Register`}web-next/src/locales/ja-JP/messages.po (3)
195-198
: JA translation: Prefer “キャンセル” for the button label.“取消” reads closer to “Revoke/Invalidate” rather than “Cancel” in UI context.
Apply this diff:
-msgid "Cancel" -msgstr "取消" +msgid "Cancel" +msgstr "キャンセル"
346-349
: JA translation: Natural phrasing for “Last used:”.“最終使用日:” (or “最終利用日:”) is more natural for a timestamp label.
Apply this diff:
-msgid "Last used:" -msgstr "最終使用:" +msgid "Last used:" +msgstr "最終使用日:"
574-577
: JA translation consistency: Use ログイン across sign-in strings.Other strings use ログイン; align this one.
Apply this diff:
-msgid "Sign in with passkey" -msgstr "パスキーでサインイン" +msgid "Sign in with passkey" +msgstr "パスキーでログイン"
📜 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.
📒 Files selected for processing (13)
graphql/login.ts
(3 hunks)graphql/mod.ts
(1 hunks)graphql/passkey.ts
(1 hunks)graphql/schema.graphql
(6 hunks)web-next/src/components/SettingsTabs.tsx
(3 hunks)web-next/src/components/ui/alert-dialog.tsx
(1 hunks)web-next/src/locales/en-US/messages.po
(18 hunks)web-next/src/locales/ja-JP/messages.po
(18 hunks)web-next/src/locales/ko-KR/messages.po
(18 hunks)web-next/src/locales/zh-CN/messages.po
(18 hunks)web-next/src/locales/zh-TW/messages.po
(18 hunks)web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
(1 hunks)web-next/src/routes/(root)/sign/index.tsx
(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (3)
models/passkey.ts (1)
verifyRegistration
(47-85)web-next/src/components/ProfilePageBreadcrumb.tsx (1)
ProfilePageBreadcrumb
(16-49)web-next/src/components/SettingsTabs.tsx (1)
SettingsTabs
(16-58)
graphql/passkey.ts (2)
models/schema.ts (1)
passkeyTable
(143-171)models/passkey.ts (2)
getRegistrationOptions
(28-45)verifyRegistration
(47-85)
graphql/login.ts (4)
models/passkey.ts (2)
getAuthenticationOptions
(87-101)verifyAuthentication
(103-148)models/uuid.ts (1)
Uuid
(4-4)graphql/session.ts (1)
SessionRef
(20-20)models/session.ts (1)
createSession
(18-28)
web-next/src/components/ui/alert-dialog.tsx (2)
web-next/src/lib/utils.ts (1)
cn
(4-6)web-next/src/components/ui/button.tsx (1)
buttonVariants
(64-64)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
[error] 69-69: Shouldn't redeclare 'passkeysPageQuery'. Consider to delete it or rename it.
'passkeysPageQuery' is defined here:
(lint/suspicious/noRedeclare)
web-next/src/routes/(root)/sign/index.tsx
[error] 85-85: Shouldn't redeclare 'signGetPasskeyAuthenticationOptionsMutation'. Consider to delete it or rename it.
'signGetPasskeyAuthenticationOptionsMutation' is defined here:
(lint/suspicious/noRedeclare)
[error] 91-91: Shouldn't redeclare 'signByPasskeyMutation'. Consider to delete it or rename it.
'signByPasskeyMutation' is defined here:
(lint/suspicious/noRedeclare)
⏰ 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: test
🔇 Additional comments (6)
web-next/src/components/ui/alert-dialog.tsx (2)
31-47
: LGTM: structure and state classes match Kobalte’s pattern.
- Using Portal > Overlay > Content with data-[expanded]/data-[closed] classes aligns with Kobalte’s Dialog/AlertDialog data attributes and shadcn-solid’s recipes. This should animate correctly. (kobalte.dev, shadcn-solid.com)
Also applies to: 91-117
34-41
: Minor: verify Kobalte state tokens if you tweak animations later.You’re using data-[expanded]/data-[closed], which is correct per Kobalte’s Dialog docs and applies to Overlay/Content as well. If the library updates these tokens, animations may silently break; keep an eye on releases. (kobalte.dev)
graphql/mod.ts (1)
9-9
: LGTM: passkey GraphQL is now wired into the schemaImporting "./passkey.ts" ensures the Passkey node, connection, and mutations are part of the schema. The schema snapshot writer below will capture changes.
web-next/src/components/SettingsTabs.tsx (1)
9-9
: Type surface updated correctlyIncluding "passkeys" in SettingsTab ensures the new tab is type-safe across callers.
graphql/login.ts (1)
288-324
: Type bug in verifyAuthentication: wrong options type used in KV getmodels/passkey.ts retrieves authentication options with PublicKeyCredentialCreationOptionsJSON (registration) instead of PublicKeyCredentialRequestOptionsJSON (authentication). It works at runtime because only challenge/rpID are read, but it’s incorrect and risks future breakage.
Proposed fix in models/passkey.ts:
-import type { - PublicKeyCredentialCreationOptionsJSON, -} from "@simplewebauthn/server"; +import type { + PublicKeyCredentialRequestOptionsJSON, +} from "@simplewebauthn/server"; ... - const options = await kv.get<PublicKeyCredentialCreationOptionsJSON>( + const options = await kv.get<PublicKeyCredentialRequestOptionsJSON>( `${KV_NAMESPACE}/authentication/${sessionId}`, );While here, consider deleting the KV entry after a successful verification (TTL already reduces risk, but explicit cleanup is nice). -->
Likely an incorrect or invalid review comment.
web-next/src/locales/ja-JP/messages.po (1)
182-185
: Use consistent Unicode ellipsis (…) in Japanese translationsApply the following diffs in
web-next/src/locales/ja-JP/messages.po
:#: src/routes/(root)/sign/index.tsx:414 -msgid "Authenticating..." -msgstr "認証中..." +msgid "Authenticating…" +msgstr "認証中…"#: src/routes/(root)/[handle]/settings/passkeys.tsx:392 -msgid "Registering..." -msgstr "登録中..." +msgid "Registering…" +msgstr "登録中…"After updating these entries, re-generate and compile your Lingui catalogs. From the repo root, for example:
cd web-next pnpm run i18n:extract pnpm run i18n:compile
Please confirm that the
i18n:extract
andi18n:compile
scripts are defined inweb-next/package.json
, or adjust the commands (e.g. usingnpx lingui extract && npx lingui compile
) as needed.
export const AlertDialogClose = <T extends ValidComponent = "button">( | ||
props: PolymorphicProps<T, alertDialogCloseProps<T>>, | ||
) => { | ||
const [local, rest] = splitProps(props as alertDialogCloseProps, ["class"]); | ||
|
||
return ( | ||
<AlertDialogPrimitive.CloseButton | ||
class={cn( | ||
buttonVariants({ | ||
variant: "outline", | ||
}), | ||
"mt-2 md:mt-0", | ||
local.class, | ||
)} | ||
{...rest} | ||
/> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Prevent accidental form submissions: set type="button" on Close/Action buttons.
Inside forms, a plain button defaults to type="submit". Since these are dialog controls, explicitly set type="button" to avoid unintended submits.
export const AlertDialogClose = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, alertDialogCloseProps<T>>,
) => {
@@
return (
<AlertDialogPrimitive.CloseButton
+ type="button"
class={cn(
buttonVariants({
variant: "outline",
}),
"mt-2 md:mt-0",
local.class,
)}
{...rest}
/>
);
};
export const AlertDialogAction = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, alertDialogCloseProps<T>>,
) => {
@@
return (
<AlertDialogPrimitive.CloseButton
+ type="button"
class={cn(buttonVariants(), local.class)}
{...rest}
/>
);
};
Also applies to: 144-155
🤖 Prompt for AI Agents
In web-next/src/components/ui/alert-dialog.tsx around lines 125-142 (and also
apply the same change to 144-155), the AlertDialog Close/Action buttons are
rendered without an explicit type, which makes them default to type="submit"
inside forms; fix this by adding type="button" on the rendered primitive
components (place the type attribute before spreading {...rest} so it acts as
the default and still allows an explicit override if desired), e.g., ensure the
AlertDialogPrimitive.CloseButton and the action button include type="button" to
prevent accidental form submissions.
import { | ||
type PublicKeyCredentialCreationOptionsJSON, | ||
type RegistrationResponseJSON, | ||
startRegistration, | ||
} from "@simplewebauthn/browser"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Guard registration behind support/secure-context checks.
Prevent avoidable failures on HTTP or unsupported browsers; provide UX feedback.
Apply this diff:
import {
type PublicKeyCredentialCreationOptionsJSON,
type RegistrationResponseJSON,
startRegistration,
+ browserSupportsWebAuthn,
} from "@simplewebauthn/browser";
@@
async function onRegisterPasskey() {
const account = data()?.accountByUsername;
const name = passkeyName().trim();
if (!account || !name) return;
+ if (!(typeof window !== "undefined" && window.isSecureContext)) {
+ showToast({
+ title: t`Failed to register passkey`,
+ description: t`Passkeys require a secure (HTTPS) context.`,
+ variant: "error",
+ });
+ return;
+ }
+ if (!browserSupportsWebAuthn()) {
+ showToast({
+ title: t`Failed to register passkey`,
+ description: t`Your browser does not support passkeys.`,
+ variant: "error",
+ });
+ return;
+ }
Also applies to: 179-186
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/[handle]/settings/passkeys.tsx around lines 1-5
(and also apply same change at lines 179-186), the registration flow currently
calls startRegistration unconditionally which can fail on non-secure contexts or
unsupported browsers; wrap the registration initiation in checks for
window.isSecureContext and for WebAuthn support (navigator.credentials and
PublicKeyCredential) and only call startRegistration when both are true; when
unsupported or insecure, prevent the call, surface a user-friendly message/state
(e.g., set an error or disabled UI) and avoid throwing runtime errors; ensure
the guard is applied in both places noted and update any UI logic to reflect the
unsupported/insecure state.
@@ -23,11 +28,13 @@ import { useLingui } from "~/lib/i18n/macro.d.ts"; | |||
import type { | |||
signByEmailMutation, | |||
} from "./__generated__/signByEmailMutation.graphql.ts"; | |||
import type { signByPasskeyMutation } from "./__generated__/signByPasskeyMutation.graphql.ts"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix Biome “noRedeclare” errors by aliasing imported GraphQL types (keep document constants as-is).
The const GraphQL documents share identifiers with imported types, tripping Biome’s noRedeclare. Alias the imported types and use them in generics.
Apply this diff:
-import type { signByPasskeyMutation } from "./__generated__/signByPasskeyMutation.graphql.ts";
+import type {
+ signByPasskeyMutation as SignByPasskeyMutation
+} from "./__generated__/signByPasskeyMutation.graphql.ts";
@@
-import type { signGetPasskeyAuthenticationOptionsMutation } from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts";
+import type {
+ signGetPasskeyAuthenticationOptionsMutation as SignGetPasskeyAuthenticationOptionsMutation
+} from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts";
@@
- const [getPasskeyOptions] = createMutation<
- signGetPasskeyAuthenticationOptionsMutation
- >(
+ const [getPasskeyOptions] = createMutation<
+ SignGetPasskeyAuthenticationOptionsMutation
+ >(
signGetPasskeyAuthenticationOptionsMutation,
);
@@
- const [loginByPasskey] = createMutation<signByPasskeyMutation>(
+ const [loginByPasskey] = createMutation<SignByPasskeyMutation>(
signByPasskeyMutation,
);
Also applies to: 37-37, 139-147
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/sign/index.tsx around lines 31, 37 and 139-147 the
imported GraphQL type names conflict with existing const document identifiers
causing Biome noRedeclare errors; fix by aliasing each imported type (e.g.,
import type { signByPasskeyMutation as SignByPasskeyMutationType } from
"./__generated__/signByPasskeyMutation.graphql.ts") and update any usages to the
aliased names in generics (keep the existing document const names unchanged) so
type imports no longer collide with value identifiers.
onMount(() => { | ||
// Automatically attempt passkey authentication when page loads | ||
if (!autoPasskeyAttempted()) { | ||
setAutoPasskeyAttempted(true); | ||
onPasskeyLogin(); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Guard auto passkey attempt behind feature/support checks to avoid unnecessary prompts.
Unconditionally starting WebAuthn on mount can annoy users on unsupported/HTTP contexts. Gate it on secure context + WebAuthn support.
Apply this diff:
import {
type AuthenticationResponseJSON,
type PublicKeyCredentialRequestOptionsJSON,
startAuthentication,
+ browserSupportsWebAuthn,
} from "@simplewebauthn/browser";
@@
onMount(() => {
- // Automatically attempt passkey authentication when page loads
- if (!autoPasskeyAttempted()) {
- setAutoPasskeyAttempted(true);
- onPasskeyLogin();
- }
+ // Automatically attempt passkey authentication when page loads on capable, secure browsers
+ if (!autoPasskeyAttempted()) {
+ setAutoPasskeyAttempted(true);
+ if (typeof window !== "undefined" && window.isSecureContext && browserSupportsWebAuthn()) {
+ onPasskeyLogin();
+ }
+ }
});
Also applies to: 2-6
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/sign/index.tsx around lines 151 to 157, the auto
passkey authentication is triggered unconditionally on mount; wrap the
onPasskeyLogin call in a guard that first checks the secure context and WebAuthn
support (e.g., window.isSecureContext && typeof window.PublicKeyCredential !==
'undefined' or navigator.credentials existence) and only then
setAutoPasskeyAttempted(true) and call onPasskeyLogin(); apply the same guard to
the other indicated locations (also applies to: 2-6).
edd227d
to
f2251f4
Compare
There was a problem hiding this 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 (9)
web-next/src/routes/(root)/sign/index.tsx (2)
343-350
: Improve error handling for passkey authenticationThe catch block currently swallows all errors and only conditionally shows a toast. Consider distinguishing between user cancellation (which shouldn't show an error) and actual failures.
- } catch (_) { + } catch (error) { if (showError) { + // Check if the error is due to user cancellation + const isUserCancellation = error instanceof Error && + (error.name === 'NotAllowedError' || + error.message.includes('cancelled') || + error.message.includes('canceled')); + + if (!isUserCancellation) { showToast({ title: t`Passkey authentication failed`, + description: error instanceof Error ? error.message : undefined, variant: "destructive", }); + } }
32-32
: Fix Biome noRedeclare errors by aliasing imported GraphQL typesThe GraphQL document constants share names with imported types, causing Biome errors. Alias the imported types to resolve the conflict.
-import type { signByPasskeyMutation } from "./__generated__/signByPasskeyMutation.graphql.ts"; +import type { + signByPasskeyMutation as SignByPasskeyMutation +} from "./__generated__/signByPasskeyMutation.graphql.ts";-import type { signGetPasskeyAuthenticationOptionsMutation } from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts"; +import type { + signGetPasskeyAuthenticationOptionsMutation as SignGetPasskeyAuthenticationOptionsMutation +} from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts";const [getPasskeyOptions] = createMutation< - signGetPasskeyAuthenticationOptionsMutation + SignGetPasskeyAuthenticationOptionsMutation >( signGetPasskeyAuthenticationOptionsMutation, ); const [loginByPasskey] = createMutation<signByPasskeyMutation>( +const [loginByPasskey] = createMutation<SignByPasskeyMutation>( signByPasskeyMutation, );Also applies to: 38-38, 140-147
web-next/src/locales/ko-KR/messages.po (1)
247-250
: Use "삭제" for revoke actions and fix punctuation/wording in Korean translationsThe current translations use "취소" for both destructive revoke actions and Cancel buttons, which creates ambiguity. Also, the punctuation uses full-width characters inconsistently.
msgid "ex) My key" -msgstr "예) 나의 키" +msgstr "예) 내 키" msgid "Last used:" -msgstr "마지막 사용:" +msgstr "마지막 사용:" msgid "Revoke" -msgstr "취소" +msgstr "삭제" msgid "Revoke passkey" -msgstr "패스키를 취소" +msgstr "패스키 삭제" msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "{0} 패스키를 취소하시겠습니까? 이 패스키를 사용하여 계정에 로그인할 수 없게 됩니다." +msgstr "패스키 {0}를 삭제하시겠습니까? 이 패스키로는 더 이상 로그인할 수 없습니다."Also applies to: 346-349, 539-546, 163-167
web-next/src/locales/en-US/messages.po (1)
247-250
: Polish English placeholder text and fix missing prepositionThe placeholder format "ex) My key" is non-idiomatic in English, and "Signing in Hackers' Pub" is missing the preposition "to".
Update the source strings in the TSX files and regenerate all
.po
files:In
src/routes/(root)/[handle]/settings/passkeys.tsx
:-placeholder={t`ex) My key`} +placeholder={t`e.g., My key`}In
src/routes/(root)/sign/index.tsx
:-{t`Signing in Hackers' Pub`} +{t`Signing in to Hackers' Pub`}Then regenerate all
.po
files to propagate the changes across all locales.Also applies to: 590-592
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (3)
1-5
: Add WebAuthn support checks before attempting registrationThe registration flow should check for WebAuthn support and secure context before attempting to register a passkey to provide better user feedback.
import { type PublicKeyCredentialCreationOptionsJSON, type RegistrationResponseJSON, startRegistration, + browserSupportsWebAuthn, } from "@simplewebauthn/browser";
async function onRegisterPasskey() { const account = data()?.accountByUsername; const name = passkeyName().trim(); if (!account || !name) return; + // Check for WebAuthn support + if (typeof window === "undefined" || !window.isSecureContext) { + showToast({ + title: t`Failed to register passkey`, + description: t`Passkeys require a secure (HTTPS) connection.`, + variant: "error", + }); + return; + } + + if (!browserSupportsWebAuthn()) { + showToast({ + title: t`Failed to register passkey`, + description: t`Your browser does not support passkeys.`, + variant: "error", + }); + return; + } setRegistering(true);Also applies to: 179-186
56-56
: Fix Biome noRedeclare error by aliasing imported GraphQL typesThe imported type shares the same identifier as the GraphQL document constant, causing a Biome error.
-import type { passkeysPageQuery } from "./__generated__/passkeysPageQuery.graphql.ts"; +import type { + passkeysPageQuery as PasskeysPageQuery +} from "./__generated__/passkeysPageQuery.graphql.ts";const loadPageQuery = query( (handle: string) => - loadQuery<passkeysPageQuery>( + loadQuery<PasskeysPageQuery>( useRelayEnvironment()(), passkeysPageQuery, { username: handle.replace(/^@/, "") }, ), "loadpasskeysPageQuery", );-const data = createPreloadedQuery<passkeysPageQuery>( +const data = createPreloadedQuery<PasskeysPageQuery>( passkeysPageQuery, () => loadPageQuery(params.handle), );Also applies to: 97-103, 136-139
141-146
: Prioritize implementing proper data refresh without page reloadThe current implementation uses
window.location.reload()
which provides a poor user experience. This should be addressed soon.The TODO comment indicates awareness, but this is a significant UX issue. Consider investigating:
- Using
refetch
functions provided by solid-relay query hooks- Implementing a manual cache invalidation pattern
- Using router navigation to trigger a re-render with fresh data
Would you like me to help research the proper solid-relay patterns for refreshing queries and open an issue to track this improvement?
graphql/passkey.ts (2)
78-95
: Significant code duplication in authentication and authorization logic.This mutation contains the same authentication, authorization, and account fetching pattern as the other mutations in this file, creating maintenance overhead and potential inconsistencies.
Extract the common logic into a helper function:
+async function getAuthorizedAccount(ctx: any, accountId: string) { + const session = await ctx.session; + if (session == null) throw new Error("Not authenticated."); + if (session.accountId !== accountId) { + throw new Error("Not authorized."); + } + const account = await ctx.db.query.accountTable.findFirst({ + where: { id: accountId }, + with: { passkeys: true }, + }); + if (account == null) throw new Error("Account not found."); + return account; +} getPasskeyRegistrationOptions: t.field({ type: "JSON", args: { accountId: t.arg.globalID({ for: Account, required: true }), }, async resolve(_, args, ctx) { - const session = await ctx.session; - if (session == null) throw new Error("Not authenticated."); - if (session.accountId !== args.accountId.id) { - throw new Error("Not authorized."); - } - const account = await ctx.db.query.accountTable.findFirst({ - where: { id: args.accountId.id }, - with: { passkeys: true }, - }); - if (account == null) throw new Error("Account not found."); + const account = await getAuthorizedAccount(ctx, args.accountId.id); const options = await getRegistrationOptions( ctx.kv, ctx.fedCtx.canonicalOrigin, account, ); return options; }, }),
104-124
: Significant code duplication in authentication and authorization logic.This mutation duplicates the same authentication, authorization, and account fetching logic as other mutations in this file.
Apply the same helper function approach as suggested for the previous mutation:
verifyPasskeyRegistration: t.field({ type: "JSON", args: { accountId: t.arg.globalID({ for: Account, required: true }), name: t.arg.string({ required: true }), registrationResponse: t.arg({ type: "JSON", required: true }), }, async resolve(_, args, ctx) { - const session = await ctx.session; - if (session == null) throw new Error("Not authenticated."); - if (session.accountId !== args.accountId.id) { - throw new Error("Not authorized."); - } - const account = await ctx.db.query.accountTable.findFirst({ - where: { id: args.accountId.id }, - with: { passkeys: true }, - }); - if (account == null) throw new Error("Account not found."); + const account = await getAuthorizedAccount(ctx, args.accountId.id); const result = await verifyRegistration( ctx.db, ctx.kv, ctx.fedCtx.canonicalOrigin, account, args.name, args.registrationResponse as RegistrationResponseJSON, ); return { verified: result.verified }; }, }),
🧹 Nitpick comments (16)
web-next/src/routes/(root)/sign/index.tsx (3)
2-6
: Consider adding WebAuthn support checks before auto-triggering passkey authenticationThe passkey authentication is triggered unconditionally on mount, which could result in unnecessary failures or browser prompts on unsupported browsers or insecure contexts.
import { type AuthenticationResponseJSON, type PublicKeyCredentialRequestOptionsJSON, startAuthentication, + browserSupportsWebAuthn, } from "@simplewebauthn/browser";
onMount(() => { // Automatically attempt passkey authentication when page loads if (!autoPasskeyAttempted()) { setAutoPasskeyAttempted(true); - onPasskeyLogin(false); + // Only attempt passkey login if the browser supports it and we're in a secure context + if (typeof window !== "undefined" && window.isSecureContext && browserSupportsWebAuthn()) { + onPasskeyLogin(false); + } } });Also applies to: 152-157
289-291
: Use generic types consistently for mutation promisesFor better type safety and consistency, use the explicit mutation type interface for the promise generic.
const optionsResponse = await new Promise< - signGetPasskeyAuthenticationOptionsMutation["response"] + SignGetPasskeyAuthenticationOptionsMutation["response"] >((resolve, reject) => {
317-319
: Use consistent generic type naming for mutation promisesUpdate this promise generic to use the aliased type name for consistency.
const loginResponse = await new Promise< - signByPasskeyMutation["response"] + SignByPasskeyMutation["response"] >((resolve, reject) => {web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (3)
188-190
: Use consistent type naming for mutation promisesUpdate the promise generic to use a consistent type interface pattern.
const optionsResponse = await new Promise< - passkeysGetPasskeyRegistrationOptionsMutation["response"] + PasskeysGetPasskeyRegistrationOptionsMutation["response"] >((resolve, reject) => {Also, alias the mutation type import at the top of the file:
-import type { passkeysGetPasskeyRegistrationOptionsMutation } from "./__generated__/passkeysGetPasskeyRegistrationOptionsMutation.graphql.ts"; +import type { + passkeysGetPasskeyRegistrationOptionsMutation as PasskeysGetPasskeyRegistrationOptionsMutation +} from "./__generated__/passkeysGetPasskeyRegistrationOptionsMutation.graphql.ts";
216-218
: Use consistent type naming for mutation promisesUpdate the promise generic for consistency.
const verifyResponse = await new Promise< - passkeysVerifyPasskeyRegistrationMutation["response"] + PasskeysVerifyPasskeyRegistrationMutation["response"] >((resolve, reject) => {Also alias the type import:
-import type { passkeysVerifyPasskeyRegistrationMutation } from "./__generated__/passkeysVerifyPasskeyRegistrationMutation.graphql.ts"; +import type { + passkeysVerifyPasskeyRegistrationMutation as PasskeysVerifyPasskeyRegistrationMutation +} from "./__generated__/passkeysVerifyPasskeyRegistrationMutation.graphql.ts";
270-272
: Use consistent type naming for mutation promisesUpdate the promise generic for consistency.
const response = await new Promise< - passkeysRevokePasskeyMutation["response"] + PasskeysRevokePasskeyMutation["response"] >((resolve, reject) => {Also alias the type import:
-import type { passkeysRevokePasskeyMutation } from "./__generated__/passkeysRevokePasskeyMutation.graphql.ts"; +import type { + passkeysRevokePasskeyMutation as PasskeysRevokePasskeyMutation +} from "./__generated__/passkeysRevokePasskeyMutation.graphql.ts";web-next/src/locales/ja-JP/messages.po (10)
578-581
: Unify “ログイン” vs “サインイン” for passkey flowMost UI strings use ログイン; only this one says サインイン. Aligning improves consistency and reduces cognitive load. This also matches Line 165 (revocation confirm) that already uses ログイン.
#: src/routes/(root)/sign/index.tsx:420 msgid "Sign in with passkey" -msgstr "パスキーでサインイン" +msgstr "パスキーでログイン"
195-198
: Standardize revocation and cancel terminology
- Prefer キャンセル over 取消 for the button label.
- Use 取り消し/取り消す rather than 取消 to match common JP UI wording and keep noun/verb forms consistent across buttons, titles, and messages.
# Cancel #: src/routes/(root)/[handle]/settings/passkeys.tsx:480 msgid "Cancel" -msgstr "取消" +msgstr "キャンセル" # Failed to revoke #: src/routes/(root)/[handle]/settings/passkeys.tsx:290 #: src/routes/(root)/[handle]/settings/passkeys.tsx:297 msgid "Failed to revoke passkey" -msgstr "パスキーの取消に失敗しました" +msgstr "パスキーの取り消しに失敗しました" # Error while revoking #: src/routes/(root)/[handle]/settings/passkeys.tsx:300 msgid "An error occurred while revoking your passkey." -msgstr "パスキーの取消中にエラーが発生しました。" +msgstr "パスキーの取り消し中にエラーが発生しました。" # Revoke (button) #: src/routes/(root)/[handle]/settings/passkeys.tsx:453 #: src/routes/(root)/[handle]/settings/passkeys.tsx:485 msgid "Revoke" -msgstr "取り消し" +msgstr "取り消す" # Dialog title #: src/routes/(root)/[handle]/settings/passkeys.tsx:474 msgid "Revoke passkey" -msgstr "パスキーを取り消し" +msgstr "パスキーを取り消す"Also applies to: 276-280, 151-154, 542-546, 547-550
182-185
: Use a single ellipsis glyph (…) instead of three dots ...Other strings already use … (U+2026). Align these two for visual and typographic consistency.
#: src/routes/(root)/sign/index.tsx:419 msgid "Authenticating..." -msgstr "認証中..." +msgstr "認証中…" #: src/routes/(root)/[handle]/settings/passkeys.tsx:392 msgid "Registering..." -msgstr "登録中..." +msgstr "登録中…"Also applies to: 538-541
206-209
: Polish labels: full-width colon and natural phrasing
- JP UI generally uses a full-width colon (:).
- “最終使用日” reads more naturally than “最終使用”.
#: src/routes/(root)/[handle]/settings/passkeys.tsx:423 msgid "Created:" -msgstr "作成日:" +msgstr "作成日:" #: src/routes/(root)/[handle]/settings/passkeys.tsx:432 msgid "Last used:" -msgstr "最終使用:" +msgstr "最終使用日:"Also applies to: 346-349
534-537
: Natural JP phrasing for list headingInsert の for smoother Japanese.
#: src/routes/(root)/[handle]/settings/passkeys.tsx:399 msgid "Registered passkeys" -msgstr "登録済みパスキー" +msgstr "登録済みのパスキー"
131-134
: Style consistency: use すでに instead of 既に, add はElsewhere you use hiragana “すでに” (e.g., Line 178). Adding は also sounds more natural.
#: src/routes/(root)/[handle]/settings/passkeys.tsx:381 msgid "A passkey with this name already exists" -msgstr "この名前のパスキーが既に存在します" +msgstr "この名前のパスキーはすでに存在します"
247-250
: Example marker: prefer “例)” in JP UI; keep example text as-isThis matches common JP documentation/UI conventions.
#: src/routes/(root)/[handle]/settings/passkeys.tsx:373 msgid "ex) My key" -msgstr "例) 私のキー" +msgstr "例)My key"
468-471
: Tone simplification for success messages
- “〜に成功しました” and “正常に〜しました” are fine but a bit stiff; shorter forms read better in UI toasts.
#: src/routes/(root)/[handle]/settings/passkeys.tsx:236 msgid "Passkey registered successfully" -msgstr "パスキーの登録に成功しました" +msgstr "パスキーを登録しました" #: src/routes/(root)/[handle]/settings/passkeys.tsx:283 msgid "The passkey has been successfully revoked." -msgstr "パスキーを正常に取り消しました。" +msgstr "パスキーを取り消しました。"Also applies to: 638-641
163-167
: Quote dynamic passkey name in the confirm dialogQuoting improves readability when the key name includes spaces or symbols. Also, minor copy tweak.
#: src/routes/(root)/[handle]/settings/passkeys.tsx:476 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "パスキー{0}を取り消しますか?このパスキーを使用してアカウントにログインできなくなります。" +msgstr "パスキー「{0}」を取り消しますか?このパスキーでは今後アカウントにログインできなくなります。"
1-15
: Please standardize terminology and punctuation in ja-JP localeTo keep translations cohesive and follow Japanese localization conventions, consider the following updates in
web-next/src/locales/ja-JP/messages.po
:
Replace “サインイン” with “ログイン”
• Line 580:- msgid "Sign in with passkey" - msgstr "パスキーでサインイン" + msgstr "パスキーでログイン"Use “キャンセル” for the “Cancel” button, and “取り消し…” for revocation messages
• Line 197:- msgid "Cancel" - msgstr "取消" + msgstr "キャンセル"• Line 153 (revoking):
- msgid "An error occurred while revoking your passkey." - msgstr "パスキーの取消中にエラーが発生しました。" + msgstr "パスキーの取り消し中にエラーが発生しました。"• Line 279 (failure):
- msgid "Failed to revoke passkey" - msgstr "パスキーの取消に失敗しました" + msgstr "パスキーの取り消しに失敗しました"Switch ASCII ellipsis
...
to Japanese ellipsis…
(U+2026)
• Line 184:- msgid "Authenticating..." - msgstr "認証中..." + msgstr "認証中…"• Line 540:
- msgid "Registering..." - msgstr "登録中..." + msgstr "登録中…"Use full-width colon
:
(U+FF1A) instead of ASCII:
• Line 208:- msgid "Created:" - msgstr "作成日:" + msgstr "作成日:"• Line 348:
- msgid "Last used:" - msgstr "最終使用:" + msgstr "最終使用:"These changes are optional but recommended for consistency across the ja-JP locale.
📜 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.
📒 Files selected for processing (12)
graphql/login.ts
(3 hunks)graphql/mod.ts
(1 hunks)graphql/passkey.ts
(1 hunks)graphql/schema.graphql
(6 hunks)web-next/src/components/SettingsTabs.tsx
(3 hunks)web-next/src/locales/en-US/messages.po
(18 hunks)web-next/src/locales/ja-JP/messages.po
(18 hunks)web-next/src/locales/ko-KR/messages.po
(18 hunks)web-next/src/locales/zh-CN/messages.po
(18 hunks)web-next/src/locales/zh-TW/messages.po
(18 hunks)web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
(1 hunks)web-next/src/routes/(root)/sign/index.tsx
(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- graphql/mod.ts
- graphql/login.ts
- web-next/src/components/SettingsTabs.tsx
- web-next/src/locales/zh-CN/messages.po
- graphql/schema.graphql
- web-next/src/locales/zh-TW/messages.po
🧰 Additional context used
🧬 Code graph analysis (3)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (4)
models/passkey.ts (1)
verifyRegistration
(47-85)web-next/src/components/ProfilePageBreadcrumb.tsx (1)
ProfilePageBreadcrumb
(16-49)web-next/src/components/SettingsTabs.tsx (1)
SettingsTabs
(16-58)web-next/src/components/ui/alert-dialog.tsx (8)
AlertDialog
(14-14)AlertDialogContent
(23-49)AlertDialogHeader
(51-63)AlertDialogTitle
(85-96)AlertDialogDescription
(104-117)AlertDialogFooter
(65-77)AlertDialogClose
(125-142)AlertDialogAction
(144-155)
graphql/passkey.ts (2)
models/schema.ts (1)
passkeyTable
(143-171)models/passkey.ts (2)
getRegistrationOptions
(28-45)verifyRegistration
(47-85)
web-next/src/routes/(root)/sign/index.tsx (2)
web-next/src/components/ui/toast.tsx (1)
showToast
(206-206)web-next/src/components/ui/button.tsx (1)
Button
(64-64)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
[error] 69-69: Shouldn't redeclare 'passkeysPageQuery'. Consider to delete it or rename it.
'passkeysPageQuery' is defined here:
(lint/suspicious/noRedeclare)
web-next/src/routes/(root)/sign/index.tsx
[error] 86-86: Shouldn't redeclare 'signGetPasskeyAuthenticationOptionsMutation'. Consider to delete it or rename it.
'signGetPasskeyAuthenticationOptionsMutation' is defined here:
(lint/suspicious/noRedeclare)
[error] 92-92: Shouldn't redeclare 'signByPasskeyMutation'. Consider to delete it or rename it.
'signByPasskeyMutation' is defined here:
(lint/suspicious/noRedeclare)
⏰ 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: image (ubuntu-latest)
- GitHub Check: image (ubuntu-24.04-arm)
🔇 Additional comments (5)
graphql/passkey.ts (4)
1-13
: LGTM!All imports are properly organized and include the necessary dependencies for passkey operations, GraphQL schema building, and database interactions.
15-25
: LGTM!The Passkey node definition correctly maps to the database table with appropriate field exposures. The use of
drizzleNode
provides proper ID handling and the exposed fields (name, lastUsed, created) align with the database schema.
27-70
: LGTM!The passkeys connection implementation is well-structured with proper cursor-based pagination, authorization scoping, and efficient database queries. The inverted mode handling and date-based cursor conversion are correctly implemented.
131-146
: LGTM!The
revokePasskey
mutation has correct authentication, authorization, and error handling logic. The approach of returningfalse
for non-existent passkeys and throwing an error for unauthorized access is appropriate.web-next/src/locales/ja-JP/messages.po (1)
522-541
: Overall passkeys localization looks comprehensiveNew strings cover all critical states (registering, listing, revoking) and align with the new routes and tabs. With the small consistency tweaks above, this will read very naturally to JP users.
<AlertDialogHeader> | ||
<AlertDialogTitle>{t`Revoke passkey`}</AlertDialogTitle> | ||
<AlertDialogDescription> | ||
{t`Are you sure you want to revoke passkey ${passkeyToRevoke()?.name}? You won't be able to use it to sign in to your account anymore.`} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix template literal syntax in i18n string
The i18n string uses incorrect template literal syntax with ${}
inside a tagged template literal.
-{t`Are you sure you want to revoke passkey ${passkeyToRevoke()?.name}? You won't be able to use it to sign in to your account anymore.`}
+{t`Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore.`(passkeyToRevoke()?.name)}
Based on the lingui macro patterns used elsewhere in the codebase, placeholders should use {0}
syntax and values passed as arguments to the tagged template function.
📝 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.
{t`Are you sure you want to revoke passkey ${passkeyToRevoke()?.name}? You won't be able to use it to sign in to your account anymore.`} | |
{t`Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore.`(passkeyToRevoke()?.name)} |
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/[handle]/settings/passkeys.tsx around line 476,
replace the incorrect template literal ${...} inside the tagged i18n template
with a placeholder and pass the value as an argument to the tag: change the
string to use {0} (e.g. t`Are you sure you want to revoke passkey {0}? You won't
be able to use it to sign in to your account anymore.`) and supply
passkeyToRevoke()?.name as the corresponding argument to the tagged template
call so the name is injected correctly for localization.
374a928
to
9f77b21
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
web-next/src/locales/zh-CN/messages.po (1)
448-451
: “Or” → “或” change looks good; matches zh-CN style.
🧹 Nitpick comments (9)
web-next/src/locales/ja-JP/messages.po (7)
578-581
: Use consistent “ログイン” (avoid mixing サインイン).Elsewhere in auth flows you use “ログイン.” Align “Sign in with passkey” accordingly.
msgid "Sign in with passkey" -msgstr "パスキーでサインイン" +msgstr "パスキーでログイン"
182-185
: Normalize punctuation: use Japanese ellipsis (…) and full-width colon(:).Small but visible polish for JP UI.
msgid "Authenticating..." -msgstr "認証中..." +msgstr "認証中…" @@ msgid "Registering..." -msgstr "登録中..." +msgstr "登録中…" @@ msgid "Created:" -msgstr "作成日:" +msgstr "作成日:" @@ msgid "Last used:" -msgstr "最終使用:" +msgstr "最終使用:"Also applies to: 538-541, 206-209, 346-349
195-198
: Button label: “取消” → “キャンセル”.JP UIs commonly label Cancel buttons as “キャンセル.”
msgid "Cancel" -msgstr "取消" +msgstr "キャンセル"
163-167
: Tighten revoke confirmation copy (quote key name + clarify effect).More natural and clearer to quote the passkey name and note the effect “以後”.
msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "パスキー{0}を取り消しますか?このパスキーを使用してアカウントにログインできなくなります。" +msgstr "パスキー「{0}」を取り消しますか?以後、このパスキーではアカウントにログインできなくなります。"
131-134
: Naturalness: add topic particle.msgid "A passkey with this name already exists" -msgstr "この名前のパスキーが既に存在します" +msgstr "この名前のパスキーは既に存在します"
247-250
: Example hint style: “ex)” → “例)”.Common JP notation uses “例)”. Keep sample text in English to match UI/placeholder expectations.
msgid "ex) My key" -msgstr "例) 私のキー" +msgstr "例)My key"
414-417
: Optional: shorter “未使用”.“使用履歴なし” is fine; “未使用” is shorter and common in lists.
msgid "Never used" -msgstr "使用履歴なし" +msgstr "未使用"web-next/src/locales/zh-CN/messages.po (2)
182-185
: Prefer “正在进行身份验证…” for clarity.“Authenticating...” reads better as “正在进行身份验证…”. Keeps wording consistent with other auth-related strings.
- msgstr "正在验证…" + msgstr "正在进行身份验证…"
526-533
: Please standardize pronouns in the “passkeys” section to formal “您/您的” for consistencyThe surrounding passkeys strings already use “您/您的” in error messages and confirmations, but these two user‐facing prompts still use informal “你/你的”:
- File:
web-next/src/locales/zh-CN/messages.po
Lines 526–533 (Register prompt)- File:
web-next/src/locales/zh-CN/messages.po
Lines 634–637 (Registered‐passkeys summary)Suggested diff:
--- a/web-next/src/locales/zh-CN/messages.po +++ b/web-next/src/locales/zh-CN/messages.po @@ -531,2 +531,2 @@ -msgstr "为你的账户注册通行密钥钥钥。你可以使用通行密钥钥钥登录,而不是通过电子邮件接收登录链接。" +msgstr "为您的账户注册通行密钥。您可以使用通行密钥登录,而不是通过电子邮件接收登录链接。" @@ -635,2 +635,2 @@ -msgstr "以下通行密钥钥钥已注册到你的账户。你可以使用它们登录你的账户。" +msgstr "以下通行密钥已注册到您的账户。您可以使用它们登录您的账户。"If you’d rather switch entirely to informal “你/你的” instead, you can adjust the other “您/您的” strings in this block accordingly. Let me know if you’d like a follow-up diff to flip them the other way!
📜 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.
📒 Files selected for processing (5)
web-next/src/locales/en-US/messages.po
(18 hunks)web-next/src/locales/ja-JP/messages.po
(18 hunks)web-next/src/locales/ko-KR/messages.po
(18 hunks)web-next/src/locales/zh-CN/messages.po
(18 hunks)web-next/src/locales/zh-TW/messages.po
(18 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- web-next/src/locales/ko-KR/messages.po
- web-next/src/locales/en-US/messages.po
- web-next/src/locales/zh-TW/messages.po
🔇 Additional comments (2)
web-next/src/locales/ja-JP/messages.po (1)
272-275
: LGTM for the rest of passkeys-related strings.Translations read naturally, placeholders are preserved, and tone matches the product.
Also applies to: 448-451, 460-463, 464-467, 468-471, 472-475, 476-480, 481-484, 522-525, 526-529, 530-533, 534-537, 561-567, 573-577, 590-593, 594-597, 602-605, 634-637, 638-641, 720-723, 749-752
web-next/src/locales/zh-CN/messages.po (1)
195-198
: LGTM: colon/wording are correct for “取消 / 创建于:/ 最后使用时间:”.Translations look good and use the fullwidth colon properly.
Also applies to: 206-209, 346-349
9f77b21
to
7970ccb
Compare
There was a problem hiding this 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 (12)
web-next/src/locales/ko-KR/messages.po (2)
247-250
: Tighten style: prefer “예) 내 키” for the example.msgid "ex) My key" -msgstr "예) 나의 키" +msgstr "예) 내 키"
147-154
: Use “삭제” for revoke actions to avoid the “취소” ambiguity (destructive vs. Cancel).“취소” is commonly read as “Cancel” in UIs; for destructive revoke flows, “삭제” (or “해지”) is clearer. Please update all revoke-related strings consistently.
Apply:
@@ msgid "An error occurred while revoking your passkey." -msgstr "패스키를 취소하는 중에 오류가 발생했습니다." +msgstr "패스키를 삭제하는 중에 오류가 발생했습니다." @@ msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "{0} 패스키를 취소하시겠습니까? 이 패스키를 사용하여 계정에 로그인할 수 없게 됩니다." +msgstr "패스키 {0}를 삭제하시겠습니까? 이 패스키로는 더 이상 로그인할 수 없습니다." @@ msgid "Failed to revoke passkey" -msgstr "패스키 취소 실패" +msgstr "패스키 삭제 실패" @@ msgid "Passkey revoked" -msgstr "패스키 취소됨" +msgstr "패스키 삭제됨" @@ msgid "Revoke" -msgstr "취소" +msgstr "삭제" @@ msgid "Revoke passkey" -msgstr "패스키를 취소" +msgstr "패스키 삭제" @@ msgid "The passkey has been successfully revoked." -msgstr "성공적으로 패스키를 취소했습니다." +msgstr "성공적으로 패스키를 삭제했습니다."Run to verify no revoke-side “취소” remains (except the Cancel button):
#!/bin/bash # Find Korean strings where passkey lines still use "취소" (destructive) rg -n '패스키.*취소' web-next/src/locales/ko-KR/messages.po -C2Also applies to: 163-167, 276-280, 472-475, 542-546, 547-550, 638-641
web-next/src/locales/zh-CN/messages.po (9)
182-185
: LGTM: Ellipsis and fullwidth colon are correct.“正在验证…” uses the ellipsis glyph, and “创建于:” uses the Chinese fullwidth colon consistently.
Also applies to: 195-198, 206-209
247-250
: Use “通行密钥” in example placeholder.- msgstr "例如:我的密钥" + msgstr "例如:我的通行密钥"
346-349
: LGTM: copy consistency fixes landed.
- “最后使用时间:” uses fullwidth colon.
- “从未使用过” reads well.
- “或” for “Or” is correct in this UI context.
Also applies to: 414-417, 448-451
131-134
: Unify “passkey” term to “通行密钥”.Use the standardized term to avoid ambiguity.
- msgstr "已存在一个名称相同的密钥" + msgstr "已存在同名的通行密钥"
147-154
: Fix passkey error messages: term + pronouns (“你/你的”).Align with the passkeys block tone and term.
- msgstr "在注册您的密钥时发生错误。" + msgstr "在注册你的通行密钥时发生错误。" - msgstr "在撤销您的密钥时发生错误。" + msgstr "在撤销你的通行密钥时发生错误。"
163-167
: Polish revoke-confirmation: add term, quote name, and unify pronouns.Improves clarity and matches adjacent strings.
- msgstr "您确定要撤销{0}密钥吗?您将无法再使用它登录您的账户。" + msgstr "你确定要撤销通行密钥“{0}”吗?你将无法再使用它登录你的账户。"
272-280
: Fix failure messages to use “通行密钥”.- msgstr "注册密钥失败" + msgstr "注册通行密钥失败" - msgstr "撤销密钥失败" + msgstr "撤销通行密钥失败"
460-463
: Unify: “通行密钥验证失败”.- msgstr "密钥验证失败" + msgstr "通行密钥验证失败"
547-550
: Finish remaining term/pronoun cleanups in passkeys block.- msgstr "撤销密钥" + msgstr "撤销通行密钥" - msgstr "使用密钥登录" + msgstr "使用通行密钥登录" - msgstr "密钥已成功撤销。" + msgstr "通行密钥已成功撤销。" - msgstr "您尚未注册任何密钥。" + msgstr "你尚未注册任何通行密钥。" - msgstr "您的密钥已注册,现在可以用于身份验证。" + msgstr "你的通行密钥已注册,现在可以用于身份验证。"Also applies to: 578-581, 638-641, 720-723, 749-752
web-next/src/locales/en-US/messages.po (1)
247-250
: Polish English copy: “e.g., My key” and “Signing in to Hackers’ Pub”.Please also consider updating the TSX source so msgid changes propagate via extraction.
- msgstr "ex) My key" + msgstr "e.g., My key" @@ - msgstr "Signing in Hackers' Pub" + msgstr "Signing in to Hackers' Pub"Follow-up:
- Update the literals in src/routes/(root)/[handle]/settings/passkeys.tsx and src/routes/(root)/sign/index.tsx, then regenerate catalogs with your Lingui workflow.
Also applies to: 590-593
🧹 Nitpick comments (7)
web-next/src/locales/ko-KR/messages.po (5)
182-185
: Normalize punctuation: use ASCII colons and the ellipsis character with proper spacing.
- “:” not “:”
- Use “…” (ellipsis) and add a space before “중”.
Apply:
@@ msgid "Authenticating..." -msgstr "인증중..." +msgstr "인증 중…" @@ msgid "Created:" -msgstr "생성일" +msgstr "생성일:" @@ msgid "Last used:" -msgstr "마지막 사용:" +msgstr "마지막 사용:" @@ msgid "Registering..." -msgstr "등록중..." +msgstr "등록 중…"Also applies to: 206-209, 346-349, 538-541
578-581
: Shorten “Sign in with passkey” to more natural Korean.msgid "Sign in with passkey" -msgstr "패스키를 사용하여 로그인" +msgstr "패스키로 로그인"
414-417
: Minor naturalness: “Never used” → “사용한 적 없음”.msgid "Never used" -msgstr "사용된 적 없음" +msgstr "사용한 적 없음"
749-752
: Fix spacing and streamline: “로그인 할” → “로그인할” and use “~로 로그인”.msgid "Your passkey has been registered and can now be used for authentication." -msgstr "패스키가 등록되었습니다. 이제 이 패스키를 사용하여 로그인 할 수 있습니다." +msgstr "패스키가 등록되었습니다. 이제 이 패스키로 로그인할 수 있습니다."
634-637
: Smoother second sentence for plural passkeys.msgid "The following passkeys are registered to your account. You can use them to sign in to your account." -msgstr "다음 패스키들이 계정에 등록되어 있습니다. 이들을 사용하여 계정에 로그인할 수 있습니다." +msgstr "다음 패스키들이 계정에 등록되어 있습니다. 이 중 하나로 로그인할 수 있습니다."web-next/src/locales/en-US/messages.po (2)
163-167
: Nit: quote the passkey name in the confirmation.Improves readability when names contain spaces or emoji.
- msgstr "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." + msgstr "Are you sure you want to revoke passkey \"{0}\"? You won't be able to use it to sign in to your account anymore."
182-185
: Unify ellipsis glyph to match the rest of the UI.Other strings (e.g., “Creating account…”, “Loading…”, “Saving…”) already use the ellipsis character.
- msgstr "Authenticating..." + msgstr "Authenticating…" @@ - msgstr "Registering..." + msgstr "Registering…"Also applies to: 538-541
📜 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.
📒 Files selected for processing (5)
web-next/src/locales/en-US/messages.po
(18 hunks)web-next/src/locales/ja-JP/messages.po
(18 hunks)web-next/src/locales/ko-KR/messages.po
(18 hunks)web-next/src/locales/zh-CN/messages.po
(18 hunks)web-next/src/locales/zh-TW/messages.po
(18 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- web-next/src/locales/zh-TW/messages.po
- web-next/src/locales/ja-JP/messages.po
⏰ 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: image (ubuntu-24.04-arm)
- GitHub Check: image (ubuntu-latest)
🔇 Additional comments (14)
web-next/src/locales/ko-KR/messages.po (5)
131-134
: LGTM: duplicate-passkey name error string reads well.
195-198
: LGTM: “Cancel” correctly remains “취소” (distinct from destructive “삭제”).
464-467
: LGTM: “Passkey name” is clear and idiomatic.
468-471
: LGTM: Success toast phrasing is consistent with existing terse style.
522-529
: LGTM: Registration-related labels are concise and consistent.Also applies to: 534-537
web-next/src/locales/zh-CN/messages.po (6)
239-242
: LGTM.Copy reads naturally; no action needed.
464-471
: LGTM: passkey nouns are consistent.“通行密钥名称/注册成功/已撤销/通行密钥” look good.
Also applies to: 472-475, 476-484
522-525
: LGTM: registration flow copy solid.“注册/注册通行密钥/为你的账户注册通行密钥…/已注册的通行密钥/正在注册…” all consistent with tone and term.
Also applies to: 526-533, 534-541
590-593
: LGTM for zh-CN.If en-US updates the source to “Signing in to Hackers' Pub”, this translation remains correct and does not need changes.
634-637
: LGTM.Clear and consistent.
131-134
: Manual Verification Required: Check for “密钥” vs “通行密钥” and duplicated “钥” typosThe automated scan did not report any matches, but please manually confirm there are no lingering inconsistencies in your translation file:
• In
web-next/src/locales/zh-CN/messages.po
, ensure every passkeys-relatedmsgstr
uses “通行密钥” (no bare “密钥”) for entries referenced bysettings/passkeys.tsx
.
• Verify there are no duplicated characters such as “通行密钥钥” or repeated “钥钥” anywhere in the file, particularly around these line ranges:
– 131–134
– 147–154
– 163–167
– 247–250
– 272–280
– 460–463
– 547–550
– 578–581
– 638–641
– 720–723
– 749–752web-next/src/locales/en-US/messages.po (3)
131-134
: LGTM: core passkey messages added in correct context.“A passkey with this name already exists” and error strings look good.
Also applies to: 147-154
522-533
: LGTM: registration/revocation UI copy.Clear, consistent, and aligned with product wording.
Also applies to: 542-550, 578-581
346-349
: LGTM.“Last used:/Never used/Or/Passkey name/Revocation success/Empty-state/Registration success” look good.
Also applies to: 414-417, 448-451, 460-467, 634-641, 720-723, 749-752
e7ee615
to
fed3924
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web-next/src/locales/ja-JP/messages.po (1)
682-685
: Fix typos: ユーサー → ユーザー.User-facing typo in two places.
@@ msgid "Username is already taken." -msgstr "他のユーサーが使用しているユーサー名です。" +msgstr "他のユーザーが使用しているユーザー名です。"
♻️ Duplicate comments (11)
web-next/src/locales/ko-KR/messages.po (3)
350-353
: Normalize label punctuation: use ASCII colon consistently.Korean UI typically uses the standard colon “:”. Please update “마지막 사용:” and add a colon to “생성일”.
@@ msgid "Last used:" -msgstr "마지막 사용:" +msgstr "마지막 사용:" @@ msgid "Created:" -msgstr "생성일" +msgstr "생성일:"Also applies to: 206-209
247-250
: Polish example phrasing.Shorten and fix punctuation in the example string.
@@ msgid "ex) My key" -msgstr "예) 나의 키" +msgstr "예) 내 키"
151-154
: Unify revoke terminology: replace “취소” with “삭제” and fix the confirm copy.Using “취소” for destructive revoke actions conflicts with the Cancel button and can confuse users. Prefer “삭제” for the destructive action across all passkey revoke messages, including the confirmation. This also aligns with prior feedback on this file.
Apply:
@@ msgid "An error occurred while revoking your passkey." -msgstr "패스키를 취소하는 중에 오류가 발생했습니다." +msgstr "패스키를 삭제하는 중에 오류가 발생했습니다." @@ msgid "Failed to revoke passkey" -msgstr "패스키 취소 실패" +msgstr "패스키 삭제 실패" @@ msgid "Passkey revoked" -msgstr "패스키 취소됨" +msgstr "패스키 삭제됨" @@ msgid "Revoke" -msgstr "취소" +msgstr "삭제" @@ msgid "Revoke passkey" -msgstr "패스키를 취소" +msgstr "패스키 삭제" @@ msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "{0} 패스키를 취소하시겠습니까? 이 패스키를 사용하여 계정에 로그인할 수 없게 됩니다." +msgstr "패스키 {0}를 삭제하시겠습니까? 이 패스키로는 더 이상 로그인할 수 없습니다."Also applies to: 280-284, 484-487, 554-558, 559-562, 163-167
web-next/src/locales/en-US/messages.po (2)
247-250
: “ex) My key” → “e.g., My key”.Non-idiomatic in en-US; use “e.g., My key”.
@@ msgid "ex) My key" -msgstr "ex) My key" +msgstr "e.g., My key"
602-605
: Grammar: add “to” in title.“Signing in Hackers’ Pub” → “Signing in to Hackers’ Pub”.
@@ msgid "Signing in Hackers' Pub" -msgstr "Signing in Hackers' Pub" +msgstr "Signing in to Hackers' Pub"web-next/src/locales/ja-JP/messages.po (1)
151-154
: Revoke phrasing: 取消 → 取り消し.Unify with natural Japanese: use 「取り消し」 for the noun phrase.
@@ msgid "An error occurred while revoking your passkey." -msgstr "パスキーの取消中にエラーが発生しました。" +msgstr "パスキーの取り消し中にエラーが発生しました。"web-next/src/routes/(root)/sign/index.tsx (1)
343-353
: Don’t toast on user-cancel; surface other errors.Differentiate NotAllowedError/AbortError (user cancelled) from real failures. Only toast for non-cancel errors.
@@ - } catch (_) { - if (showError) { + } catch (err) { + const name = + err && typeof err === "object" && "name" in err + ? // DOMException or Error + // @ts-expect-error safe duck typing + (err.name as string | undefined) + : undefined; + const isUserCancel = + name === "NotAllowedError" || name === "AbortError"; + if (showError && !isUserCancel) { showToast({ title: t`Passkey authentication failed`, variant: "destructive", }); }web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (4)
142-169
: Good use of Relay list directives to keep UI in sync without reloads.
@appendNode
on registration and@deleteEdge
on revocation are spot-on. This cleanly replaces the prior full-page reload UX.
1-5
: Guard registration behind secure-context and WebAuthn support checks.Preempt obvious failures with actionable UX messages before calling mutations or
startRegistration
. This avoids network calls and runtime errors on HTTP or unsupported browsers.Apply this diff:
@@ import { type PublicKeyCredentialCreationOptionsJSON, type RegistrationResponseJSON, startRegistration, + browserSupportsWebAuthn, } from "@simplewebauthn/browser"; @@ async function onRegisterPasskey() { const account = data()?.accountByUsername; const name = passkeyName().trim(); if (!account || !name) return; + + // WebAuthn prerequisites + if (!(typeof window !== "undefined" && window.isSecureContext)) { + showToast({ + title: t`Failed to register passkey`, + description: t`Passkeys require a secure (HTTPS) context.`, + variant: "error", + }); + return; + } + if (!browserSupportsWebAuthn()) { + showToast({ + title: t`Failed to register passkey`, + description: t`Your browser does not support passkeys.`, + variant: "error", + }); + return; + }Also applies to: 234-241
561-565
: Fix i18n interpolation syntax for Lingui tagged templates.
${...}
inside a tagged template won’t be extracted/translated. Use placeholders and pass values as arguments.Apply this diff:
- <AlertDialogDescription> - {t`Are you sure you want to revoke passkey ${passkeyToRevoke()?.name}? You won't be able to use it to sign in to your account anymore.`} - </AlertDialogDescription> + <AlertDialogDescription> + {t`Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore.`(passkeyToRevoke()?.name)} + </AlertDialogDescription>
58-58
: Action Required: Replace all unaliasedpasskeysPageQuery
references with the aliasedPasskeysPageQuery
Verification uncovered remaining occurrences of the old, unaliased type:
- Line 58: still imports the lowercase
passkeysPageQuery
- Line 124:
loadQuery<passkeysPageQuery>(…)
- Line 176:
createPreloadedQuery<passkeysPageQuery>(…)
Please update each to use the aliased
PasskeysPageQuery
. For example:--- a/web-next/src/routes/(root)/[handle]/settings/passkeys.tsx +++ b/web-next/src/routes/(root)/[handle]/settings/passkeys.tsx @@ -56,7 +56,7 @@ import type { passkeysGetPasskeyRegistrationOptionsMutation } from "./__generated__/passkeysGetPasskeyRegistrationOptionsMutation.graphql.ts"; -import type { passkeysPageQuery } from "./__generated__/passkeysPageQuery.graphql.ts"; +import type { PasskeysPageQuery } from "./__generated__/passkeysPageQuery.graphql.ts"; import type { passkeysRevokePasskeyMutation } from "./__generated__/passkeysRevokePasskeyMutation.graphql.ts"; import type { passkeysVerifyPasskeyRegistrationMutation } from "./__generated__/passkeysVerifyPasskeyRegistrationMutation.graphql.ts"; @@ -122,7 +122,7 @@ ) => loadQuery<passkeysPageQuery>( - useRelayEnvironment()(), - passkeysPageQuery, + useRelayEnvironment()(), + PasskeysPageQuery, { handle: params.handle }, { fetchPolicy: "store-or-network" } ); @@ -174,7 +174,7 @@ export default function SettingsPasskeys() { const { t } = useLingui(); - const data = createPreloadedQuery<passkeysPageQuery>( - passkeysPageQuery, + const data = createPreloadedQuery<PasskeysPageQuery>( + PasskeysPageQuery, () => loadPageQuery(params.handle) ); // …
🧹 Nitpick comments (15)
web-next/src/locales/ko-KR/messages.po (3)
182-185
: Add spacing in progressive ellipsis labels (“중…”).Korean spacing: use “인증 중…”, “등록 중…”.
@@ msgid "Authenticating…" -msgstr "인증중…" +msgstr "인증 중…" @@ msgid "Registering…" -msgstr "등록중…" +msgstr "등록 중…"Also applies to: 550-553
590-593
: Make “Sign in with passkey” more natural.“패스키를 사용하여 로그인” reads formal; “패스키로 로그인” is shorter and idiomatic.
@@ msgid "Sign in with passkey" -msgstr "패스키를 사용하여 로그인" +msgstr "패스키로 로그인"
761-764
: Fix spacing/wording in the success message.Remove the spacing error and streamline.
@@ msgid "Your passkey has been registered and can now be used for authentication." -msgstr "패스키가 등록되었습니다. 이제 이 패스키를 사용하여 로그인 할 수 있습니다." +msgstr "패스키가 등록되었습니다. 이제 이 패스키로 로그인할 수 있습니다."web-next/src/locales/ja-JP/messages.po (1)
590-593
: Optional: Align “Sign in with passkey” with other “ログイン” strings.Most strings use ログイン; consider “パスキーでログイン”.
@@ msgid "Sign in with passkey" -msgstr "パスキーでサインイン" +msgstr "パスキーでログイン"web-next/src/locales/zh-TW/messages.po (1)
542-545
: Polish register-help text to formal tone.Use 您/您的 consistently in settings.
@@ msgid "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." -msgstr "為你的帳戶註冊通行金鑰。你可以使用通行金鑰登入,而不是通過電子郵件接收登入連結。" +msgstr "為您的帳戶註冊通行金鑰。您可以使用通行金鑰登入,而非透過電子郵件接收登入連結。"web-next/src/routes/(root)/sign/index.tsx (4)
2-6
: Gate auto passkey attempt on support + secure context.Starting WebAuthn unconditionally can show prompts on unsupported/HTTP contexts. Check support and secure context before auto-attempt.
@@ -import { +import { type AuthenticationResponseJSON, type PublicKeyCredentialRequestOptionsJSON, startAuthentication, + browserSupportsWebAuthn, } from "@simplewebauthn/browser"; @@ onMount(() => { // Automatically attempt passkey authentication when page loads if (!autoPasskeyAttempted()) { setAutoPasskeyAttempted(true); - onPasskeyLogin(false); + if (typeof window !== "undefined" && window.isSecureContext && browserSupportsWebAuthn()) { + onPasskeyLogin(false); + } } });Also applies to: 152-158
363-363
: Grammar: “Signing in to Hackers’ Pub”.Update the title string; this also keeps en-US locale consistent.
- {t`Signing in Hackers' Pub`} + {t`Signing in to Hackers' Pub`}
100-111
: Cookie lifetime math nit: readability.Minor: 365 * 24 * 60 * 60 * 1000 improves readability (days → ms).
- expires: new Date(Date.now() + 365 * 60 * 60 * 24 * 1000), // 365 days + expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 365 days
281-314
: Optional: narrow error from startAuthentication.You already wrap and rethrow a generic Error. Consider preserving the original name to improve diagnostics in logs.
- } catch (error) { - throw new Error( - error instanceof Error ? error.message : "Authentication failed", - ); - } + } catch (error) { + const message = error instanceof Error ? error.message : "Authentication failed"; + const name = error instanceof Error && "name" in error ? (error as Error).name : "Error"; + const e = new Error(message); + // @ts-ignore annotate for logging + (e as any).name = name; + throw e; + }web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (6)
260-268
: Provide user-friendly handling for NotAllowedError (user cancel) from WebAuthn.Mapping common WebAuthn exceptions to clear messages improves UX and reduces “mystery” failures.
Apply this diff:
- } catch (error) { - throw new Error( - error instanceof Error ? error.message : "Registration failed", - ); - } + } catch (error) { + const message = + error instanceof Error && (error as any).name === "NotAllowedError" + ? t`Passkey registration was canceled.` + : error instanceof Error + ? error.message + : t`Registration failed`; + throw new Error(message as string); + }
215-219
: Duplicate-name check is only as complete as the edges loaded. Consider server-side validation too.Client-side checks help, but duplicates can still slip in if an older passkey with the same name exists on a later page. Back-end uniqueness per account (or a mutation-level validation) would close the gap.
Would you like a follow-up patch to add a uniqueness check in
verifyRegistration
and surface a specific GraphQL error for duplicate names?
221-232
: Minor UX: whenhasNextPage
is false, ensure the Load More button is not focusable.You already hide the container behind a
<Show>
whenhasNextPage
is false, which is good. If you ever flip to disabling instead of hiding, also setaria-disabled="true"
to aid screen readers.
171-194
: Mutation commit wrappers are fine; consider a small helper to reduce Promise boilerplate.You’re wrapping
createMutation
commits in Promises repeatedly. A tiny helper that promisifiescommit
would DRY things up and standardize error handling.I can sketch a small
commitMutation
helper if you’d like.
118-134
: Nit: consider naming the loaderloadPasskeysPageQuery
for clarity.Matches the tag
"loadpasskeysPageQuery"
and avoids confusion with other page loaders.
234-310
: Edge case: handle stale connection id safely.
passkeyData()!.passkeys.__id
assumes the fragment is present; extremely unlikely to be null here, but if the preloaded query ever fails, this would throw. A defensive fallback to omitconnections
when unavailable will make the mutation still succeed (store won’t auto-update, but UX remains functional).I can add a tiny helper to safely collect connection IDs if you’d like.
📜 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.
📒 Files selected for processing (7)
web-next/src/locales/en-US/messages.po
(20 hunks)web-next/src/locales/ja-JP/messages.po
(20 hunks)web-next/src/locales/ko-KR/messages.po
(20 hunks)web-next/src/locales/zh-CN/messages.po
(20 hunks)web-next/src/locales/zh-TW/messages.po
(20 hunks)web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
(1 hunks)web-next/src/routes/(root)/sign/index.tsx
(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- web-next/src/locales/zh-CN/messages.po
🧰 Additional context used
🧬 Code graph analysis (2)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (2)
models/passkey.ts (1)
verifyRegistration
(47-85)graphql/passkey.ts (4)
resolve
(51-86)resolve
(95-112)resolve
(121-155)resolve
(163-177)
web-next/src/routes/(root)/sign/index.tsx (2)
web-next/src/components/ui/toast.tsx (1)
showToast
(206-206)web-next/src/components/ui/button.tsx (1)
Button
(64-64)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
[error] 73-73: Shouldn't redeclare 'passkeysPageQuery'. Consider to delete it or rename it.
'passkeysPageQuery' is defined here:
(lint/suspicious/noRedeclare)
web-next/src/routes/(root)/sign/index.tsx
[error] 86-86: Shouldn't redeclare 'signGetPasskeyAuthenticationOptionsMutation'. Consider to delete it or rename it.
'signGetPasskeyAuthenticationOptionsMutation' is defined here:
(lint/suspicious/noRedeclare)
[error] 92-92: Shouldn't redeclare 'signByPasskeyMutation'. Consider to delete it or rename it.
'signByPasskeyMutation' is defined here:
(lint/suspicious/noRedeclare)
🔇 Additional comments (6)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (6)
205-209
: Pagination fragment wiring looks correct.
@refetchable
+@connection
withloadNext
provides scalable passkey lists. The state machine for loading/errored/loaded with retry text is a nice touch.
73-116
: Query/fragment shape matches server resolvers and pagination contract.The query requests
viewer
andaccountByUsername
with a paginatedpasskeys
connection, aligning with the server snippets provided (cursor = created timestamp). Looks consistent.
360-381
: Auth/ownership gating is correct.
- Unauthed → redirect to sign-in with
next
param.- Non-owner → navigate to
/
.Clean, and avoids leaking account existence via UI differences.
300-309
: Nice: consistent toasts and state reset.Good feedback on both success and failure, and clearing
registering
infinally
.
334-347
: Nice: connection-driven removal on revoke.The success path relies on
@deleteEdge
, keeping the UI instantaneous. Toasts cover both success and failure.
1-5
: Type-only imports look clean and keep bundles lean.Good use of
import type
with generated GraphQL types.
<AlertDialog | ||
open={passkeyToRevoke() != null} | ||
onOpenChange={() => setPasskeyToRevoke(null)} | ||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Make AlertDialog controlled-close only; avoid clearing state on open.
onOpenChange
fires for both open and close. Clearing the selection unconditionally on any change risks flicker or immediate re-close on some implementations. Only clear when closing.
Apply this diff:
- <AlertDialog
- open={passkeyToRevoke() != null}
- onOpenChange={() => setPasskeyToRevoke(null)}
- >
+ <AlertDialog
+ open={passkeyToRevoke() != null}
+ onOpenChange={(open) => {
+ if (!open) setPasskeyToRevoke(null);
+ }}
+ >
📝 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.
<AlertDialog | |
open={passkeyToRevoke() != null} | |
onOpenChange={() => setPasskeyToRevoke(null)} | |
> | |
<AlertDialog | |
open={passkeyToRevoke() != null} | |
onOpenChange={(open) => { | |
if (!open) setPasskeyToRevoke(null); | |
}} | |
> |
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/[handle]/settings/passkeys.tsx around lines
555-558, the AlertDialog currently clears passkeyToRevoke unconditionally in
onOpenChange which runs on both open and close; change the handler to accept the
open boolean and only call setPasskeyToRevoke(null) when open is false (i.e. on
close) so the dialog remains stable when opening and avoids flicker or immediate
re-close.
const signGetPasskeyAuthenticationOptionsMutation = graphql` | ||
mutation signGetPasskeyAuthenticationOptionsMutation($sessionId: UUID!) { | ||
getPasskeyAuthenticationOptions(sessionId: $sessionId) | ||
} | ||
`; | ||
|
||
const signByPasskeyMutation = graphql` | ||
mutation signByPasskeyMutation($sessionId: UUID!, $authenticationResponse: JSON!) { | ||
loginByPasskey(sessionId: $sessionId, authenticationResponse: $authenticationResponse) { | ||
id | ||
} | ||
} | ||
`; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix Biome noRedeclare: alias imported GraphQL types so they don’t collide with document consts.
Type identifiers shadow the GraphQL document constant names, triggering Biome’s noRedeclare. Alias the imported types and update generics.
@@
-import type { signByPasskeyMutation } from "./__generated__/signByPasskeyMutation.graphql.ts";
+import type {
+ signByPasskeyMutation as SignByPasskeyMutation
+} from "./__generated__/signByPasskeyMutation.graphql.ts";
@@
-import type { signGetPasskeyAuthenticationOptionsMutation } from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts";
+import type {
+ signGetPasskeyAuthenticationOptionsMutation as SignGetPasskeyAuthenticationOptionsMutation
+} from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts";
@@
- const [getPasskeyOptions] = createMutation<
- signGetPasskeyAuthenticationOptionsMutation
- >(
+ const [getPasskeyOptions] = createMutation<
+ SignGetPasskeyAuthenticationOptionsMutation
+ >(
signGetPasskeyAuthenticationOptionsMutation,
);
@@
- const [loginByPasskey] = createMutation<signByPasskeyMutation>(
+ const [loginByPasskey] = createMutation<SignByPasskeyMutation>(
signByPasskeyMutation,
);
Also applies to: 140-147
🧰 Tools
🪛 Biome (2.1.2)
[error] 86-86: Shouldn't redeclare 'signGetPasskeyAuthenticationOptionsMutation'. Consider to delete it or rename it.
'signGetPasskeyAuthenticationOptionsMutation' is defined here:
(lint/suspicious/noRedeclare)
[error] 92-92: Shouldn't redeclare 'signByPasskeyMutation'. Consider to delete it or rename it.
'signByPasskeyMutation' is defined here:
(lint/suspicious/noRedeclare)
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/sign/index.tsx around lines 86 to 99 (and
similarly for lines 140 to 147), the imported GraphQL type names collide with
the GraphQL document constant names causing Biome noRedeclare errors;
rename/alias the imported types (for example import {
signGetPasskeyAuthenticationOptionsMutation as SignGetPasskeyTypes } or similar)
so they no longer match the document constant identifiers, then update any
usages/generic type parameters to use the aliased type names to avoid shadowing
and satisfy the linter.
There was a problem hiding this 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 (1)
web-next/src/locales/zh-CN/messages.po (1)
1-778
: Translate “passkey” consistently as “通行密钥” and quote the revocation placeholderSeveral entries still use “密钥” without the “通行” qualifier, and the revocation confirmation placeholder isn’t wrapped in quotes. Please update the translations as follows:
• web-next/src/locales/zh-CN/messages.po (line 131)
msgid "A passkey with this name already exists"
msgstr “已存在一个名称相同的通行密钥”• (line 147)
msgid "An error occurred while registering your passkey."
msgstr “在注册您的通行密钥时发生错误。”• (line 151)
msgid "An error occurred while revoking your passkey."
msgstr “在撤销您的通行密钥时发生错误。”• (line 165–166)
msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore."
msgstr “您确定要撤销 “{0}” 通行密钥吗?您将无法再使用它登录您的账户。”• (line 277–278)
msgid "Failed to register passkey"
msgstr “注册通行密钥失败”• (line 283–284)
msgid "Failed to revoke passkey"
msgstr “撤销通行密钥失败”• (line 473–474)
msgid "Passkey authentication failed"
msgstr “通行密钥验证失败”• (line 734–735)
msgid "You don't have any passkeys registered yet."
msgstr “您尚未注册任何通行密钥。”• (line 763–764)
msgid "Your passkey has been registered and can now be used for authentication."
msgstr “您的通行密钥已注册,现在可以用于身份验证。”Add a CI guardrail to catch future inconsistencies:
#!/bin/bash set -euo pipefail FILE="web-next/src/locales/zh-CN/messages.po" # 1) All msgids with “passkey” must translate to include “通行密钥” rg -nPU '(?s)msgid\s+".*passkey[^"]*"\s*msgstr\s+"(?!.*通行密钥).*"' "$FILE" \ && { echo "⛔ Missing 通行密钥 in some translations"; exit 1; } \ || echo "✅ passkey translations all include 通行密钥" # 2) Revocation confirmation must wrap {0} in Chinese quotes rg -nPU 'msgid\s+"Are you sure you want to revoke passkey.*"\s*msgstr\s+".*“\{0\}”.*"' "$FILE" \ || echo "⚠️ Revocation placeholder {0} should be quoted as “{0}” and mention 通行密钥"
♻️ Duplicate comments (19)
web-next/src/locales/zh-CN/messages.po (11)
182-185
: LGTM: Ellipsis glyph fixed (“…”)“Authenticating…” and “Registering…” now use the single ellipsis glyph consistently. Nice.
Also applies to: 551-554
206-209
: LGTM: Fullwidth colon applied“创建于:/ 最后使用时间:” correctly use the Chinese fullwidth colon.
Also applies to: 351-354
461-464
: LGTM: “Or” → “或”This addresses the previous suggestion.
276-280
: Fix: “Failed to register passkey”Use the unified term.
-msgstr "注册密钥失败" +msgstr "注册通行密钥失败"
733-736
: Empty-state copy: unify term and pronoun-msgstr "您尚未注册任何密钥。" +msgstr "你尚未注册任何通行密钥。"
131-134
: Unify “passkey” term hereUse “通行密钥” and tighter Chinese phrasing.
-msgstr "已存在一个名称相同的密钥" +msgstr "已存在同名的通行密钥"
281-285
: Fix: “Failed to revoke passkey”Use the unified term.
-msgstr "撤销密钥失败" +msgstr "撤销通行密钥失败"
586-594
: Sign-in CTA: use “通行密钥”-msgstr "使用密钥登录" +msgstr "使用通行密钥登录"
163-167
: Revoke-confirmation copy: quote the placeholder and unify styleQuote the name, use “通行密钥”, and align pronouns.
-msgstr "您确定要撤销{0}密钥吗?您将无法再使用它登录您的账户。" +msgstr "你确定要撤销通行密钥“{0}”吗?你将无法再使用它登录你的账户。"
147-154
: Fix wording and pronouns; keep “通行密钥” consistentAlign to “你/你的” within the passkeys block and replace “密钥”.
-msgstr "在注册您的密钥时发生错误。" +msgstr "在注册你的通行密钥时发生错误。" -msgstr "在撤销您的密钥时发生错误。" +msgstr "在撤销你的通行密钥时发生错误。"
473-476
: Auth error message: use “通行密钥”-msgstr "密钥验证失败" +msgstr "通行密钥验证失败"web-next/src/locales/ja-JP/messages.po (2)
151-154
: Japanese revocation error: 取消 → 取り消しUse 「取り消し」 for the noun to match natural UI Japanese and stay consistent with the rest of the file.
msgid "An error occurred while revoking your passkey." -msgstr "パスキーの取消中にエラーが発生しました。" +msgstr "パスキーの取り消し中にエラーが発生しました。"
195-198
: “Cancel” should be 「キャンセル」Standard UI label in Japanese is 「キャンセル」, not 「取消」.
msgid "Cancel" -msgstr "取消" +msgstr "キャンセル"web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (4)
58-59
: Fix Biome noRedeclare: alias the imported GraphQL typeThe value-level constant
passkeysPageQuery
conflicts with the type import of the same name. Alias the type and update generics.-import type { passkeysPageQuery } from "./__generated__/passkeysPageQuery.graphql.ts"; +import type { + passkeysPageQuery as PasskeysPageQuery +} from "./__generated__/passkeysPageQuery.graphql.ts"; @@ - loadQuery<passkeysPageQuery>( + loadQuery<PasskeysPageQuery>( useRelayEnvironment()(), passkeysPageQuery, { username: handle.replace(/^@/, ""), first, after, }, ), @@ - const data = createPreloadedQuery<passkeysPageQuery>( + const data = createPreloadedQuery<PasskeysPageQuery>( passkeysPageQuery, () => loadPageQuery(params.handle), );Also applies to: 124-134, 176-179
1-5
: Guard WebAuthn registration behind secure-context/support checks; improve cancel UXAvoid avoidable runtime failures on HTTP and unsupported browsers. Provide user-friendly toasts and special-case “user cancel”.
import { type PublicKeyCredentialCreationOptionsJSON, type RegistrationResponseJSON, startRegistration, + browserSupportsWebAuthn, } from "@simplewebauthn/browser"; @@ async function onRegisterPasskey() { const account = data()?.accountByUsername; const name = passkeyNameRef?.value?.trim() ?? ""; if (!account || !name) return; + // Environment checks + if (!(typeof window !== "undefined" && window.isSecureContext)) { + showToast({ + title: t`Failed to register passkey`, + description: t`Passkeys require a secure (HTTPS) context.`, + variant: "error", + }); + return; + } + if (!browserSupportsWebAuthn()) { + showToast({ + title: t`Failed to register passkey`, + description: t`Your browser does not support passkeys.`, + variant: "error", + }); + return; + } + // Check for duplicate name if (checkDuplicateName(name)) { showToast({ title: t`Failed to register passkey`, description: t`A passkey with this name already exists`, variant: "error", }); return; } @@ // Use @simplewebauthn/browser to handle registration let registrationResponse: RegistrationResponseJSON; try { registrationResponse = await startRegistration({ optionsJSON: options as PublicKeyCredentialCreationOptionsJSON, }); } catch (error) { - throw new Error( - error instanceof Error ? error.message : "Registration failed", - ); + // Friendly message for user/cancel + if (error instanceof DOMException && error.name === "NotAllowedError") { + throw new Error(t`Registration canceled`); + } + throw new Error(error instanceof Error ? error.message : t`Registration failed`); }Also applies to: 234-248
552-555
: Dialog state: only clear selection on close
onOpenChange
fires on both open and close; clearing unconditionally can cause flicker.- <AlertDialog - open={passkeyToRevoke() != null} - onOpenChange={() => setPasskeyToRevoke(null)} - > + <AlertDialog + open={passkeyToRevoke() != null} + onOpenChange={(open) => { + if (!open) setPasskeyToRevoke(null); + }} + >
560-561
: Fix Lingui placeholder usage inside tagged template
${...}
insidet`` is not substituted; use
{0}` with argument list.- <AlertDialogDescription> - {t`Are you sure you want to revoke passkey ${passkeyToRevoke()?.name}? You won't be able to use it to sign in to your account anymore.`} - </AlertDialogDescription> + <AlertDialogDescription> + {t`Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore.`( + passkeyToRevoke()?.name, + )} + </AlertDialogDescription>web-next/src/locales/zh-TW/messages.po (2)
163-167
: Standardize terminology and formality; fix spacing and duplicated wordingUse 「通行金鑰」 consistently, polite 「您」 forms in settings, and add spaces around placeholders. Also fix duplicated “通行通行金鑰”.
msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "你確定要撤銷{0}通行金鑰嗎?你將無法再使用它登入你的帳戶。" +msgstr "您確定要撤銷 {0} 通行金鑰嗎?您將無法再使用它登入您的帳戶。" @@ msgid "The following passkeys are registered to your account. You can use them to sign in to your account." -msgstr "以下通行通行金鑰已註冊到你的帳戶。你可以使用它們登入你的帳戶。" +msgstr "以下通行金鑰已註冊到您的帳戶。您可以使用它們登入您的帳戶。" @@ msgid "The passkey has been successfully revoked." -msgstr "密碼已成功撤銷。" +msgstr "通行金鑰已成功撤銷。" @@ msgid "You don't have any passkeys registered yet." -msgstr "您尚未註冊任何密碼。" +msgstr "您尚未註冊任何通行金鑰。" @@ msgid "Your passkey has been registered and can now be used for authentication." -msgstr "您的密碼已經註冊,現在可以用於驗證。" +msgstr "您的通行金鑰已經註冊,現在可以用於驗證。"Also applies to: 647-650, 651-654, 733-736, 762-765
267-270
: Use 「通行金鑰」 and natural progress wordingFix remaining “密碼” mentions and progress ellipses.
msgid "Failed to load more passkeys; click to retry" -msgstr "載入更多密碼失敗;點擊重試" +msgstr "載入更多通行金鑰失敗,點擊重試" @@ msgid "Load more passkeys" -msgstr "載入更多密碼" +msgstr "載入更多通行金鑰" @@ msgid "Loading more passkeys…" -msgstr "正在載入更多密碼…" +msgstr "正在載入更多通行金鑰…" @@ msgid "Registering…" -msgstr "註冊…" +msgstr "註冊中…"Also applies to: 371-374, 396-399, 551-554
🧹 Nitpick comments (5)
web-next/src/locales/zh-CN/messages.po (2)
247-250
: Example placeholder: include “通行密钥”Keep the term consistent in the example.
-msgstr "例如:我的密钥" +msgstr "例如:我的通行密钥"
147-154
: Confirm pronoun style for the passkeys block (“你/你的” vs “您/您的”)The passkeys section currently mixes registers. Above diffs normalize to “你/你的” to match adjacent strings (e.g., lines 543-546, 647-650). If project-wide guidance prefers “您/您的”, I can flip these in one pass.
Would you like me to generate a follow-up patch to enforce this consistently across all passkey-related entries?
Also applies to: 163-167, 647-650, 733-736, 762-765
web-next/src/locales/ja-JP/messages.po (2)
371-374
: Natural phrasing for “Load more passkeys”Current translation sounds like “load passkeys.” Prefer “さらに読み込む”.
msgid "Load more passkeys" -msgstr "パスキーを読み込む" +msgstr "さらにパスキーを読み込む"
591-594
: Unify “Sign in” wording: サインイン → ログイン (consistent with rest of file)Elsewhere this locale uses 「ログイン」/「ログイン中…」. Consider aligning.
msgid "Sign in with passkey" -msgstr "パスキーでサインイン" +msgstr "パスキーでログイン"web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (1)
435-442
: Optional: disable Register button when name is emptyCurrently you early-return silently if the name is blank. Disabling with simple local state improves UX.
I can send a small patch wiring a name signal and using
disabled={registering() || name().trim().length === 0}
; let me know if you want it.
📜 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.
📒 Files selected for processing (6)
web-next/src/locales/en-US/messages.po
(20 hunks)web-next/src/locales/ja-JP/messages.po
(20 hunks)web-next/src/locales/ko-KR/messages.po
(20 hunks)web-next/src/locales/zh-CN/messages.po
(20 hunks)web-next/src/locales/zh-TW/messages.po
(20 hunks)web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- web-next/src/locales/en-US/messages.po
- web-next/src/locales/ko-KR/messages.po
🧰 Additional context used
🧬 Code graph analysis (1)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (5)
models/passkey.ts (1)
verifyRegistration
(47-85)web-next/src/components/Title.tsx (1)
Title
(7-14)web-next/src/components/ProfilePageBreadcrumb.tsx (1)
ProfilePageBreadcrumb
(16-49)web-next/src/components/SettingsTabs.tsx (1)
SettingsTabs
(16-58)web-next/src/components/ui/alert-dialog.tsx (5)
AlertDialog
(14-14)AlertDialogContent
(23-49)AlertDialogHeader
(51-63)AlertDialogClose
(125-142)AlertDialogAction
(144-155)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
[error] 73-73: Shouldn't redeclare 'passkeysPageQuery'. Consider to delete it or rename it.
'passkeysPageQuery' is defined here:
(lint/suspicious/noRedeclare)
⏰ 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: image (ubuntu-24.04-arm)
- GitHub Check: image (ubuntu-latest)
🔇 Additional comments (3)
web-next/src/locales/zh-CN/messages.po (2)
477-484
: LGTM: Core passkey nouns and headings are consistent“通行密钥名称/注册成功/已撤销/passkeys/Passkeys/撤销” look good.
Also applies to: 489-497, 535-538, 539-546, 547-550, 555-559
647-650
: LGTM: Passkeys list description reads naturallyConsistent term and pronouns within the passkeys block.
web-next/src/locales/zh-TW/messages.po (1)
535-538
: Translations read well; passkey lifecycle strings LGTMThe new/relocated terms for register/revoke/authenticate are clear and consistent after the above fixes.
Also applies to: 539-542, 473-476, 477-480, 481-484, 555-559, 560-563
c662150
to
297fb30
Compare
There was a problem hiding this 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 (13)
web-next/src/locales/en-US/messages.po (2)
264-267
: Use idiomatic example phrasing: “e.g., My key”.Replace the placeholder example with standard en-US phrasing.
- msgstr "ex) My key" + msgstr "e.g., My key"
624-627
: Fix preposition: “Signing in to Hackers' Pub”.Update the en-US translation to include “to.” For long-term consistency, prefer updating the TSX source literal and re-extracting, but this
msgstr
tweak is fine for now.- msgstr "Signing in Hackers' Pub" + msgstr "Signing in to Hackers' Pub"web-next/src/routes/(root)/sign/index.tsx (2)
2-6
: Gate auto WebAuthn attempt behind capability and secure-context checks.Avoid prompting on unsupported/HTTP contexts and only mark “attempted” when actually attempting.
@@ -import { +import { type AuthenticationResponseJSON, type PublicKeyCredentialRequestOptionsJSON, startAuthentication, + browserSupportsWebAuthn, } from "@simplewebauthn/browser"; @@ -onMount(() => { - // Automatically attempt passkey authentication when page loads - if (!autoPasskeyAttempted()) { - setAutoPasskeyAttempted(true); - onPasskeyLogin(false); - } -}); +onMount(() => { + if (autoPasskeyAttempted()) return; + // Attempt only on capable, secure browsers + if ( + typeof window !== "undefined" && + window.isSecureContext && + browserSupportsWebAuthn() + ) { + setAutoPasskeyAttempted(true); + onPasskeyLogin(false); + } +});Also applies to: 152-158
32-39
: Fix Biome noRedeclare by aliasing imported GraphQL types.The generated GraphQL type identifiers collide with the document consts of the same name, tripping Biome’s noRedeclare. Alias the type imports and update generics accordingly.
@@ -import type { signByPasskeyMutation } from "./__generated__/signByPasskeyMutation.graphql.ts"; +import type { + signByPasskeyMutation as SignByPasskeyMutation +} from "./__generated__/signByPasskeyMutation.graphql.ts"; @@ -import type { signGetPasskeyAuthenticationOptionsMutation } from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts"; +import type { + signGetPasskeyAuthenticationOptionsMutation as SignGetPasskeyAuthenticationOptionsMutation +} from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts"; @@ - const [getPasskeyOptions] = createMutation< - signGetPasskeyAuthenticationOptionsMutation - >( + const [getPasskeyOptions] = createMutation< + SignGetPasskeyAuthenticationOptionsMutation + >( signGetPasskeyAuthenticationOptionsMutation, ); @@ - const [loginByPasskey] = createMutation<signByPasskeyMutation>( + const [loginByPasskey] = createMutation<SignByPasskeyMutation>( signByPasskeyMutation, );Also applies to: 86-99, 140-147
web-next/src/locales/zh-CN/messages.po (1)
147-154
: Unify passkey pronouns to “你/你的” and fix quote/word order in revoke confirmation.Other passkey strings in this file use “你/你的”. Align these and format the placeholder with Chinese quotes for clarity.
@@ -msgstr "在注册您的通行密钥时发生错误。" +msgstr "在注册你的通行密钥时发生错误。" @@ -msgstr "在撤销您的通行密钥时发生错误。" +msgstr "在撤销你的通行密钥时发生错误。" @@ -msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "您确定要撤销{0}通行密钥吗?您将无法再使用它登录您的账户。" +msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." +msgstr "你确定要撤销通行密钥“{0}”吗?你将无法再使用它登录你的账户。" @@ -msgstr "您尚未注册任何通行密钥。" +msgstr "你尚未注册任何通行密钥。" @@ -msgstr "您的通行密钥已注册,现在可以用于身份验证。" +msgstr "你的通行密钥已注册,现在可以用于身份验证。"Also applies to: 163-167, 754-757, 787-790
web-next/src/locales/ko-KR/messages.po (2)
368-371
: Colon: use ASCII “:” not fullwidth “:”.Punctuation normalization.
-msgid "Last used:" -msgstr "마지막 사용:" +msgid "Last used:" +msgstr "마지막 사용:"
576-580
: Use “삭제” (not “취소”) for revoke actions to avoid confusion with the Cancel button; adjust confirm text.“취소” reads as “Cancel” and conflicts with the Cancel button. “삭제” or “해지” is the standard destructive label. Also tweak the confirmation for natural Korean.
@@ -msgid "Revoke" -msgstr "취소" +msgid "Revoke" +msgstr "삭제" @@ -msgid "Revoke passkey" -msgstr "패스키를 취소" +msgid "Revoke passkey" +msgstr "패스키 삭제" @@ -msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "{0} 패스키를 취소하시겠습니까? 이 패스키를 사용하여 계정에 로그인할 수 없게 됩니다." +msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." +msgstr "패스키 {0}를 삭제하시겠습니까? 이 패스키로는 더 이상 로그인할 수 없습니다."Also applies to: 581-584, 163-167
web-next/src/locales/ja-JP/messages.po (1)
151-154
: “取消” → “取り消し” (grammar and consistency).Use 「取り消し」 for the noun form in messages.
-msgid "An error occurred while revoking your passkey." -msgstr "パスキーの取消中にエラーが発生しました。" +msgid "An error occurred while revoking your passkey." +msgstr "パスキーの取り消し中にエラーが発生しました。"web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (5)
552-555
: AlertDialog: only clear state on close to avoid flicker/immediate re-close.
onOpenChange
fires on both open and close; clearing unconditionally can fight the dialog.- <AlertDialog - open={passkeyToRevoke() != null} - onOpenChange={() => setPasskeyToRevoke(null)} - > + <AlertDialog + open={passkeyToRevoke() != null} + onOpenChange={(open) => { + if (!open) setPasskeyToRevoke(null); + }} + >
58-60
: Fix Biome “noRedeclare”: alias the imported GraphQL type and update generics.Type
passkeysPageQuery
collides with the document constant of the same name. Alias the type and update generics to eliminate the lint error.-import type { passkeysPageQuery } from "./__generated__/passkeysPageQuery.graphql.ts"; +import type { + passkeysPageQuery as PasskeysPageQuery +} from "./__generated__/passkeysPageQuery.graphql.ts"; @@ - loadQuery<passkeysPageQuery>( + loadQuery<PasskeysPageQuery>( useRelayEnvironment()(), passkeysPageQuery, { username: handle.replace(/^@/, ""), first, after, }, ), @@ - const data = createPreloadedQuery<passkeysPageQuery>( + const data = createPreloadedQuery<PasskeysPageQuery>( passkeysPageQuery, () => loadPageQuery(params.handle), );Also applies to: 118-134, 176-179
234-320
: Guard WebAuthn calls behind secure-context and support checks; improve UX for common errors.
startRegistration
requires HTTPS and WebAuthn support. Add guards to avoid unnecessary network calls and provide actionable messages. Also map common WebAuthn errors (e.g., NotAllowedError) to friendly text.-import { - type PublicKeyCredentialCreationOptionsJSON, - type RegistrationResponseJSON, - startRegistration, -} from "@simplewebauthn/browser"; +import { + type PublicKeyCredentialCreationOptionsJSON, + type RegistrationResponseJSON, + startRegistration, + browserSupportsWebAuthn, +} from "@simplewebauthn/browser"; @@ async function onRegisterPasskey() { const account = data()?.accountByUsername; const name = passkeyNameRef?.value?.trim() ?? ""; if (!account || !name) return; + // Environment checks + if (!(typeof window !== "undefined" && window.isSecureContext)) { + showToast({ + title: t`Failed to register passkey`, + description: t`Passkeys require a secure (HTTPS) context.`, + variant: "error", + }); + return; + } + if (!browserSupportsWebAuthn()) { + showToast({ + title: t`Failed to register passkey`, + description: t`Your browser does not support passkeys.`, + variant: "error", + }); + return; + } @@ - } catch (error) { - throw new Error( - error instanceof Error ? error.message : "Registration failed", - ); - } + } catch (error) { + // Provide friendlier messages for common cases + const message = + error && typeof error === "object" && "name" in (error as any) + ? ((error as any).name === "NotAllowedError" + ? t`Registration was cancelled or timed out.` + : (error as Error).message) + : t`Registration failed`; + throw new Error(message); + } @@ - connections: [passkeyData()!.passkeys.__id], + connections: [passkeyData()?.passkeys.__id ?? ""],Optionally, guard the empty connection id:
const connectionId = passkeyData()?.passkeys.__id; if (!connectionId) { showToast({ title: t`Failed to update list`, variant: "error" }); return; }
210-219
: Client-only duplicate-name check is insufficient; enforce at DB level (case-insensitive).The local check only covers the loaded page and races under concurrency. Add a unique index on
(accountId, LOWER(name))
and align server validation.
- Drizzle schema:
unique() .on(passkeyTable.accountId, sql`LOWER(${passkeyTable.name})`) .name("passkey_account_id_name_lower_unique");
- SQL migration:
CREATE UNIQUE INDEX IF NOT EXISTS passkey_account_id_name_lower_unique ON passkey(account_id, LOWER(name));
- On conflict, map to a user-friendly error message returned by
verifyPasskeyRegistration
.
560-561
: Fix Lingui template interpolation; avoid${}
inside tagged template.Use
{0}
with an argument for correct extraction/localization.- {t`Are you sure you want to revoke passkey ${passkeyToRevoke()?.name}? You won't be able to use it to sign in to your account anymore.`} + {t`Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore.`(passkeyToRevoke()?.name)}
🧹 Nitpick comments (15)
web-next/src/locales/en-US/messages.po (5)
131-134
: Consider showing the conflicting passkey name in the error.Current copy is clear but could be more actionable if it included the name, e.g., “A passkey named “{name}” already exists.” This requires passing the value from UI and adding a placeholder to the message.
If you choose to implement, update the TSX to pass a variable:
// in src/routes/(root)/[handle]/settings/passkeys.tsx i18n._(/*i18n*/ t({ id: 'passkey-name-duplicate', message: 'A passkey named “{name}” already exists', }), { name })Then re-extract translations so the
.po
includes a{name}
placeholder.
506-510
: Verify casing consistency: “passkeys” vs “Passkeys”.This file has both lowercase “passkeys” and title-case “Passkeys.” If the lowercase string is used as a heading/breadcrumb/tab label, consider title case for consistency with other settings tabs.
Potential en-US tweak if it’s UI chrome (do not apply if it’s a sentence fragment):
- msgstr "passkeys" + msgstr "Passkeys"Also applies to: 511-514
668-671
: Tighten microcopy to avoid repetition.Drop the second “to your account” for smoother reading.
- msgstr "The following passkeys are registered to your account. You can use them to sign in to your account." + msgstr "The following passkeys are registered to your account. You can use them to sign in."
564-567
: Prefer “via email” over “by email”.Minor phrasing polish for en-US.
- msgstr "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link by email." + msgstr "Register a passkey to sign in to your account. You can use a passkey instead of receiving a sign-in link via email."
264-267
: Ensure consistent example markers and UI copy across locales and sourceA repo-wide sweep shows these literals still need updating before re-extracting translations:
“ex) My key”
- Appears in every locale’s messages.po (e.g. en-US at src/locales/en-US/messages.po:265–266) and as the placeholder in the Passkeys settings page (src/routes/(root)/[handle]/settings/passkeys.tsx:430).
- Action: Replace with the agreed example marker (e.g.
e.g. My key
).“Signing in Hackers’ Pub”
- Present in all locales’ messages.po (e.g. en-US at src/locales/en-US/messages.po:625–626) and in the sign-in route (src/routes/(root)/sign/index.tsx:363).
- Action: Update or remove this legacy copy as appropriate.
Lowercase “passkeys” in UI chrome
- Found in messages.po (e.g. src/locales/en-US/messages.po:508–509) and in components:
•<Title>{t
passkeys}</Title>
(passkeys.tsx:395)
• SettingsTabs value and href (SettingsTabs.tsx:48–49
).- Action: Standardize to title-case
Passkeys
in both source and locale files.After adjusting these literals in your source code, re-run your i18n tooling to propagate the changes through all locale files.
web-next/src/routes/(root)/sign/index.tsx (1)
281-353
: Handle user-cancelled WebAuthn gracefully; don’t toast on NotAllowedError.If the user cancels the WebAuthn prompt, silently ignore (even on manual trigger). Only toast for real failures.
@@ - } catch (_) { - if (showError) { - showToast({ - title: t`Passkey authentication failed`, - variant: "destructive", - }); - } - } finally { + } catch (error) { + const name = (error as any)?.name ?? ""; + const message = (error as Error)?.message ?? ""; + // Ignore common user-cancel/timeouts without noisy UX + if ( + name === "NotAllowedError" || + name === "AbortError" || + /NotAllowedError|timed out|user.?cancel/i.test(message) + ) { + return; + } + if (showError) { + showToast({ + title: t`Passkey authentication failed`, + variant: "destructive", + }); + } + } finally { setPasskeyAuthenticating(false); }web-next/src/locales/zh-CN/messages.po (1)
131-134
: Style: prefer “同名的通行密钥”.More natural phrasing for existence checks.
-msgstr "已存在一个名称相同的通行密钥" +msgstr "已存在同名的通行密钥"web-next/src/locales/ko-KR/messages.po (1)
264-267
: Minor Korean UX/polish: example string, spacing around “중…”, and more natural phrasing.
- Prefer “내” over “나의” in short UI.
- Add a space before “중…” in progress labels.
- “패스키로 로그인” is more concise.
@@ -msgid "ex) My key" -msgstr "예) 나의 키" +msgid "ex) My key" +msgstr "예) 내 키" @@ -msgid "Registering…" -msgstr "등록중…" +msgid "Registering…" +"등록 중…" @@ -msgid "Sign in with passkey" -msgstr "패스키를 사용하여 로그인" +msgid "Sign in with passkey" +"패스키로 로그인" @@ -msgid "Your passkey has been registered and can now be used for authentication." -msgstr "패스키가 등록되었습니다. 이제 이 패스키를 사용하여 로그인 할 수 있습니다." +msgid "Your passkey has been registered and can now be used for authentication." +"패스키가 등록되었습니다. 이제 이 패스키로 로그인할 수 있습니다." @@ -msgid "Created:" -msgstr "생성일" +msgid "Created:" +"생성일:"Also applies to: 556-575, 612-615, 787-790, 211-214
web-next/src/locales/ja-JP/messages.po (2)
388-391
: Unify “Load more …” phrasing.Other entries use “もっと読み込む”. Align passkeys.
-msgid "Load more passkeys" -msgstr "パスキーを読み込む" +msgid "Load more passkeys" +"パスキーをもっと読み込む"
163-167
: Optional: add Japanese quotes around the name placeholder.Improves readability in confirmation dialogs.
-msgstr "パスキー{0}を取り消しますか?このパスキーを使用してアカウントにログインできなくなります。" +msgstr "パスキー「{0}」を取り消しますか?このパスキーを使用してアカウントにログインできなくなります。"graphql/schema.graphql (2)
99-108
: Connection types are correctly defined; considertotalCount
only if needed.
AccountPasskeysConnection
and its edge look correct. If UX requires showing total number of passkeys regardless of pagination, consider addingtotalCount: Int!
for parity with other connections like followers/followees. Otherwise, current minimal shape is fine.
555-566
: Passkey and PasskeyRegistrationResult types are sufficient; consider future-proofing.The fields cover current needs. If you later expose authenticator metadata (platform vs cross-platform, transports), consider extending
Passkey
with readonly metadata fields.web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (3)
326-368
: Add a “revoking” state to prevent duplicate submissions and reflect progress.Currently, revocation can be triggered multiple times. Track a
revoking
signal, disable the confirm button, and optionally the per-item “Revoke” buttons while in-flight.@@ - async function confirmRevokePasskey() { + const [revoking, setRevoking] = createSignal(false); + async function confirmRevokePasskey() { const passkey = passkeyToRevoke(); if (!passkey) return; try { + setRevoking(true); const response = await new Promise< passkeysRevokePasskeyMutation["response"] >((resolve, reject) => { @@ } } catch (error) { @@ } finally { setPasskeyToRevoke(null); + setRevoking(false); } } @@ - <AlertDialogAction + <AlertDialogAction class="bg-destructive text-destructive-foreground hover:bg-destructive/70" - onClick={confirmRevokePasskey} + disabled={revoking()} + onClick={confirmRevokePasskey} > {t`Revoke`} </AlertDialogAction>Also applies to: 552-573
221-232
: Pagination loadMore: OK; consider exposing errors to the user.You already switch to an “errored” state and reuse the same button to retry—nice. Optionally add a toast for persistent failures.
371-391
: Auth gate logic: Navigate on mismatch is fine; consider early-return for clarity.Rendering
<Navigate />
inside aShow
works, but an early-return pattern can reduce rendering of the rest of the tree before navigation occurs. 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.
📒 Files selected for processing (13)
graphql/login.ts
(3 hunks)graphql/mod.ts
(1 hunks)graphql/passkey.ts
(1 hunks)graphql/schema.graphql
(6 hunks)web-next/src/components/SettingsTabs.tsx
(3 hunks)web-next/src/components/ui/alert-dialog.tsx
(1 hunks)web-next/src/locales/en-US/messages.po
(22 hunks)web-next/src/locales/ja-JP/messages.po
(22 hunks)web-next/src/locales/ko-KR/messages.po
(22 hunks)web-next/src/locales/zh-CN/messages.po
(22 hunks)web-next/src/locales/zh-TW/messages.po
(22 hunks)web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
(1 hunks)web-next/src/routes/(root)/sign/index.tsx
(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- graphql/mod.ts
- web-next/src/components/ui/alert-dialog.tsx
- graphql/login.ts
- web-next/src/locales/zh-TW/messages.po
- graphql/passkey.ts
🧰 Additional context used
🧬 Code graph analysis (1)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (2)
models/passkey.ts (1)
verifyRegistration
(47-85)graphql/passkey.ts (4)
resolve
(51-86)resolve
(95-112)resolve
(121-155)resolve
(163-177)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
[error] 73-73: Shouldn't redeclare 'passkeysPageQuery'. Consider to delete it or rename it.
'passkeysPageQuery' is defined here:
(lint/suspicious/noRedeclare)
web-next/src/routes/(root)/sign/index.tsx
[error] 86-86: Shouldn't redeclare 'signGetPasskeyAuthenticationOptionsMutation'. Consider to delete it or rename it.
'signGetPasskeyAuthenticationOptionsMutation' is defined here:
(lint/suspicious/noRedeclare)
[error] 92-92: Shouldn't redeclare 'signByPasskeyMutation'. Consider to delete it or rename it.
'signByPasskeyMutation' is defined here:
(lint/suspicious/noRedeclare)
⏰ 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: image (ubuntu-latest)
- GitHub Check: image (ubuntu-24.04-arm)
🔇 Additional comments (9)
web-next/src/locales/en-US/messages.po (1)
147-154
: Passkey i18n coverage looks solid.Strings cover the end-to-end passkey flows (register, list, revoke, auth) with clear user-facing copy and consistent punctuation/ellipsis usage. No issues from a translation/catalog standpoint.
Also applies to: 163-167, 182-185, 211-214, 284-287, 293-297, 298-302, 368-371, 388-391, 413-416, 444-447, 490-505, 511-514, 556-575, 576-584, 607-615, 668-675, 754-757, 787-790
web-next/src/routes/(root)/sign/index.tsx (1)
401-422
: Passkey UI on sign-in page looks good.The divider, disabled state, and status text are clear and accessible. Once the guards above are added, UX should be solid.
web-next/src/components/SettingsTabs.tsx (1)
9-9
: New “Passkeys” tab wiring looks correct.
- Type union extended and used consistently.
- grid-cols-3 matches the three triggers.
- Href path to /@{username}/settings/passkeys aligns with the new route.
Also applies to: 31-31, 46-52
web-next/src/locales/zh-CN/messages.po (1)
413-416
: Terminology and punctuation look consistent for passkeys (“通行密钥”、全角冒号、单一省略号).Nice cleanup; no further issues spotted in these ranges.
Also applies to: 388-391, 284-287
graphql/schema.graphql (5)
18-24
: Account.passkeys connection: alignment looks good; confirm Relay edge type naming.The
Account.passkeys
field and corresponding connection types are well-structured and consistent with other connections. Ensure client updaters use the exact edge type nameAccountPasskeysConnectionEdge
(see my note in the TSX file about@appendNode
).Would you confirm that all client
@appendNode
usages referenceedgeTypeName: "AccountPasskeysConnectionEdge"
?
439-444
: getPasskeyRegistrationOptions: OK; keep strong server-side authorization.Returning
JSON!
is acceptable here given WebAuthn options variability. The server-side checks (session + account ownership) shown in graphql/passkey.ts align with this schema and are essential—keep them.
456-463
: loginByPasskey return type is appropriate.Returning a
Session
on successful passkey auth matches the email/username login flows. No issues.
475-476
: Schema vs AI summary mismatch on revokePasskey return type.The schema returns
ID
, but the AI summary claims it was changed toBoolean
. TheID
return is correct and necessary for client@deleteEdge
directives to work against the connection. Please ignore the summary and keep theID
return unless you plan a broader client mutation rewrite.If any client code expects a boolean, update it to handle
ID | null
instead.
483-484
: verifyPasskeyRegistration return type looks right.Returning
PasskeyRegistrationResult!
allows you to pass the newly createdPasskey
for client-side list updates. This pairs well with Relay directives.
verifyPasskeyRegistration( | ||
accountId: $accountId | ||
name: $name | ||
registrationResponse: $registrationResponse | ||
) { | ||
verified | ||
passkey @appendNode(connections: $connections, edgeTypeName: "Passkey") { | ||
id | ||
name | ||
lastUsed | ||
created | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relay append directive uses wrong edge type; list won’t auto-update.
edgeTypeName
must be the connection’s edge type (AccountPasskeysConnectionEdge
), not Passkey
. Using the wrong edge type prevents Relay from constructing the edge and appending the node.
- passkey @appendNode(connections: $connections, edgeTypeName: "Passkey") {
+ passkey @appendNode(
+ connections: $connections,
+ edgeTypeName: "AccountPasskeysConnectionEdge"
+ ) {
id
name
lastUsed
created
}
📝 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.
verifyPasskeyRegistration( | |
accountId: $accountId | |
name: $name | |
registrationResponse: $registrationResponse | |
) { | |
verified | |
passkey @appendNode(connections: $connections, edgeTypeName: "Passkey") { | |
id | |
name | |
lastUsed | |
created | |
} | |
} | |
verifyPasskeyRegistration( | |
accountId: $accountId | |
name: $name | |
registrationResponse: $registrationResponse | |
) { | |
verified | |
passkey @appendNode( | |
connections: $connections, | |
edgeTypeName: "AccountPasskeysConnectionEdge" | |
) { | |
id | |
name | |
lastUsed | |
created | |
} | |
} |
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/[handle]/settings/passkeys.tsx around lines
149–161 the Relay @appendNode directive incorrectly sets edgeTypeName to
"Passkey", which prevents Relay from building the connection edge and
auto-updating the list; change edgeTypeName to the connection edge type
"AccountPasskeysConnectionEdge" so Relay can construct and append the proper
edge (and verify the server response provides the expected edge fields for that
connection if needed).
297fb30
to
f40436a
Compare
rebase & arranging commits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web-next/src/locales/ja-JP/messages.po (1)
699-702
: Fix typos: “ユーサー/ユーサー名” → “ユーザー/ユーザー名”.User-facing typo in a common validation message.
-msgstr "他のユーサーが使用しているユーサー名です。" +msgstr "他のユーザーが使用しているユーザー名です。"
♻️ Duplicate comments (9)
web-next/src/routes/(root)/sign/index.tsx (2)
2-6
: Guard auto passkey attempt: only run in secure, capable browsers.Avoid auto-invoking WebAuthn in unsupported/non-secure contexts. Check
window.isSecureContext
andbrowserSupportsWebAuthn()
before callingonPasskeyLogin
.@@ -import { - type AuthenticationResponseJSON, - type PublicKeyCredentialRequestOptionsJSON, - startAuthentication, -} from "@simplewebauthn/browser"; +import { + type AuthenticationResponseJSON, + type PublicKeyCredentialRequestOptionsJSON, + startAuthentication, + browserSupportsWebAuthn, +} from "@simplewebauthn/browser"; @@ onMount(() => { // Automatically attempt passkey authentication when page loads if (!autoPasskeyAttempted()) { - setAutoPasskeyAttempted(true); - onPasskeyLogin(false); + setAutoPasskeyAttempted(true); + if (typeof window !== "undefined" && window.isSecureContext && browserSupportsWebAuthn()) { + onPasskeyLogin(false); + } } });Also applies to: 149-151, 152-158
32-38
: Fix Biome noRedeclare by aliasing imported GraphQL types and updating generics.The imported type identifiers collide with the GraphQL document consts of the same name, tripping Biome’s noRedeclare. Alias the type imports and update all usages (including Promise response typings) to the aliases. Keep the document const names unchanged.
Apply:
@@ -import type { signByPasskeyMutation } from "./__generated__/signByPasskeyMutation.graphql.ts"; +import type { + signByPasskeyMutation as SignByPasskeyMutation +} from "./__generated__/signByPasskeyMutation.graphql.ts"; @@ -import type { signGetPasskeyAuthenticationOptionsMutation } from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts"; +import type { + signGetPasskeyAuthenticationOptionsMutation as SignGetPasskeyAuthenticationOptionsMutation +} from "./__generated__/signGetPasskeyAuthenticationOptionsMutation.graphql.ts"; @@ -const signGetPasskeyAuthenticationOptionsMutation = graphql` +const signGetPasskeyAuthenticationOptionsMutation = graphql` mutation signGetPasskeyAuthenticationOptionsMutation($sessionId: UUID!) { getPasskeyAuthenticationOptions(sessionId: $sessionId) } `; @@ -const signByPasskeyMutation = graphql` +const signByPasskeyMutation = graphql` mutation signByPasskeyMutation($sessionId: UUID!, $authenticationResponse: JSON!) { loginByPasskey(sessionId: $sessionId, authenticationResponse: $authenticationResponse) { id } } `; @@ - const [getPasskeyOptions] = createMutation< - signGetPasskeyAuthenticationOptionsMutation - >( + const [getPasskeyOptions] = createMutation< + SignGetPasskeyAuthenticationOptionsMutation + >( signGetPasskeyAuthenticationOptionsMutation, ); @@ - const [loginByPasskey] = createMutation<signByPasskeyMutation>( + const [loginByPasskey] = createMutation<SignByPasskeyMutation>( signByPasskeyMutation, ); @@ - const optionsResponse = await new Promise< - signGetPasskeyAuthenticationOptionsMutation["response"] - >((resolve, reject) => { + const optionsResponse = await new Promise< + SignGetPasskeyAuthenticationOptionsMutation["response"] + >((resolve, reject) => { getPasskeyOptions({ variables: { sessionId: tempSessionId }, onCompleted: resolve, onError: reject, }); }); @@ - const loginResponse = await new Promise< - signByPasskeyMutation["response"] - >((resolve, reject) => { + const loginResponse = await new Promise< + SignByPasskeyMutation["response"] + >((resolve, reject) => { loginByPasskey({ variables: { sessionId: tempSessionId, authenticationResponse, }, onCompleted: resolve, onError: reject, }); });Also applies to: 86-99, 140-147, 289-296, 317-327
web-next/src/locales/ja-JP/messages.po (1)
147-150
: Japanese: use 取り消し for revocation (“取消” → “取り消し”).Unify with other entries and fix grammar.
-msgstr "パスキーの取消中にエラーが発生しました。" +msgstr "パスキーの取り消し中にエラーが発生しました。"web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (5)
531-534
: Make AlertDialog clear selection only on close; avoid flicker on open.- <AlertDialog - open={passkeyToRevoke() != null} - onOpenChange={() => setPasskeyToRevoke(null)} - > + <AlertDialog + open={passkeyToRevoke() != null} + onOpenChange={(open) => { + if (!open) setPasskeyToRevoke(null); + }} + >
58-58
: Fix Biome “noRedeclare” by aliasing the generated GraphQL type and updating generics.The constant
passkeysPageQuery
and the imported GraphQL type have the same identifier, triggering Biome’s noRedeclare. Alias the type and update the generics where used.@@ -import type { passkeysPageQuery } from "./__generated__/passkeysPageQuery.graphql.ts"; +import type { + passkeysPageQuery as PasskeysPageQuery +} from "./__generated__/passkeysPageQuery.graphql.ts"; @@ - loadQuery<passkeysPageQuery>( + loadQuery<PasskeysPageQuery>( useRelayEnvironment()(), passkeysPageQuery, { username: handle.replace(/^@/, ""), first, after, }, ), @@ - const data = createPreloadedQuery<passkeysPageQuery>( + const data = createPreloadedQuery<PasskeysPageQuery>( passkeysPageQuery, () => loadPageQuery(params.handle), );Also applies to: 118-134, 176-179
1-5
: Guard registration behind secure-context and WebAuthn support checks.Avoids runtime failures on HTTP and unsupported browsers; provides actionable UX.
@@ import { type PublicKeyCredentialCreationOptionsJSON, type RegistrationResponseJSON, startRegistration, + browserSupportsWebAuthn, } from "@simplewebauthn/browser"; @@ async function onRegisterPasskey() { const account = data()?.accountByUsername; const name = passkeyNameRef?.value?.trim() ?? ""; if (!account || !name) return; + if (!(typeof window !== "undefined" && window.isSecureContext)) { + showToast({ + title: t`Failed to register passkey`, + description: t`Passkeys require a secure (HTTPS) context.`, + variant: "error", + }); + return; + } + if (!browserSupportsWebAuthn()) { + showToast({ + title: t`Failed to register passkey`, + description: t`Your browser does not support passkeys.`, + variant: "error`, + }); + return; + }Also applies to: 223-229
142-163
: Relay append directive uses wrong edge type; list won’t auto-update.
edgeTypeName
must be the connection’s edge type (AccountPasskeysConnectionEdge
), notPasskey
.) { verifyPasskeyRegistration( accountId: $accountId name: $name registrationResponse: $registrationResponse ) { verified - passkey @appendNode(connections: $connections, edgeTypeName: "Passkey") { + passkey @appendNode( + connections: $connections, + edgeTypeName: "AccountPasskeysConnectionEdge" + ) { id name lastUsed created } } }
539-539
: Fix Lingui placeholder usage in tagged template literal.Use
{0}
and pass the value as an argument;${…}
won’t be interpolated by the macro.- {t`Are you sure you want to revoke passkey ${passkeyToRevoke()?.name}? You won't be able to use it to sign in to your account anymore.`} + {t`Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore.`(passkeyToRevoke()?.name)}graphql/passkey.ts (1)
89-113
: Deduplicate auth + account-fetch logic across mutations.The same session/authorization/account-loading block appears three times. Extract a helper to centralize behavior and reduce maintenance cost.
Sketch:
async function requireSelfAndAccount(ctx, accountGlobalId) { const session = await ctx.session; if (!session) throw new Error("Not authenticated."); if (session.accountId !== accountGlobalId.id) throw new Error("Not authorized."); const account = await ctx.db.query.accountTable.findFirst({ where: { id: accountGlobalId.id }, with: { passkeys: true }, }); if (!account) throw new Error("Account not found."); return account; }Then reuse in
getPasskeyRegistrationOptions
andverifyPasskeyRegistration
.Also applies to: 114-156, 157-179
🧹 Nitpick comments (9)
web-next/src/locales/zh-CN/messages.po (1)
143-150
: Unify pronouns in passkey strings (“你/你的” vs “您/您的”).Passkey-related messages mix polite and casual pronouns. Aligning to one style improves consistency; nearby passkey strings already use “你/你的”.
@@ -msgstr "在注册您的通行密钥时发生错误。" +msgstr "在注册你的通行密钥时发生错误。" @@ -msgstr "在撤销您的通行密钥时发生错误。" +msgstr "在撤销你的通行密钥时发生错误。" @@ -msgstr "您尚未注册任何通行密钥。" +msgstr "你尚未注册任何通行密钥。" @@ -msgstr "您的通行密钥已注册,现在可以用于身份验证。" +msgstr "你的通行密钥已注册,现在可以用于身份验证。"Also applies to: 749-752, 782-785
graphql/schema.graphql (1)
451-454
: Fix typos in docstrings (“variabvles” → “variables”).Minor typo in parameter docs.
@@ - The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. + The RFC 6570-compliant URI Template for the verification link. Available variables: `{token}` and `{code}`. @@ - The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. + The RFC 6570-compliant URI Template for the verification link. Available variables: `{token}` and `{code}`.Also applies to: 471-473
web-next/src/locales/ja-JP/messages.po (3)
207-210
: Punctuation consistency: use fullwidth colon “:”.Align with other JP UI strings.
-msgstr "作成日:" +msgstr "作成日:" @@ -msgstr "最終使用:" +msgstr "最終使用:"Also applies to: 363-366
383-386
: Copy: match “Load more …” phrasing used elsewhere.Other lists use “…をもっと読み込む”. Adjust for consistency.
-msgstr "パスキーを読み込む" +msgstr "パスキーをもっと読み込む"
260-263
: Example phrasing: prefer 「例)」 and keep “My key” literal.More natural JP UI example.
-msgstr "例) 私のキー" +msgstr "例)My key"web-next/src/locales/en-US/messages.po (2)
260-263
: Polish example phrasing (“e.g., My key”).“ex) …” is non-idiomatic in en-US.
-msgstr "ex) My key" +msgstr "e.g., My key"
619-622
: Grammar: add “to” (“Signing in to Hackers' Pub”).Reads more naturally.
-msgstr "Signing in Hackers' Pub" +msgstr "Signing in to Hackers' Pub"web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (1)
223-229
: Add a lightweight client-side duplicate-name check (case-insensitive).This is a UX guard to prevent obvious duplicates in the currently loaded page(s). Server must still enforce uniqueness (see GraphQL note).
async function onRegisterPasskey() { const account = data()?.accountByUsername; const name = passkeyNameRef?.value?.trim() ?? ""; if (!account || !name) return; + const existing = + passkeyData()?.passkeys.edges.some( + (e) => e.node.name.toLowerCase() === name.toLowerCase(), + ) ?? false; + if (existing) { + showToast({ + title: t`Failed to register passkey`, + description: t`A passkey with this name already exists.`, + variant: "error", + }); + return; + }graphql/passkey.ts (1)
44-87
: Stabilize cursor pagination with a deterministic tie-breaker.Using only
created
as the cursor risks duplicates/omissions when multiple rows share the same millisecond. Add a secondary sort (e.g.,id
) and encode both in the cursor.Example approach:
- order by
(created DESC, id DESC)
when not inverted; inverse when inverted- use
toCursor: ({ created, id }) => \
${created.valueOf()}:${id}``- parse the composite cursor in the where clause to compare
(created, id)
pairs accordinglyIf you prefer to keep a single field cursor, consider switching to a high-resolution timestamp column.
📜 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.
📒 Files selected for processing (10)
graphql/passkey.ts
(1 hunks)graphql/schema.graphql
(6 hunks)web-next/src/components/SettingsTabs.tsx
(3 hunks)web-next/src/locales/en-US/messages.po
(22 hunks)web-next/src/locales/ja-JP/messages.po
(22 hunks)web-next/src/locales/ko-KR/messages.po
(22 hunks)web-next/src/locales/zh-CN/messages.po
(22 hunks)web-next/src/locales/zh-TW/messages.po
(22 hunks)web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
(1 hunks)web-next/src/routes/(root)/sign/index.tsx
(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- web-next/src/locales/ko-KR/messages.po
- web-next/src/components/SettingsTabs.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (2)
models/passkey.ts (1)
verifyRegistration
(47-85)graphql/passkey.ts (4)
resolve
(51-86)resolve
(95-112)resolve
(121-155)resolve
(163-177)
graphql/passkey.ts (2)
models/schema.ts (1)
passkeyTable
(143-171)models/passkey.ts (2)
getRegistrationOptions
(28-45)verifyRegistration
(47-85)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/sign/index.tsx
[error] 86-86: Shouldn't redeclare 'signGetPasskeyAuthenticationOptionsMutation'. Consider to delete it or rename it.
'signGetPasskeyAuthenticationOptionsMutation' is defined here:
(lint/suspicious/noRedeclare)
[error] 92-92: Shouldn't redeclare 'signByPasskeyMutation'. Consider to delete it or rename it.
'signByPasskeyMutation' is defined here:
(lint/suspicious/noRedeclare)
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
[error] 73-73: Shouldn't redeclare 'passkeysPageQuery'. Consider to delete it or rename it.
'passkeysPageQuery' is defined here:
(lint/suspicious/noRedeclare)
⏰ 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: image (ubuntu-24.04-arm)
- GitHub Check: image (ubuntu-latest)
🔇 Additional comments (4)
web-next/src/routes/(root)/sign/index.tsx (1)
400-423
: UI/UX: Nice addition of divider and passkey CTA.The “Or” divider and passkey button are placed well, and disabling the button while authenticating avoids duplicate ceremonies.
Also applies to: 473-476
web-next/src/locales/zh-CN/messages.po (1)
473-476
: “Or” translation looks good.Using “或” here matches common UI copy conventions.
graphql/schema.graphql (1)
439-444
: Schema shape for passkeys looks solid.
- Account.passkeys connection + dedicated edge types are well-formed.
- Mutations returning JSON for WebAuthn options/responses are pragmatic.
- Passkey node fields (id, name, created, lastUsed) are sufficient MVP.
Also applies to: 456-463, 555-565, 99-107, 18-19
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx (1)
248-253
: Confirm@simplewebauthn/browser
is installed and itsstartRegistration
signature matches your callI didn’t find any
@simplewebauthn/browser
entry inweb-next/package.json
, nor anode_modules
to inspect—so I can’t verify which version you’re on. Please:
- Ensure you’ve added
@simplewebauthn/browser
as a dependency (and runpnpm install
/npm install
).- Check whether your version of
startRegistration
expects:
- an object form:
(recent versions), orawait startRegistration({ optionsJSON: options as PublicKeyCredentialCreationOptionsJSON, });- the raw
PublicKeyCredentialCreationOptionsJSON
:(older versions).await startRegistration( options as PublicKeyCredentialCreationOptionsJSON, );Adjust the call in
web-next/src/routes/(root)/[handle]/settings/passkeys.tsx
accordingly to avoid TS or runtime mismatches.
async resolve(_, args, ctx) { | ||
const session = await ctx.session; | ||
if (session == null) throw new Error("Not authenticated."); | ||
if (session.accountId !== args.accountId.id) { | ||
throw new Error("Not authorized."); | ||
} | ||
const account = await ctx.db.query.accountTable.findFirst({ | ||
where: { id: args.accountId.id }, | ||
with: { passkeys: true }, | ||
}); | ||
if (account == null) throw new Error("Account not found."); | ||
const result = await verifyRegistration( | ||
ctx.db, | ||
ctx.kv, | ||
ctx.fedCtx.canonicalOrigin, | ||
account, | ||
args.name, | ||
args.registrationResponse as RegistrationResponseJSON, | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enforce case-insensitive uniqueness of passkey names (server-side).
Currently neither server nor DB prevents duplicate names per account. UX-only checks can be bypassed. Add a DB unique index on (account_id, LOWER(name))
and optionally validate before insert to return a friendly error.
Proposed DB change (Drizzle + migration):
- schema:
import { unique, sql } from "drizzle-orm/pg-core";
// …
unique()
.on(passkeyTable.accountId, sql`LOWER(${passkeyTable.name})`)
.name("passkey_account_id_name_lower_unique"),
- SQL:
CREATE UNIQUE INDEX IF NOT EXISTS passkey_account_id_name_lower_unique
ON passkey(account_id, LOWER(name));
Optional pre-check here to fail fast:
const exists = await ctx.db.query.passkeyTable.findFirst({
where: and(
eq(passkeyTable.accountId, account.id),
sql`LOWER(${passkeyTable.name}) = LOWER(${args.name})`,
),
});
if (exists) throw new Error("Passkey name already exists.");
Would you like a follow-up patch/migration?
import { | ||
type AuthenticationResponseJSON, | ||
type PublicKeyCredentialRequestOptionsJSON, | ||
startAuthentication, | ||
} from "@simplewebauthn/browser"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Handle user-cancelled WebAuthn gracefully and preserve error details.
Currently you rethrow a new Error, losing name/code
to distinguish user cancellation (“NotAllowedError”, “AbortError”, or WebAuthnError codes). Preserve the original error and silence toasts on cancel; show a toast for genuine failures.
@@
-import {
- type AuthenticationResponseJSON,
- type PublicKeyCredentialRequestOptionsJSON,
- startAuthentication,
-} from "@simplewebauthn/browser";
+import {
+ type AuthenticationResponseJSON,
+ type PublicKeyCredentialRequestOptionsJSON,
+ startAuthentication,
+ WebAuthnError,
+} from "@simplewebauthn/browser";
@@
- try {
- authenticationResponse = await startAuthentication({
- optionsJSON: options as PublicKeyCredentialRequestOptionsJSON,
- });
- } catch (error) {
- throw new Error(
- error instanceof Error ? error.message : "Authentication failed",
- );
- }
+ try {
+ authenticationResponse = await startAuthentication({
+ optionsJSON: options as PublicKeyCredentialRequestOptionsJSON,
+ });
+ } catch (error) {
+ // Re-throw as-is so outer catch can identify cancellation vs real error
+ throw error;
+ }
@@
- } catch (_) {
- if (showError) {
- showToast({
- title: t`Passkey authentication failed`,
- variant: "destructive",
- });
- }
+ } catch (err) {
+ const name = (err as Error)?.name ?? "";
+ const isCancelled =
+ err instanceof WebAuthnError
+ ? err.code === "ERROR_CEREMONY_ABORTED"
+ : name === "NotAllowedError" || name === "AbortError";
+ if (showError && !isCancelled) {
+ showToast({
+ title: t`Passkey authentication failed`,
+ variant: "destructive",
+ });
+ }
} finally {
setPasskeyAuthenticating(false);
}
Also applies to: 304-314, 343-352
🤖 Prompt for AI Agents
In web-next/src/routes/(root)/sign/index.tsx around lines 2-6 (and also apply to
304-314 and 343-352): the current catch blocks rethrow new Error which discards
the original error.name/code so you cannot detect user-cancelled WebAuthn
events; instead preserve and rethrow or return the original error object (or
wrap it while retaining the original as a cause), check error.name and
error.code for "NotAllowedError", "AbortError", or WebAuthn-specific codes and
suppress toasts for those cancellation cases, but show a toast for all other
errors; update the catch handlers to not replace the error, use the original
error for logging/toast decision, and ensure callers receive the original error
(or an error with the original as its cause) so cancellation can be
distinguished.
f40436a
to
29fdc45
Compare
There was a problem hiding this 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)
web-next/src/locales/ja-JP/messages.po (1)
699-701
: Fix user-facing typo: ユーサー → ユーザー (twice).Visible typo in a common validation message.
-msgstr "他のユーサーが使用しているユーサー名です。" +msgstr "他のユーザーが使用しているユーザー名です。"
♻️ Duplicate comments (4)
web-next/src/locales/ko-KR/messages.po (2)
178-181
: Normalize typography (spacing, colon, word choice) per ko-KR style.Minor but user-visible polish: insert required spaces before “중…”, use ASCII colon “:”, and prefer concise “내”.
#: src/routes/(root)/sign/index.tsx:419 -msgid "Authenticating…" -msgstr "인증중…" +msgid "Authenticating…" +msgstr "인증 중…" #: src/routes/(root)/[handle]/settings/passkeys.tsx:462 -msgid "Created:" -msgstr "생성일" +msgid "Created:" +msgstr "생성일:" #: src/routes/(root)/[handle]/settings/passkeys.tsx:409 -msgid "ex) My key" -msgstr "예) 나의 키" +msgid "ex) My key" +msgstr "예) 내 키" #: src/routes/(root)/[handle]/settings/passkeys.tsx:471 -msgid "Last used:" -msgstr "마지막 사용:" +msgid "Last used:" +msgstr "마지막 사용:" #: src/routes/(root)/[handle]/settings/passkeys.tsx:420 -msgid "Registering…" -msgstr "등록중…" +msgid "Registering…" +msgstr "등록 중…" #: src/routes/(root)/[handle]/settings/passkeys.tsx:280 -msgid "Your passkey has been registered and can now be used for authentication." -msgstr "패스키가 등록되었습니다. 이제 이 패스키를 사용하여 로그인 할 수 있습니다." +msgid "Your passkey has been registered and can now be used for authentication." +msgstr "패스키가 등록되었습니다. 이제 이 패스키로 로그인할 수 있습니다."Also applies to: 207-210, 260-263, 363-366, 567-570, 782-785
147-150
: Use “삭제” (or “해지”) for destructive revoke actions; avoid clashing with the “취소” button.“취소” is ambiguous here and collides with the Cancel button. Please switch revoke-related copy to “삭제” (or “해지” if preferred) across errors, confirmation, status, and action labels for consistency and clarity.
Apply:
#: src/routes/(root)/[handle]/settings/passkeys.tsx:341 -msgid "An error occurred while revoking your passkey." -msgstr "패스키를 취소하는 중에 오류가 발생했습니다." +msgid "An error occurred while revoking your passkey." +msgstr "패스키를 삭제하는 중 오류가 발생했습니다." #. placeholder {0}: passkeyToRevoke()?.name #: src/routes/(root)/[handle]/settings/passkeys.tsx:539 -msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." -msgstr "{0} 패스키를 취소하시겠습니까? 이 패스키를 사용하여 계정에 로그인할 수 없게 됩니다." +msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." +msgstr "패스키 {0}를 삭제하시겠습니까? 이 패스키로는 더 이상 로그인할 수 없습니다." #: src/routes/(root)/[handle]/settings/passkeys.tsx:332 #: src/routes/(root)/[handle]/settings/passkeys.tsx:338 -msgid "Failed to revoke passkey" -msgstr "패스키 취소 실패" +msgid "Failed to revoke passkey" +msgstr "패스키 삭제 실패" #: src/routes/(root)/[handle]/settings/passkeys.tsx:325 -msgid "Passkey revoked" -msgstr "패스키 취소됨" +msgid "Passkey revoked" +msgstr "패스키 삭제됨" #: src/routes/(root)/[handle]/settings/passkeys.tsx:492 #: src/routes/(root)/[handle]/settings/passkeys.tsx:548 -msgid "Revoke" -msgstr "취소" +msgid "Revoke" +msgstr "삭제" #: src/routes/(root)/[handle]/settings/passkeys.tsx:537 -msgid "Revoke passkey" -msgstr "패스키를 취소" +msgid "Revoke passkey" +msgstr "패스키 삭제" #: src/routes/(root)/[handle]/settings/passkeys.tsx:326 -msgid "The passkey has been successfully revoked." -msgstr "성공적으로 패스키를 취소했습니다." +msgid "The passkey has been successfully revoked." +msgstr "패스키가 성공적으로 삭제되었습니다."If “삭제” doesn’t fit your product voice, “해지” is also acceptable—just apply consistently across all related strings.
Also applies to: 159-163, 289-297, 497-500, 571-575, 576-579, 667-670
web-next/src/locales/ja-JP/messages.po (1)
147-150
: Unify “取消” → “取り消し” (one lingering string).Prior feedback asked to replace 取消 with 取り消し/取り消す; this one instance remains.
-msgstr "パスキーの取消中にエラーが発生しました。" +msgstr "パスキーの取り消し中にエラーが発生しました。"web-next/src/locales/zh-CN/messages.po (1)
473-476
: “Or” → “或” confirmed (matches prior review)This addresses the earlier suggestion; thanks for applying it.
🧹 Nitpick comments (19)
web-next/src/locales/ko-KR/messages.po (2)
607-610
: Polish: more natural phrasing for “Sign in with passkey”.Shorter and idiomatic in Korean.
#: src/routes/(root)/sign/index.tsx:420 -msgid "Sign in with passkey" -msgstr "패스키를 사용하여 로그인" +msgid "Sign in with passkey" +msgstr "패스키로 로그인"
663-666
: Tighten grammar for the multi-passkey notice.Avoid plural marker “들이” + “이들을”; use “해당 패스키들” for clearer reference.
#: src/routes/(root)/[handle]/settings/passkeys.tsx:429 -msgid "The following passkeys are registered to your account. You can use them to sign in to your account." -msgstr "다음 패스키들이 계정에 등록되어 있습니다. 이들을 사용하여 계정에 로그인할 수 있습니다." +msgid "The following passkeys are registered to your account. You can use them to sign in to your account." +msgstr "다음 패스키가 계정에 등록되어 있습니다. 해당 패스키들로 계정에 로그인할 수 있습니다."web-next/src/locales/ja-JP/messages.po (11)
159-163
: Quote the passkey name and tighten the second sentence.More natural Japanese and clearer delineation of the name.
-msgstr "パスキー{0}を取り消しますか?このパスキーを使用してアカウントにログインできなくなります。" +msgstr "パスキー「{0}」を取り消しますか?このパスキーではアカウントにログインできなくなります。"
607-610
: Use ログイン consistently (“Sign in with passkey”).Elsewhere “Sign in” → 「ログイン」; avoid mixing with 「サインイン」.
-msgstr "パスキーでサインイン" +msgstr "パスキーでログイン"
131-134
: Polish phrasing and politeness for the sign-in link notice.-msgstr "ログインリンクがメールに送信されました。受信トレイ(または迷惑メールフォルダ)を確認してください。" +msgstr "ログイン用リンクをメールで送信しました。受信トレイ(または迷惑メールフォルダ)をご確認ください。"
207-210
: Use full-width colon for JP UI labels.Unify to “:” to match other entries (e.g., 「Hackers' Pub:通知」).
-msgstr "作成日:" +msgstr "作成日:" @@ -msgstr "最終使用:" +msgstr "最終使用:"Also applies to: 363-366
383-386
: Match “Load more …” phrasing with other lists.Other lists use 「…をもっと読み込む」; align passkeys.
-msgstr "パスキーを読み込む" +msgstr "パスキーをもっと読み込む"
260-263
: Naturalize the placeholder example and punctuation.Use full-width parenthesis style and a JP example.
-msgstr "例) 私のキー" +msgstr "例)自分のキー"
439-442
: Prefer concise “未使用”.-msgstr "使用履歴なし" +msgstr "未使用"
477-480
: Clarify “code from the email”.「メールに記載されたコード」 reads more naturally.
-msgstr "またはメールのコードを入力してください" +msgstr "またはメールに記載されたコードを入力してください"
559-562
: Unify wording with ログイン and make the benefit explicit.-msgstr "アカウントにパスキーを登録してください。メールでログインリンクを受け取る代わりにパスキーを使用できます。" +msgstr "アカウントにパスキーを登録してください。メールでログインリンクを受け取る代わりに、パスキーでログインできます。"
236-239
: Minor refinement for natural phrasing.-msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので、友人に招待をお願いしてください。" +msgstr "アカウントが必要ですか?Hackers' Pubは招待制のため、友人に招待してもらってください。"
663-666
: Tighten and de-duplicate “あなたの/アカウント”.-msgstr "以下のパスキーがあなたのアカウントに登録されています。これらを使用してアカウントにログインできます。" +msgstr "以下のパスキーがアカウントに登録されています。これらを使ってログインできます。"web-next/src/locales/zh-CN/messages.po (6)
131-134
: Polish punctuation and tone for the sign‑in link messageUse a single sentence with a comma before the parenthetical, and align tone with the informal style used elsewhere.
-msgstr "登录链接已发送至您的邮箱。请检查收件箱。(或垃圾邮件文件夹)" +msgstr "登录链接已发送至你的邮箱,请检查收件箱(或垃圾邮件文件夹)。"
143-150
: Unify pronoun to match passkey block style (“你/你的”)Other passkey strings in this file use “你/你的”; align these two for consistency.
-msgstr "在注册您的通行密钥时发生错误。" +msgstr "在注册你的通行密钥时发生错误。" -msgstr "在撤销您的通行密钥时发生错误。" +msgstr "在撤销你的通行密钥时发生错误。"
178-181
: Clarify “Authenticating…” as “身份验证”Slightly clearer in security contexts, keeps the ellipsis glyph.
-msgstr "正在验证…" +msgstr "正在进行身份验证…"
619-626
: Make “Signing in Hackers’ Pub” progressive to match “Signing in…”Small tone/polish tweak; optional.
-msgstr "登录 Hackers' Pub" +msgstr "正在登录 Hackers' Pub"
782-785
: Align registration success message toneMatch nearby passkey strings that use “你/你的”.
-msgstr "您的通行密钥已注册,现在可以用于身份验证。" +msgstr "你的通行密钥已注册,现在可以用于身份验证。"
749-752
: Unify passkey empty-state message tone to informal “你”Confirmed that nearby passkey UI strings at lines 561 and 665 use the informal “你/你的”, while the empty-state message at line 751 still uses the formal “您”. Please update it for consistency:
- msgstr "您尚未注册任何通行密钥。" + msgstr "你尚未注册任何通行密钥。"
📜 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.
📒 Files selected for processing (5)
web-next/src/locales/en-US/messages.po
(22 hunks)web-next/src/locales/ja-JP/messages.po
(22 hunks)web-next/src/locales/ko-KR/messages.po
(22 hunks)web-next/src/locales/zh-CN/messages.po
(22 hunks)web-next/src/locales/zh-TW/messages.po
(22 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- web-next/src/locales/en-US/messages.po
- web-next/src/locales/zh-TW/messages.po
⏰ 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: test
🔇 Additional comments (8)
web-next/src/locales/ja-JP/messages.po (1)
493-500
: Passkeys JP translations: overall solid and consistent.Coverage is good, tone matches the rest of the app, and placeholders are handled correctly. Nice work.
Also applies to: 501-509, 485-488, 663-666, 749-752, 782-785
web-next/src/locales/zh-CN/messages.po (7)
207-210
: LGTM: “Created:” punctuation is correctFullwidth colon used consistently.
363-366
: LGTM: “Last used:”Terminology and fullwidth colon are consistent.
383-386
: LGTM: “Load more passkeys”Unified term “通行密钥” applied.
408-411
: LGTM: “Loading more passkeys…”Unified term and ellipsis glyph are correct.
551-558
: LGTM: Registration labels“注册 / 注册通行密钥” read well and match product terminology.
559-562
: LGTM: Registration helper copyTone and term usage are consistent; nice.
663-670
: LGTM: Revocation successTerminology consistent and affirmative voice is clear.
29fdc45
to
f752a56
Compare
f752a56
to
4efd64e
Compare
migrate passkey management & login by passkey
Summary by CodeRabbit
New Features
UI
Localization