From 4ea76f97a026299ee0def83e258f14c4c84e3885 Mon Sep 17 00:00:00 2001 From: Gyusun Yeom Date: Sun, 10 Aug 2025 16:25:28 +0900 Subject: [PATCH 1/7] Use errors plugin --- deno.json | 1 + deno.lock | 9 +++++++++ graphql/builder.ts | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/deno.json b/deno.json index 7c8c474a..24e79d56 100644 --- a/deno.json +++ b/deno.json @@ -42,6 +42,7 @@ "@pothos/core": "npm:@pothos/core@^4.7.3", "@pothos/plugin-complexity": "npm:@pothos/plugin-complexity@4.1.1", "@pothos/plugin-drizzle": "npm:@pothos/plugin-drizzle@^0.11.1", + "@pothos/plugin-errors": "npm:@pothos/plugin-errors@^4.4.2", "@pothos/plugin-relay": "npm:@pothos/plugin-relay@^4.6.2", "@pothos/plugin-scope-auth": "npm:@pothos/plugin-scope-auth@^4.1.5", "@pothos/plugin-simple-objects": "npm:@pothos/plugin-simple-objects@^4.1.3", diff --git a/deno.lock b/deno.lock index b55f18b6..55fa6629 100644 --- a/deno.lock +++ b/deno.lock @@ -112,6 +112,7 @@ "npm:@pothos/core@^4.7.3": "4.8.1_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02", "npm:@pothos/plugin-complexity@4.1.1": "4.1.1_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02", "npm:@pothos/plugin-drizzle@~0.11.1": "0.11.1_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_drizzle-orm@1.0.0-beta.1-5e64efc__@opentelemetry+api@1.9.0__postgres@3.4.7_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_@opentelemetry+api@1.9.0_postgres@3.4.7_@cloudflare+workers-types@4.20250807.0", + "npm:@pothos/plugin-errors@^4.4.2": "4.4.2_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02", "npm:@pothos/plugin-relay@^4.6.2": "4.6.2_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02", "npm:@pothos/plugin-scope-auth@^4.1.5": "4.1.5_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02", "npm:@pothos/plugin-simple-objects@^4.1.3": "4.1.3_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02", @@ -3670,6 +3671,13 @@ "graphql" ] }, + "@pothos/plugin-errors@4.4.2_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02": { + "integrity": "sha512-5uyvsZZjTMUyhld+6rV0RIjhgjvlo4QI0ivts7mGtv9k7cRF3vXLplKTOo3KpNsUfkUkn6ZCDQUnQkmmAGvmhQ==", + "dependencies": [ + "@pothos/core", + "graphql" + ] + }, "@pothos/plugin-relay@4.6.2_@pothos+core@4.8.1__graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02": { "integrity": "sha512-9aweCv9T53z4+CmE+JF8QoXeAEd+wT/rZflZNzrvH6ln2Lj6qy/EVEcL5BMr6en3/IYHH+ROyHAAsy12t4uIUQ==", "dependencies": [ @@ -12054,6 +12062,7 @@ "npm:@pothos/core@^4.7.3", "npm:@pothos/plugin-complexity@4.1.1", "npm:@pothos/plugin-drizzle@~0.11.1", + "npm:@pothos/plugin-errors@^4.4.2", "npm:@pothos/plugin-relay@^4.6.2", "npm:@pothos/plugin-scope-auth@^4.1.5", "npm:@pothos/plugin-simple-objects@^4.1.3", diff --git a/graphql/builder.ts b/graphql/builder.ts index 6c71542f..35a8153e 100644 --- a/graphql/builder.ts +++ b/graphql/builder.ts @@ -9,6 +9,7 @@ import type { Uuid } from "@hackerspub/models/uuid"; import SchemaBuilder from "@pothos/core"; import ComplexityPlugin from "@pothos/plugin-complexity"; import DrizzlePlugin from "@pothos/plugin-drizzle"; +import ErrorsPlugin from "@pothos/plugin-errors"; import RelayPlugin from "@pothos/plugin-relay"; import ScopeAuthPlugin from "@pothos/plugin-scope-auth"; import SimpleObjectsPlugin from "@pothos/plugin-simple-objects"; @@ -29,6 +30,9 @@ import { import { createGraphQLError } from "graphql-yoga"; import type Keyv from "keyv"; +export type ValuesOfEnumType = T extends + PothosSchemaTypes.EnumRef ? V : never; + export interface ServerContext { db: Database; kv: Keyv; @@ -114,6 +118,7 @@ export const builder = new SchemaBuilder({ SimpleObjectsPlugin, TracingPlugin, WithInputPlugin, + ErrorsPlugin, ], complexity: { defaultComplexity: 1, From ca6f6ccfdc89019db7edaf0711c5b60ca4b4302f Mon Sep 17 00:00:00 2001 From: Gyusun Yeom Date: Sun, 10 Aug 2025 16:46:10 +0900 Subject: [PATCH 2/7] Apply errors-plugin to login mutations --- graphql/login.ts | 48 ++++++++++++-- graphql/schema.graphql | 18 +++++- web-next/src/locales/en-US/messages.po | 22 ++++--- web-next/src/locales/ja-JP/messages.po | 22 ++++--- web-next/src/locales/ko-KR/messages.po | 22 ++++--- web-next/src/locales/zh-CN/messages.po | 22 ++++--- web-next/src/locales/zh-TW/messages.po | 22 ++++--- web-next/src/routes/sign/index.tsx | 88 +++++++++++++++++++------- 8 files changed, 188 insertions(+), 76 deletions(-) diff --git a/graphql/login.ts b/graphql/login.ts index 49c0aeee..a2f9cdd7 100644 --- a/graphql/login.ts +++ b/graphql/login.ts @@ -19,12 +19,34 @@ import { createMessage, type Message } from "@upyo/core"; import { sql } from "drizzle-orm"; import { parseTemplate } from "url-template"; import { Account } from "./account.ts"; -import { builder } from "./builder.ts"; -import { SessionRef } from "./session.ts"; +import { builder, type ValuesOfEnumType } from "./builder.ts"; import { EMAIL_FROM } from "./email.ts"; +import { SessionRef } from "./session.ts"; const logger = getLogger(["hackerspub", "graphql", "login"]); +const LoginErrorKind = builder.enumType("LoginErrorKind", { + values: ["ACCOUNT_NOT_FOUND"] as const, +}); + +class LoginError extends Error { + public constructor( + public readonly kind: ValuesOfEnumType, + ) { + super(`Login error - ${kind}`); + } +} + +builder.objectType(LoginError, { + name: "LoginError", + fields: (t) => ({ + loginErrorKind: t.field({ + type: LoginErrorKind, + resolve: (error) => error.kind, + }), + }), +}); + interface LoginChallenge { accountId: Uuid; token: Uuid; @@ -54,7 +76,12 @@ LoginChallengeRef.implement({ builder.mutationFields((t) => ({ loginByUsername: t.field({ type: LoginChallengeRef, - nullable: true, + errors: { + types: [LoginError], + result: { + name: "LoginSuccess", + }, + }, args: { username: t.arg.string({ required: true, @@ -80,7 +107,9 @@ builder.mutationFields((t) => ({ with: { emails: true }, where: { username: args.username }, }); - if (account == null) return null; + if (account == null) { + throw new LoginError("ACCOUNT_NOT_FOUND"); + } const token = await createSigninToken(ctx.kv, account.id); const messages: Message[] = []; for (const { email } of account.emails) { @@ -110,7 +139,12 @@ builder.mutationFields((t) => ({ loginByEmail: t.field({ type: LoginChallengeRef, - nullable: true, + errors: { + types: [LoginError], + result: { + name: "LoginSuccess", + }, + }, args: { email: t.arg.string({ required: true, @@ -150,7 +184,9 @@ builder.mutationFields((t) => ({ with: { emails: true }, }); } - if (account == null) return null; + if (account == null) { + throw new LoginError("ACCOUNT_NOT_FOUND"); + } const token = await createSigninToken(ctx.kv, account.id); const messages: Message[] = []; for (const { email } of account.emails) { diff --git a/graphql/schema.graphql b/graphql/schema.graphql index a66b140a..82804f2b 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -375,6 +375,18 @@ type LoginChallenge { token: UUID! } +type LoginError { + loginErrorKind: LoginErrorKind! +} + +enum LoginErrorKind { + ACCOUNT_NOT_FOUND +} + +type LoginSuccess { + data: LoginChallenge! +} + """A Hackers' Pub-flavored Markdown text.""" scalar Markdown @@ -421,7 +433,7 @@ type Mutation { The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. """ verifyUrl: URITemplate! - ): LoginChallenge + ): MutationLoginByUsernameResult! loginByUsername( """The locale for the sign-in email.""" locale: Locale! @@ -433,7 +445,7 @@ type Mutation { The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. """ verifyUrl: URITemplate! - ): LoginChallenge + ): MutationLoginByUsernameResult! """Revoke a session by its ID.""" revokeSession( @@ -443,6 +455,8 @@ type Mutation { updateAccount(input: UpdateAccountInput!): UpdateAccountPayload! } +union MutationLoginByUsernameResult = LoginError | LoginSuccess + interface Node { id: ID! } diff --git a/web-next/src/locales/en-US/messages.po b/web-next/src/locales/en-US/messages.po index b98ecf81..9117af66 100644 --- a/web-next/src/locales/en-US/messages.po +++ b/web-next/src/locales/en-US/messages.po @@ -128,7 +128,7 @@ msgstr "{0}'s notes" msgid "{0}'s shares" msgstr "{0}'s shares" -#: src/routes/sign/index.tsx:194 +#: src/routes/sign/index.tsx:180 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." @@ -172,7 +172,7 @@ msgstr "Creating account…" msgid "Display name" msgstr "Display name" -#: src/routes/sign/index.tsx:230 +#: src/routes/sign/index.tsx:272 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." @@ -180,11 +180,11 @@ msgstr "Do you need an account? Hackers' Pub is invite-only—please ask a frien msgid "Email address" msgstr "Email address" -#: src/routes/sign/index.tsx:208 +#: src/routes/sign/index.tsx:250 msgid "Email or username" msgstr "Email or username" -#: src/routes/sign/index.tsx:193 +#: src/routes/sign/index.tsx:188 msgid "Enter your email or username below to sign in." msgstr "Enter your email or username below to sign in." @@ -346,7 +346,7 @@ msgstr "No posts found" #~ msgid "No posts found." #~ msgstr "No posts found." -#: src/routes/sign/index.tsx:192 +#: src/routes/sign/index.tsx:185 msgid "No such account in Hackers' Pub—please try again." msgstr "No such account in Hackers' Pub—please try again." @@ -355,7 +355,7 @@ msgstr "No such account in Hackers' Pub—please try again." msgid "Notes" msgstr "Notes" -#: src/routes/sign/index.tsx:241 +#: src/routes/sign/index.tsx:283 msgid "Or enter the code from the email" msgstr "Or enter the code from the email" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "Shares" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Sign in" msgstr "Sign in" @@ -401,11 +401,11 @@ msgstr "Sign out" msgid "Sign up" msgstr "Sign up" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:233 msgid "Signing in Hackers' Pub" msgstr "Signing in Hackers' Pub" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Signing in…" msgstr "Signing in…" @@ -413,6 +413,10 @@ msgstr "Signing in…" msgid "Signing up for Hackers' Pub" msgstr "Signing up for Hackers' Pub" +#: src/routes/sign/index.tsx:190 +msgid "Something went wrong—please try again." +msgstr "Something went wrong—please try again." + #: src/components/ArticleCard.tsx:231 msgid "Summarized by LLM" msgstr "Summarized by LLM" diff --git a/web-next/src/locales/ja-JP/messages.po b/web-next/src/locales/ja-JP/messages.po index 568c15c7..631a1c7f 100644 --- a/web-next/src/locales/ja-JP/messages.po +++ b/web-next/src/locales/ja-JP/messages.po @@ -128,7 +128,7 @@ msgstr "{0}さんの投稿" msgid "{0}'s shares" msgstr "{0}さんの共有" -#: src/routes/sign/index.tsx:194 +#: src/routes/sign/index.tsx:180 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "ログインリンクがメールに送信されました。受信トレイ(または迷惑メールフォルダ)を確認してください。" @@ -172,7 +172,7 @@ msgstr "アカウントを作成中…" msgid "Display name" msgstr "名前" -#: src/routes/sign/index.tsx:230 +#: src/routes/sign/index.tsx:272 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので、友人に招待をお願いしてください。" @@ -180,11 +180,11 @@ msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので msgid "Email address" msgstr "メールアドレス" -#: src/routes/sign/index.tsx:208 +#: src/routes/sign/index.tsx:250 msgid "Email or username" msgstr "メールアドレスまたはユーザー名" -#: src/routes/sign/index.tsx:193 +#: src/routes/sign/index.tsx:188 msgid "Enter your email or username below to sign in." msgstr "以下にメールアドレスまたはユーザー名を入力してログインしてください。" @@ -346,7 +346,7 @@ msgstr "コンテンツはありません" #~ msgid "No posts found." #~ msgstr "コンテンツが見つかりませんでした。" -#: src/routes/sign/index.tsx:192 +#: src/routes/sign/index.tsx:185 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pubにそのようなアカウントはありません。もう一度お試しください。" @@ -355,7 +355,7 @@ msgstr "Hackers' Pubにそのようなアカウントはありません。もう msgid "Notes" msgstr "投稿" -#: src/routes/sign/index.tsx:241 +#: src/routes/sign/index.tsx:283 msgid "Or enter the code from the email" msgstr "またはメールのコードを入力してください" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "共有" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Sign in" msgstr "ログイン" @@ -401,11 +401,11 @@ msgstr "ログアウト" msgid "Sign up" msgstr "登録" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:233 msgid "Signing in Hackers' Pub" msgstr "Hackers' Pubにログイン" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Signing in…" msgstr "ログイン中…" @@ -413,6 +413,10 @@ msgstr "ログイン中…" msgid "Signing up for Hackers' Pub" msgstr "Hackers' Pubに登録" +#: src/routes/sign/index.tsx:190 +msgid "Something went wrong—please try again." +msgstr "問題が発生しました。再度お試しください。" + #: src/components/ArticleCard.tsx:231 msgid "Summarized by LLM" msgstr "LLMによる要約" diff --git a/web-next/src/locales/ko-KR/messages.po b/web-next/src/locales/ko-KR/messages.po index ea98030e..372659c0 100644 --- a/web-next/src/locales/ko-KR/messages.po +++ b/web-next/src/locales/ko-KR/messages.po @@ -128,7 +128,7 @@ msgstr "{0} 님의 단문" msgid "{0}'s shares" msgstr "{0} 님의 공유" -#: src/routes/sign/index.tsx:194 +#: src/routes/sign/index.tsx:180 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "로그인 링크가 이메일로 전송되었습니다. 받은 편지함(또는 스팸 폴더)을 확인해주세요." @@ -172,7 +172,7 @@ msgstr "계정을 생성하는 중…" msgid "Display name" msgstr "이름" -#: src/routes/sign/index.tsx:230 +#: src/routes/sign/index.tsx:272 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "계정이 필요하신가요? Hackers' Pub은 초대 전용입니다. 친구에게 초대를 요청해주세요." @@ -180,11 +180,11 @@ msgstr "계정이 필요하신가요? Hackers' Pub은 초대 전용입니다. msgid "Email address" msgstr "이메일 주소" -#: src/routes/sign/index.tsx:208 +#: src/routes/sign/index.tsx:250 msgid "Email or username" msgstr "이메일 또는 아이디" -#: src/routes/sign/index.tsx:193 +#: src/routes/sign/index.tsx:188 msgid "Enter your email or username below to sign in." msgstr "로그인하려면 아래에 이메일 또는 아이디를 입력해주세요." @@ -346,7 +346,7 @@ msgstr "콘텐츠가 없습니다" #~ msgid "No posts found." #~ msgstr "콘텐츠를 찾을 수 없습니다." -#: src/routes/sign/index.tsx:192 +#: src/routes/sign/index.tsx:185 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub에 해당 계정이 없습니다. 다시 시도해주세요." @@ -355,7 +355,7 @@ msgstr "Hackers' Pub에 해당 계정이 없습니다. 다시 시도해주세요 msgid "Notes" msgstr "단문" -#: src/routes/sign/index.tsx:241 +#: src/routes/sign/index.tsx:283 msgid "Or enter the code from the email" msgstr "또는 이메일의 코드를 입력하세요" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "공유" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Sign in" msgstr "로그인" @@ -401,11 +401,11 @@ msgstr "로그아웃" msgid "Sign up" msgstr "가입" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:233 msgid "Signing in Hackers' Pub" msgstr "Hackers' Pub에 로그인" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Signing in…" msgstr "로그인 중…" @@ -413,6 +413,10 @@ msgstr "로그인 중…" msgid "Signing up for Hackers' Pub" msgstr "Hackers' Pub 가입" +#: src/routes/sign/index.tsx:190 +msgid "Something went wrong—please try again." +msgstr "문제가 발생했습니다. 다시 시도해주세요." + #: src/components/ArticleCard.tsx:231 msgid "Summarized by LLM" msgstr "LLM 요약" diff --git a/web-next/src/locales/zh-CN/messages.po b/web-next/src/locales/zh-CN/messages.po index 614fef70..c3de89f8 100644 --- a/web-next/src/locales/zh-CN/messages.po +++ b/web-next/src/locales/zh-CN/messages.po @@ -128,7 +128,7 @@ msgstr "{0}的帖子" msgid "{0}'s shares" msgstr "{0}的转帖" -#: src/routes/sign/index.tsx:194 +#: src/routes/sign/index.tsx:180 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "登录链接已发送至您的邮箱。请检查收件箱。(或垃圾邮件文件夹)" @@ -172,7 +172,7 @@ msgstr "正在创建账户…" msgid "Display name" msgstr "昵称" -#: src/routes/sign/index.tsx:230 +#: src/routes/sign/index.tsx:272 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "需要创建账户吗?Hackers' Pub 仅限邀请,请联系朋友邀请您。" @@ -180,11 +180,11 @@ msgstr "需要创建账户吗?Hackers' Pub 仅限邀请,请联系朋友邀 msgid "Email address" msgstr "电子邮件地址" -#: src/routes/sign/index.tsx:208 +#: src/routes/sign/index.tsx:250 msgid "Email or username" msgstr "邮箱或用户名" -#: src/routes/sign/index.tsx:193 +#: src/routes/sign/index.tsx:188 msgid "Enter your email or username below to sign in." msgstr "请在下方输入您的邮箱或用户名以登录。" @@ -346,7 +346,7 @@ msgstr "未找到内容" #~ msgid "No posts found." #~ msgstr "未找到内容。" -#: src/routes/sign/index.tsx:192 +#: src/routes/sign/index.tsx:185 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub 中无此账户,请重试。" @@ -355,7 +355,7 @@ msgstr "Hackers' Pub 中无此账户,请重试。" msgid "Notes" msgstr "帖子" -#: src/routes/sign/index.tsx:241 +#: src/routes/sign/index.tsx:283 msgid "Or enter the code from the email" msgstr "或输入邮件中的验证码" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "转帖" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Sign in" msgstr "登录" @@ -401,11 +401,11 @@ msgstr "登出" msgid "Sign up" msgstr "注册" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:233 msgid "Signing in Hackers' Pub" msgstr "登录 Hackers' Pub" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Signing in…" msgstr "登录中…" @@ -413,6 +413,10 @@ msgstr "登录中…" msgid "Signing up for Hackers' Pub" msgstr "注册 Hackers' Pub" +#: src/routes/sign/index.tsx:190 +msgid "Something went wrong—please try again." +msgstr "出现错误,请重试。" + #: src/components/ArticleCard.tsx:231 msgid "Summarized by LLM" msgstr "由 AI 生成的摘要" diff --git a/web-next/src/locales/zh-TW/messages.po b/web-next/src/locales/zh-TW/messages.po index 3e811d7b..b3696da6 100644 --- a/web-next/src/locales/zh-TW/messages.po +++ b/web-next/src/locales/zh-TW/messages.po @@ -128,7 +128,7 @@ msgstr "{0}的貼文" msgid "{0}'s shares" msgstr "{0}的轉貼" -#: src/routes/sign/index.tsx:194 +#: src/routes/sign/index.tsx:180 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "登入連結已傳送至您的郵箱。請檢查收件匣。(或垃圾郵件資料夾)" @@ -172,7 +172,7 @@ msgstr "創建帳戶…" msgid "Display name" msgstr "暱稱" -#: src/routes/sign/index.tsx:230 +#: src/routes/sign/index.tsx:272 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "需要建立帳戶嗎?Hackers' Pub 僅限邀請,請聯繫朋友邀請您。" @@ -180,11 +180,11 @@ msgstr "需要建立帳戶嗎?Hackers' Pub 僅限邀請,請聯繫朋友邀 msgid "Email address" msgstr "電子郵件地址" -#: src/routes/sign/index.tsx:208 +#: src/routes/sign/index.tsx:250 msgid "Email or username" msgstr "電子郵件或使用者名稱" -#: src/routes/sign/index.tsx:193 +#: src/routes/sign/index.tsx:188 msgid "Enter your email or username below to sign in." msgstr "請在下方輸入您的電子郵件或使用者名稱以登入。" @@ -346,7 +346,7 @@ msgstr "未找到內容" #~ msgid "No posts found." #~ msgstr "未找到內容。" -#: src/routes/sign/index.tsx:192 +#: src/routes/sign/index.tsx:185 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub 中無此帳戶,請重試。" @@ -355,7 +355,7 @@ msgstr "Hackers' Pub 中無此帳戶,請重試。" msgid "Notes" msgstr "貼文" -#: src/routes/sign/index.tsx:241 +#: src/routes/sign/index.tsx:283 msgid "Or enter the code from the email" msgstr "或輸入郵件中的驗證碼" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "轉貼" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Sign in" msgstr "登入" @@ -401,11 +401,11 @@ msgstr "登出" msgid "Sign up" msgstr "註冊" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:233 msgid "Signing in Hackers' Pub" msgstr "登入 Hackers' Pub" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:266 msgid "Signing in…" msgstr "登入中…" @@ -413,6 +413,10 @@ msgstr "登入中…" msgid "Signing up for Hackers' Pub" msgstr "註冊 Hackers' Pub" +#: src/routes/sign/index.tsx:190 +msgid "Something went wrong—please try again." +msgstr "發生錯誤,請重試。" + #: src/components/ArticleCard.tsx:231 msgid "Summarized by LLM" msgstr "由 AI 生成的摘要" diff --git a/web-next/src/routes/sign/index.tsx b/web-next/src/routes/sign/index.tsx index 8f7a5114..f9c66000 100644 --- a/web-next/src/routes/sign/index.tsx +++ b/web-next/src/routes/sign/index.tsx @@ -20,7 +20,10 @@ import { } from "~/components/ui/text-field.tsx"; import { useLingui } from "~/lib/i18n/macro.d.ts"; import { Button } from "../../components/ui/button.tsx"; -import type { signByEmailMutation } from "./__generated__/signByEmailMutation.graphql.ts"; +import type { + LoginErrorKind, + signByEmailMutation, +} from "./__generated__/signByEmailMutation.graphql.ts"; import type { signByUsernameMutation, signByUsernameMutation$data, @@ -30,12 +33,20 @@ import type { signCompleteMutation } from "./__generated__/signCompleteMutation. const signByEmailMutation = graphql` mutation signByEmailMutation($locale: Locale!, $email: String!, $verifyUrl: URITemplate!) { loginByEmail(locale: $locale, email: $email, verifyUrl: $verifyUrl) { - account { - name - handle - avatarUrl + __typename + ... on LoginSuccess { + data { + account { + name + handle + avatarUrl + } + token + } + } + ... on LoginError { + loginErrorKind } - token } } `; @@ -43,12 +54,20 @@ const signByEmailMutation = graphql` const signByUsernameMutation = graphql` mutation signByUsernameMutation($locale: Locale!, $username: String!, $verifyUrl: URITemplate!) { loginByUsername(locale: $locale, username: $username, verifyUrl: $verifyUrl) { - account { - name - handle - avatarUrl + __typename + ... on LoginSuccess { + data { + account { + name + handle + avatarUrl + } + token + } + } + ... on LoginError { + loginErrorKind } - token } } `; @@ -80,7 +99,11 @@ export default function SignPage() { let codeInput: HTMLInputElement | undefined; const [challenging, setChallenging] = createSignal(false); const [email, setEmail] = createSignal(""); - const [invalid, setInvalid] = createSignal(false); + const [errorCode, setErrorCode] = createSignal< + LoginErrorKind | "UNKNOWN" | undefined + >( + undefined, + ); const [token, setToken] = createSignal(undefined); const [loginByEmail] = createMutation( signByEmailMutation, @@ -96,7 +119,7 @@ export default function SignPage() { function onInput() { if (emailInput == null) return; setEmail(emailInput.value.trim()); - setInvalid(false); + setErrorCode(undefined); } function onChallengeSubmit(event: SubmitEvent) { @@ -137,11 +160,34 @@ export default function SignPage() { function onCompleted(data: signByUsernameMutation$data["loginByUsername"]) { setChallenging(false); - if (data == null) { - setInvalid(true); - } else { - setToken(data.token); + if (data.__typename === "LoginSuccess") { + setToken(data.data.token); codeInput?.focus(); + } else if ( + data.__typename === "LoginError" + ) { + setErrorCode(data.loginErrorKind); + } else { + setErrorCode("UNKNOWN"); + } + } + + function getSignInMessage() { + const currentToken = token(); + const currentErrorCode = errorCode(); + + if (currentToken != null) { + return t`A sign-in link has been sent to your email. Please check your inbox (or spam folder).`; + } + + switch (currentErrorCode) { + case "ACCOUNT_NOT_FOUND": + return t`No such account in Hackers' Pub—please try again.`; + case undefined: + case null: + return t`Enter your email or username below to sign in.`; + default: + return t`Something went wrong—please try again.`; } } @@ -187,11 +233,7 @@ export default function SignPage() { {t`Signing in Hackers' Pub`}

- {token() == null - ? invalid() - ? t`No such account in Hackers' Pub—please try again.` - : t`Enter your email or username below to sign in.` - : t`A sign-in link has been sent to your email. Please check your inbox (or spam folder).`} + {getSignInMessage()}

@@ -201,7 +243,7 @@ export default function SignPage() { > From b5838533924c211a278097f7cf6ade7c1499c461 Mon Sep 17 00:00:00 2001 From: Gyusun Yeom Date: Sun, 10 Aug 2025 17:28:35 +0900 Subject: [PATCH 3/7] Handle unknown errors ex) server internal error, client network error --- web-next/src/routes/sign/index.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/web-next/src/routes/sign/index.tsx b/web-next/src/routes/sign/index.tsx index f9c66000..5420bb4c 100644 --- a/web-next/src/routes/sign/index.tsx +++ b/web-next/src/routes/sign/index.tsx @@ -143,6 +143,9 @@ export default function SignPage() { onCompleted(response) { onCompleted(response.loginByEmail); }, + onError(_error) { + onError(); + }, }); } else { loginByUsername({ @@ -154,6 +157,9 @@ export default function SignPage() { onCompleted(response) { onCompleted(response.loginByUsername); }, + onError(_error) { + onError(); + }, }); } } @@ -168,10 +174,16 @@ export default function SignPage() { ) { setErrorCode(data.loginErrorKind); } else { - setErrorCode("UNKNOWN"); + onError(); } } + function onError() { + setChallenging(false); + setErrorCode("UNKNOWN"); + setToken(undefined); + } + function getSignInMessage() { const currentToken = token(); const currentErrorCode = errorCode(); From f3ad49063223697ac9d09f3cb5cf5f5e506d0b83 Mon Sep 17 00:00:00 2001 From: Gyusun Yeom Date: Sun, 10 Aug 2025 19:16:00 +0900 Subject: [PATCH 4/7] Apply errors-plugin to CreateNote mutation --- graphql/post.ts | 43 +++++++++++++++++++++++++++++++++++++----- graphql/schema.graphql | 19 ++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/graphql/post.ts b/graphql/post.ts index d40755f8..f3306820 100644 --- a/graphql/post.ts +++ b/graphql/post.ts @@ -9,7 +9,7 @@ import { unreachable } from "@std/assert"; import { assertNever } from "@std/assert/unstable-never"; import { Account } from "./account.ts"; import { Actor } from "./actor.ts"; -import { builder, Node } from "./builder.ts"; +import { builder, Node, type ValuesOfEnumType } from "./builder.ts"; import { Reactable } from "./reactable.ts"; const PostVisibility = builder.enumType("PostVisibility", { @@ -22,6 +22,33 @@ const PostVisibility = builder.enumType("PostVisibility", { ] as const, }); +const CreateNoteErrorKind = builder.enumType("CreateNoteErrorKind", { + values: [ + "NOT_AUTHENTICATED", + "REPLY_TARGET_NOT_FOUND", + "QUOTED_POST_NOT_FOUND", + "NOTE_CREATION_FAILED", + ] as const, +}); + +class CreateNoteError extends Error { + public constructor( + public readonly kind: ValuesOfEnumType, + ) { + super(`Create note error - ${kind}`); + } +} + +builder.objectType(CreateNoteError, { + name: "CreateNoteError", + fields: (t) => ({ + createNoteErrorKind: t.field({ + type: CreateNoteErrorKind, + resolve: (error) => error.kind, + }), + }), +}); + export const Post = builder.drizzleInterface("postTable", { variant: "Post", interfaces: [Reactable, Node], @@ -458,10 +485,16 @@ builder.relayMutationField( }), }, { + errors: { + types: [CreateNoteError], + result: { + name: "CreateNoteSuccess", + }, + }, async resolve(_root, args, ctx) { const session = await ctx.session; if (session == null) { - throw new Error("Not authenticated."); + throw new CreateNoteError("NOT_AUTHENTICATED"); } const { visibility, content, language, replyTargetId, quotedPostId } = args.input; @@ -472,7 +505,7 @@ builder.relayMutationField( where: { id: replyTargetId.id }, }); if (replyTarget == null) { - throw new Error("Reply target not found."); + throw new CreateNoteError("REPLY_TARGET_NOT_FOUND"); } } let quotedPost: schema.Post & { actor: schema.Actor } | undefined; @@ -482,7 +515,7 @@ builder.relayMutationField( where: { id: quotedPostId.id }, }); if (quotedPost == null) { - throw new Error("Quoted post not found."); + throw new CreateNoteError("QUOTED_POST_NOT_FOUND"); } } return await withTransaction(ctx.fedCtx, async (context) => { @@ -511,7 +544,7 @@ builder.relayMutationField( { replyTarget, quotedPost }, ); if (note == null) { - throw new Error("Failed to create note."); + throw new CreateNoteError("NOTE_CREATION_FAILED"); } return note; }); diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 82804f2b..328d02eb 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -270,6 +270,17 @@ type ArticleContent implements Node { url: URL! } +type CreateNoteError { + createNoteErrorKind: CreateNoteErrorKind! +} + +enum CreateNoteErrorKind { + NOTE_CREATION_FAILED + NOT_AUTHENTICATED + QUOTED_POST_NOT_FOUND + REPLY_TARGET_NOT_FOUND +} + input CreateNoteInput { clientMutationId: ID content: Markdown! @@ -284,6 +295,10 @@ type CreateNotePayload { note: Note! } +type CreateNoteSuccess { + data: CreateNotePayload! +} + type CustomEmoji implements Node { id: ID! imageUrl: String! @@ -421,7 +436,7 @@ type Mutation { """The signup token.""" token: UUID! ): SignupResult! - createNote(input: CreateNoteInput!): CreateNotePayload! + createNote(input: CreateNoteInput!): MutationCreateNoteResult! loginByEmail( """The email of the account to sign in.""" email: String! @@ -455,6 +470,8 @@ type Mutation { updateAccount(input: UpdateAccountInput!): UpdateAccountPayload! } +union MutationCreateNoteResult = CreateNoteError | CreateNoteSuccess + union MutationLoginByUsernameResult = LoginError | LoginSuccess interface Node { From 6d8f1b44b7296a15ccd83025ae1ecb490af55f73 Mon Sep 17 00:00:00 2001 From: Gyusun Yeom Date: Sun, 17 Aug 2025 16:11:41 +0900 Subject: [PATCH 5/7] Declare each error types --- graphql/login.ts | 31 ++++++++-------------- graphql/post.ts | 42 ++++++++++++------------------ graphql/schema.graphql | 35 ++++++++++--------------- graphql/session.ts | 15 +++++++++++ web-next/src/routes/sign/index.tsx | 29 ++++++++++++--------- 5 files changed, 73 insertions(+), 79 deletions(-) diff --git a/graphql/login.ts b/graphql/login.ts index a2f9cdd7..c0730866 100644 --- a/graphql/login.ts +++ b/graphql/login.ts @@ -19,31 +19,22 @@ import { createMessage, type Message } from "@upyo/core"; import { sql } from "drizzle-orm"; import { parseTemplate } from "url-template"; import { Account } from "./account.ts"; -import { builder, type ValuesOfEnumType } from "./builder.ts"; +import { builder } from "./builder.ts"; import { EMAIL_FROM } from "./email.ts"; import { SessionRef } from "./session.ts"; const logger = getLogger(["hackerspub", "graphql", "login"]); -const LoginErrorKind = builder.enumType("LoginErrorKind", { - values: ["ACCOUNT_NOT_FOUND"] as const, -}); - -class LoginError extends Error { - public constructor( - public readonly kind: ValuesOfEnumType, - ) { - super(`Login error - ${kind}`); +class AccountNotFoundError extends Error { + public constructor(public readonly query: string) { + super(`Account not found`); } } -builder.objectType(LoginError, { - name: "LoginError", +builder.objectType(AccountNotFoundError, { + name: "AccountNotFoundError", fields: (t) => ({ - loginErrorKind: t.field({ - type: LoginErrorKind, - resolve: (error) => error.kind, - }), + query: t.exposeString("query"), }), }); @@ -77,7 +68,7 @@ builder.mutationFields((t) => ({ loginByUsername: t.field({ type: LoginChallengeRef, errors: { - types: [LoginError], + types: [AccountNotFoundError], result: { name: "LoginSuccess", }, @@ -108,7 +99,7 @@ builder.mutationFields((t) => ({ where: { username: args.username }, }); if (account == null) { - throw new LoginError("ACCOUNT_NOT_FOUND"); + throw new AccountNotFoundError(args.username); } const token = await createSigninToken(ctx.kv, account.id); const messages: Message[] = []; @@ -140,7 +131,7 @@ builder.mutationFields((t) => ({ loginByEmail: t.field({ type: LoginChallengeRef, errors: { - types: [LoginError], + types: [AccountNotFoundError], result: { name: "LoginSuccess", }, @@ -185,7 +176,7 @@ builder.mutationFields((t) => ({ }); } if (account == null) { - throw new LoginError("ACCOUNT_NOT_FOUND"); + throw new AccountNotFoundError(args.email); } const token = await createSigninToken(ctx.kv, account.id); const messages: Message[] = []; diff --git a/graphql/post.ts b/graphql/post.ts index f3306820..42e2d3ba 100644 --- a/graphql/post.ts +++ b/graphql/post.ts @@ -9,8 +9,9 @@ import { unreachable } from "@std/assert"; import { assertNever } from "@std/assert/unstable-never"; import { Account } from "./account.ts"; import { Actor } from "./actor.ts"; -import { builder, Node, type ValuesOfEnumType } from "./builder.ts"; +import { builder, Node } from "./builder.ts"; import { Reactable } from "./reactable.ts"; +import { NotAuthenticatedError } from "./session.ts"; const PostVisibility = builder.enumType("PostVisibility", { values: [ @@ -22,30 +23,16 @@ const PostVisibility = builder.enumType("PostVisibility", { ] as const, }); -const CreateNoteErrorKind = builder.enumType("CreateNoteErrorKind", { - values: [ - "NOT_AUTHENTICATED", - "REPLY_TARGET_NOT_FOUND", - "QUOTED_POST_NOT_FOUND", - "NOTE_CREATION_FAILED", - ] as const, -}); - -class CreateNoteError extends Error { - public constructor( - public readonly kind: ValuesOfEnumType, - ) { - super(`Create note error - ${kind}`); +class InvalidInputError extends Error { + public constructor(public readonly inputPath: string) { + super(`Invalid input - ${inputPath}`); } } -builder.objectType(CreateNoteError, { - name: "CreateNoteError", +builder.objectType(InvalidInputError, { + name: "InvalidInputError", fields: (t) => ({ - createNoteErrorKind: t.field({ - type: CreateNoteErrorKind, - resolve: (error) => error.kind, - }), + inputPath: t.expose("inputPath", { type: "String" }), }), }); @@ -486,7 +473,10 @@ builder.relayMutationField( }, { errors: { - types: [CreateNoteError], + types: [ + NotAuthenticatedError, + InvalidInputError, + ], result: { name: "CreateNoteSuccess", }, @@ -494,7 +484,7 @@ builder.relayMutationField( async resolve(_root, args, ctx) { const session = await ctx.session; if (session == null) { - throw new CreateNoteError("NOT_AUTHENTICATED"); + throw new NotAuthenticatedError(); } const { visibility, content, language, replyTargetId, quotedPostId } = args.input; @@ -505,7 +495,7 @@ builder.relayMutationField( where: { id: replyTargetId.id }, }); if (replyTarget == null) { - throw new CreateNoteError("REPLY_TARGET_NOT_FOUND"); + throw new InvalidInputError("replyTargetId"); } } let quotedPost: schema.Post & { actor: schema.Actor } | undefined; @@ -515,7 +505,7 @@ builder.relayMutationField( where: { id: quotedPostId.id }, }); if (quotedPost == null) { - throw new CreateNoteError("QUOTED_POST_NOT_FOUND"); + throw new InvalidInputError("quotedPostId"); } } return await withTransaction(ctx.fedCtx, async (context) => { @@ -544,7 +534,7 @@ builder.relayMutationField( { replyTarget, quotedPost }, ); if (note == null) { - throw new CreateNoteError("NOTE_CREATION_FAILED"); + throw new Error("Failed to create note"); } return note; }); diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 328d02eb..55b1e8cc 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -78,6 +78,10 @@ input AccountLinkInput { url: URL! } +type AccountNotFoundError { + query: String! +} + type AccountNotificationsConnection { edges: [AccountNotificationsConnectionEdge!]! pageInfo: PageInfo! @@ -270,17 +274,6 @@ type ArticleContent implements Node { url: URL! } -type CreateNoteError { - createNoteErrorKind: CreateNoteErrorKind! -} - -enum CreateNoteErrorKind { - NOTE_CREATION_FAILED - NOT_AUTHENTICATED - QUOTED_POST_NOT_FOUND - REPLY_TARGET_NOT_FOUND -} - input CreateNoteInput { clientMutationId: ID content: Markdown! @@ -375,6 +368,10 @@ type Instance implements Node { updated: DateTime! } +type InvalidInputError { + inputPath: String! +} + """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ @@ -390,14 +387,6 @@ type LoginChallenge { token: UUID! } -type LoginError { - loginErrorKind: LoginErrorKind! -} - -enum LoginErrorKind { - ACCOUNT_NOT_FOUND -} - type LoginSuccess { data: LoginChallenge! } @@ -470,14 +459,18 @@ type Mutation { updateAccount(input: UpdateAccountInput!): UpdateAccountPayload! } -union MutationCreateNoteResult = CreateNoteError | CreateNoteSuccess +union MutationCreateNoteResult = CreateNoteSuccess | InvalidInputError | NotAuthenticatedError -union MutationLoginByUsernameResult = LoginError | LoginSuccess +union MutationLoginByUsernameResult = AccountNotFoundError | LoginSuccess interface Node { id: ID! } +type NotAuthenticatedError { + notAuthenticated: String! +} + type Note implements Node & Post & Reactable { actor: Actor! content: HTML! diff --git a/graphql/session.ts b/graphql/session.ts index be60f177..b15b09f5 100644 --- a/graphql/session.ts +++ b/graphql/session.ts @@ -2,6 +2,21 @@ import type { Session } from "@hackerspub/models/session"; import { Account } from "./account.ts"; import { builder } from "./builder.ts"; +export class NotAuthenticatedError extends Error { + public constructor() { + super("Not authenticated"); + } +} + +builder.objectType(NotAuthenticatedError, { + name: "NotAuthenticatedError", + fields: (t) => ({ + notAuthenticated: t.string({ + resolve: () => "", + }), + }), +}); + export const SessionRef = builder.objectRef("Session"); SessionRef.implement({ diff --git a/web-next/src/routes/sign/index.tsx b/web-next/src/routes/sign/index.tsx index 5420bb4c..aa9c76ea 100644 --- a/web-next/src/routes/sign/index.tsx +++ b/web-next/src/routes/sign/index.tsx @@ -21,7 +21,6 @@ import { import { useLingui } from "~/lib/i18n/macro.d.ts"; import { Button } from "../../components/ui/button.tsx"; import type { - LoginErrorKind, signByEmailMutation, } from "./__generated__/signByEmailMutation.graphql.ts"; import type { @@ -33,8 +32,8 @@ import type { signCompleteMutation } from "./__generated__/signCompleteMutation. const signByEmailMutation = graphql` mutation signByEmailMutation($locale: Locale!, $email: String!, $verifyUrl: URITemplate!) { loginByEmail(locale: $locale, email: $email, verifyUrl: $verifyUrl) { - __typename ... on LoginSuccess { + __typename data { account { name @@ -44,8 +43,8 @@ const signByEmailMutation = graphql` token } } - ... on LoginError { - loginErrorKind + ... on AccountNotFoundError { + __typename } } } @@ -54,8 +53,8 @@ const signByEmailMutation = graphql` const signByUsernameMutation = graphql` mutation signByUsernameMutation($locale: Locale!, $username: String!, $verifyUrl: URITemplate!) { loginByUsername(locale: $locale, username: $username, verifyUrl: $verifyUrl) { - __typename ... on LoginSuccess { + __typename data { account { name @@ -65,8 +64,8 @@ const signByUsernameMutation = graphql` token } } - ... on LoginError { - loginErrorKind + ... on AccountNotFoundError { + __typename } } } @@ -93,6 +92,11 @@ const setSessionCookie = async (sessionId: Uuid) => { return true; }; +const enum LoginError { + ACCOUNT_NOT_FOUND, + UNKNOWN, +} + export default function SignPage() { const { t, i18n } = useLingui(); let emailInput: HTMLInputElement | undefined; @@ -100,7 +104,8 @@ export default function SignPage() { const [challenging, setChallenging] = createSignal(false); const [email, setEmail] = createSignal(""); const [errorCode, setErrorCode] = createSignal< - LoginErrorKind | "UNKNOWN" | undefined + | LoginError + | undefined >( undefined, ); @@ -170,9 +175,9 @@ export default function SignPage() { setToken(data.data.token); codeInput?.focus(); } else if ( - data.__typename === "LoginError" + data.__typename === "AccountNotFoundError" ) { - setErrorCode(data.loginErrorKind); + setErrorCode(LoginError.ACCOUNT_NOT_FOUND); } else { onError(); } @@ -180,7 +185,7 @@ export default function SignPage() { function onError() { setChallenging(false); - setErrorCode("UNKNOWN"); + setErrorCode(LoginError.UNKNOWN); setToken(undefined); } @@ -193,7 +198,7 @@ export default function SignPage() { } switch (currentErrorCode) { - case "ACCOUNT_NOT_FOUND": + case LoginError.ACCOUNT_NOT_FOUND: return t`No such account in Hackers' Pub—please try again.`; case undefined: case null: From d44ab93cd1230c5ca840f203b4b4e3ab40d339ee Mon Sep 17 00:00:00 2001 From: Gyusun Yeom Date: Tue, 19 Aug 2025 20:42:36 +0900 Subject: [PATCH 6/7] Fix naming & remove wrapper --- graphql/builder.ts | 10 ++++++++++ graphql/login.ts | 6 ++++++ graphql/post.ts | 3 --- graphql/schema.graphql | 18 +++++------------ web-next/src/routes/sign/index.tsx | 32 +++++++++++++----------------- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/graphql/builder.ts b/graphql/builder.ts index 35a8153e..3d543c15 100644 --- a/graphql/builder.ts +++ b/graphql/builder.ts @@ -153,6 +153,16 @@ export const builder = new SchemaBuilder({ relay: { clientMutationId: "optional", }, + errors: { + directResult: true, + defaultUnionOptions: { + name(options) { + return `${options.fieldName.charAt(0).toUpperCase()}${ + options.fieldName.slice(1) + }Result`; + }, + }, + }, }); builder.addScalarType("Date", DateResolver); diff --git a/graphql/login.ts b/graphql/login.ts index c0730866..b2dad329 100644 --- a/graphql/login.ts +++ b/graphql/login.ts @@ -69,6 +69,9 @@ builder.mutationFields((t) => ({ type: LoginChallengeRef, errors: { types: [AccountNotFoundError], + union: { + name: "LoginResult", + }, result: { name: "LoginSuccess", }, @@ -132,6 +135,9 @@ builder.mutationFields((t) => ({ type: LoginChallengeRef, errors: { types: [AccountNotFoundError], + union: { + name: "LoginResult", + }, result: { name: "LoginSuccess", }, diff --git a/graphql/post.ts b/graphql/post.ts index 42e2d3ba..a08eb86c 100644 --- a/graphql/post.ts +++ b/graphql/post.ts @@ -477,9 +477,6 @@ builder.relayMutationField( NotAuthenticatedError, InvalidInputError, ], - result: { - name: "CreateNoteSuccess", - }, }, async resolve(_root, args, ctx) { const session = await ctx.session; diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 55b1e8cc..26284cf5 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -288,9 +288,7 @@ type CreateNotePayload { note: Note! } -type CreateNoteSuccess { - data: CreateNotePayload! -} +union CreateNoteResult = CreateNotePayload | InvalidInputError | NotAuthenticatedError type CustomEmoji implements Node { id: ID! @@ -387,9 +385,7 @@ type LoginChallenge { token: UUID! } -type LoginSuccess { - data: LoginChallenge! -} +union LoginResult = AccountNotFoundError | LoginChallenge """A Hackers' Pub-flavored Markdown text.""" scalar Markdown @@ -425,7 +421,7 @@ type Mutation { """The signup token.""" token: UUID! ): SignupResult! - createNote(input: CreateNoteInput!): MutationCreateNoteResult! + createNote(input: CreateNoteInput!): CreateNoteResult! loginByEmail( """The email of the account to sign in.""" email: String! @@ -437,7 +433,7 @@ type Mutation { The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. """ verifyUrl: URITemplate! - ): MutationLoginByUsernameResult! + ): LoginResult! loginByUsername( """The locale for the sign-in email.""" locale: Locale! @@ -449,7 +445,7 @@ type Mutation { The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. """ verifyUrl: URITemplate! - ): MutationLoginByUsernameResult! + ): LoginResult! """Revoke a session by its ID.""" revokeSession( @@ -459,10 +455,6 @@ type Mutation { updateAccount(input: UpdateAccountInput!): UpdateAccountPayload! } -union MutationCreateNoteResult = CreateNoteSuccess | InvalidInputError | NotAuthenticatedError - -union MutationLoginByUsernameResult = AccountNotFoundError | LoginSuccess - interface Node { id: ID! } diff --git a/web-next/src/routes/sign/index.tsx b/web-next/src/routes/sign/index.tsx index aa9c76ea..3a057819 100644 --- a/web-next/src/routes/sign/index.tsx +++ b/web-next/src/routes/sign/index.tsx @@ -32,16 +32,14 @@ import type { signCompleteMutation } from "./__generated__/signCompleteMutation. const signByEmailMutation = graphql` mutation signByEmailMutation($locale: Locale!, $email: String!, $verifyUrl: URITemplate!) { loginByEmail(locale: $locale, email: $email, verifyUrl: $verifyUrl) { - ... on LoginSuccess { + ... on LoginChallenge { __typename - data { - account { - name - handle - avatarUrl - } - token + account { + name + handle + avatarUrl } + token } ... on AccountNotFoundError { __typename @@ -53,16 +51,14 @@ const signByEmailMutation = graphql` const signByUsernameMutation = graphql` mutation signByUsernameMutation($locale: Locale!, $username: String!, $verifyUrl: URITemplate!) { loginByUsername(locale: $locale, username: $username, verifyUrl: $verifyUrl) { - ... on LoginSuccess { + ... on LoginChallenge { __typename - data { - account { - name - handle - avatarUrl - } - token + account { + name + handle + avatarUrl } + token } ... on AccountNotFoundError { __typename @@ -171,8 +167,8 @@ export default function SignPage() { function onCompleted(data: signByUsernameMutation$data["loginByUsername"]) { setChallenging(false); - if (data.__typename === "LoginSuccess") { - setToken(data.data.token); + if (data.__typename === "LoginChallenge") { + setToken(data.token); codeInput?.focus(); } else if ( data.__typename === "AccountNotFoundError" From b124ed45e8a124928db701258c3fa4af50d4e6f5 Mon Sep 17 00:00:00 2001 From: Gyusun Yeom Date: Tue, 19 Aug 2025 20:48:47 +0900 Subject: [PATCH 7/7] regenerate translations --- web-next/src/locales/en-US/messages.po | 22 +++++++++++----------- web-next/src/locales/ja-JP/messages.po | 22 +++++++++++----------- web-next/src/locales/ko-KR/messages.po | 22 +++++++++++----------- web-next/src/locales/zh-CN/messages.po | 22 +++++++++++----------- web-next/src/locales/zh-TW/messages.po | 22 +++++++++++----------- 5 files changed, 55 insertions(+), 55 deletions(-) diff --git a/web-next/src/locales/en-US/messages.po b/web-next/src/locales/en-US/messages.po index 9117af66..f473826b 100644 --- a/web-next/src/locales/en-US/messages.po +++ b/web-next/src/locales/en-US/messages.po @@ -128,7 +128,7 @@ msgstr "{0}'s notes" msgid "{0}'s shares" msgstr "{0}'s shares" -#: src/routes/sign/index.tsx:180 +#: src/routes/sign/index.tsx:193 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." @@ -172,7 +172,7 @@ msgstr "Creating account…" msgid "Display name" msgstr "Display name" -#: src/routes/sign/index.tsx:272 +#: src/routes/sign/index.tsx:285 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." @@ -180,11 +180,11 @@ msgstr "Do you need an account? Hackers' Pub is invite-only—please ask a frien msgid "Email address" msgstr "Email address" -#: src/routes/sign/index.tsx:250 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "Email or username" -#: src/routes/sign/index.tsx:188 +#: src/routes/sign/index.tsx:201 msgid "Enter your email or username below to sign in." msgstr "Enter your email or username below to sign in." @@ -246,7 +246,7 @@ msgstr "GitHub repository" #: src/components/AppSidebar.tsx:151 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/markdown.tsx:53 -#: src/routes/sign.tsx:19 +#: src/routes/sign.tsx:14 msgid "Hackers' Pub" msgstr "Hackers' Pub" @@ -346,7 +346,7 @@ msgstr "No posts found" #~ msgid "No posts found." #~ msgstr "No posts found." -#: src/routes/sign/index.tsx:185 +#: src/routes/sign/index.tsx:198 msgid "No such account in Hackers' Pub—please try again." msgstr "No such account in Hackers' Pub—please try again." @@ -355,7 +355,7 @@ msgstr "No such account in Hackers' Pub—please try again." msgid "Notes" msgstr "Notes" -#: src/routes/sign/index.tsx:283 +#: src/routes/sign/index.tsx:296 msgid "Or enter the code from the email" msgstr "Or enter the code from the email" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "Shares" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "Sign in" @@ -401,11 +401,11 @@ msgstr "Sign out" msgid "Sign up" msgstr "Sign up" -#: src/routes/sign/index.tsx:233 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "Signing in Hackers' Pub" -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "Signing in…" @@ -413,7 +413,7 @@ msgstr "Signing in…" msgid "Signing up for Hackers' Pub" msgstr "Signing up for Hackers' Pub" -#: src/routes/sign/index.tsx:190 +#: src/routes/sign/index.tsx:203 msgid "Something went wrong—please try again." msgstr "Something went wrong—please try again." diff --git a/web-next/src/locales/ja-JP/messages.po b/web-next/src/locales/ja-JP/messages.po index 631a1c7f..a6b5e025 100644 --- a/web-next/src/locales/ja-JP/messages.po +++ b/web-next/src/locales/ja-JP/messages.po @@ -128,7 +128,7 @@ msgstr "{0}さんの投稿" msgid "{0}'s shares" msgstr "{0}さんの共有" -#: src/routes/sign/index.tsx:180 +#: src/routes/sign/index.tsx:193 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "ログインリンクがメールに送信されました。受信トレイ(または迷惑メールフォルダ)を確認してください。" @@ -172,7 +172,7 @@ msgstr "アカウントを作成中…" msgid "Display name" msgstr "名前" -#: src/routes/sign/index.tsx:272 +#: src/routes/sign/index.tsx:285 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので、友人に招待をお願いしてください。" @@ -180,11 +180,11 @@ msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので msgid "Email address" msgstr "メールアドレス" -#: src/routes/sign/index.tsx:250 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "メールアドレスまたはユーザー名" -#: src/routes/sign/index.tsx:188 +#: src/routes/sign/index.tsx:201 msgid "Enter your email or username below to sign in." msgstr "以下にメールアドレスまたはユーザー名を入力してログインしてください。" @@ -246,7 +246,7 @@ msgstr "GitHubリポジトリ" #: src/components/AppSidebar.tsx:151 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/markdown.tsx:53 -#: src/routes/sign.tsx:19 +#: src/routes/sign.tsx:14 msgid "Hackers' Pub" msgstr "Hackers' Pub" @@ -346,7 +346,7 @@ msgstr "コンテンツはありません" #~ msgid "No posts found." #~ msgstr "コンテンツが見つかりませんでした。" -#: src/routes/sign/index.tsx:185 +#: src/routes/sign/index.tsx:198 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pubにそのようなアカウントはありません。もう一度お試しください。" @@ -355,7 +355,7 @@ msgstr "Hackers' Pubにそのようなアカウントはありません。もう msgid "Notes" msgstr "投稿" -#: src/routes/sign/index.tsx:283 +#: src/routes/sign/index.tsx:296 msgid "Or enter the code from the email" msgstr "またはメールのコードを入力してください" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "共有" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "ログイン" @@ -401,11 +401,11 @@ msgstr "ログアウト" msgid "Sign up" msgstr "登録" -#: src/routes/sign/index.tsx:233 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "Hackers' Pubにログイン" -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "ログイン中…" @@ -413,7 +413,7 @@ msgstr "ログイン中…" msgid "Signing up for Hackers' Pub" msgstr "Hackers' Pubに登録" -#: src/routes/sign/index.tsx:190 +#: src/routes/sign/index.tsx:203 msgid "Something went wrong—please try again." msgstr "問題が発生しました。再度お試しください。" diff --git a/web-next/src/locales/ko-KR/messages.po b/web-next/src/locales/ko-KR/messages.po index 372659c0..a7654029 100644 --- a/web-next/src/locales/ko-KR/messages.po +++ b/web-next/src/locales/ko-KR/messages.po @@ -128,7 +128,7 @@ msgstr "{0} 님의 단문" msgid "{0}'s shares" msgstr "{0} 님의 공유" -#: src/routes/sign/index.tsx:180 +#: src/routes/sign/index.tsx:193 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "로그인 링크가 이메일로 전송되었습니다. 받은 편지함(또는 스팸 폴더)을 확인해주세요." @@ -172,7 +172,7 @@ msgstr "계정을 생성하는 중…" msgid "Display name" msgstr "이름" -#: src/routes/sign/index.tsx:272 +#: src/routes/sign/index.tsx:285 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "계정이 필요하신가요? Hackers' Pub은 초대 전용입니다. 친구에게 초대를 요청해주세요." @@ -180,11 +180,11 @@ msgstr "계정이 필요하신가요? Hackers' Pub은 초대 전용입니다. msgid "Email address" msgstr "이메일 주소" -#: src/routes/sign/index.tsx:250 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "이메일 또는 아이디" -#: src/routes/sign/index.tsx:188 +#: src/routes/sign/index.tsx:201 msgid "Enter your email or username below to sign in." msgstr "로그인하려면 아래에 이메일 또는 아이디를 입력해주세요." @@ -246,7 +246,7 @@ msgstr "GitHub 저장소" #: src/components/AppSidebar.tsx:151 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/markdown.tsx:53 -#: src/routes/sign.tsx:19 +#: src/routes/sign.tsx:14 msgid "Hackers' Pub" msgstr "Hackers' Pub" @@ -346,7 +346,7 @@ msgstr "콘텐츠가 없습니다" #~ msgid "No posts found." #~ msgstr "콘텐츠를 찾을 수 없습니다." -#: src/routes/sign/index.tsx:185 +#: src/routes/sign/index.tsx:198 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub에 해당 계정이 없습니다. 다시 시도해주세요." @@ -355,7 +355,7 @@ msgstr "Hackers' Pub에 해당 계정이 없습니다. 다시 시도해주세요 msgid "Notes" msgstr "단문" -#: src/routes/sign/index.tsx:283 +#: src/routes/sign/index.tsx:296 msgid "Or enter the code from the email" msgstr "또는 이메일의 코드를 입력하세요" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "공유" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "로그인" @@ -401,11 +401,11 @@ msgstr "로그아웃" msgid "Sign up" msgstr "가입" -#: src/routes/sign/index.tsx:233 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "Hackers' Pub에 로그인" -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "로그인 중…" @@ -413,7 +413,7 @@ msgstr "로그인 중…" msgid "Signing up for Hackers' Pub" msgstr "Hackers' Pub 가입" -#: src/routes/sign/index.tsx:190 +#: src/routes/sign/index.tsx:203 msgid "Something went wrong—please try again." msgstr "문제가 발생했습니다. 다시 시도해주세요." diff --git a/web-next/src/locales/zh-CN/messages.po b/web-next/src/locales/zh-CN/messages.po index c3de89f8..fd2a542e 100644 --- a/web-next/src/locales/zh-CN/messages.po +++ b/web-next/src/locales/zh-CN/messages.po @@ -128,7 +128,7 @@ msgstr "{0}的帖子" msgid "{0}'s shares" msgstr "{0}的转帖" -#: src/routes/sign/index.tsx:180 +#: src/routes/sign/index.tsx:193 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "登录链接已发送至您的邮箱。请检查收件箱。(或垃圾邮件文件夹)" @@ -172,7 +172,7 @@ msgstr "正在创建账户…" msgid "Display name" msgstr "昵称" -#: src/routes/sign/index.tsx:272 +#: src/routes/sign/index.tsx:285 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "需要创建账户吗?Hackers' Pub 仅限邀请,请联系朋友邀请您。" @@ -180,11 +180,11 @@ msgstr "需要创建账户吗?Hackers' Pub 仅限邀请,请联系朋友邀 msgid "Email address" msgstr "电子邮件地址" -#: src/routes/sign/index.tsx:250 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "邮箱或用户名" -#: src/routes/sign/index.tsx:188 +#: src/routes/sign/index.tsx:201 msgid "Enter your email or username below to sign in." msgstr "请在下方输入您的邮箱或用户名以登录。" @@ -246,7 +246,7 @@ msgstr "GitHub 仓库" #: src/components/AppSidebar.tsx:151 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/markdown.tsx:53 -#: src/routes/sign.tsx:19 +#: src/routes/sign.tsx:14 msgid "Hackers' Pub" msgstr "Hackers' Pub" @@ -346,7 +346,7 @@ msgstr "未找到内容" #~ msgid "No posts found." #~ msgstr "未找到内容。" -#: src/routes/sign/index.tsx:185 +#: src/routes/sign/index.tsx:198 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub 中无此账户,请重试。" @@ -355,7 +355,7 @@ msgstr "Hackers' Pub 中无此账户,请重试。" msgid "Notes" msgstr "帖子" -#: src/routes/sign/index.tsx:283 +#: src/routes/sign/index.tsx:296 msgid "Or enter the code from the email" msgstr "或输入邮件中的验证码" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "转帖" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "登录" @@ -401,11 +401,11 @@ msgstr "登出" msgid "Sign up" msgstr "注册" -#: src/routes/sign/index.tsx:233 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "登录 Hackers' Pub" -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "登录中…" @@ -413,7 +413,7 @@ msgstr "登录中…" msgid "Signing up for Hackers' Pub" msgstr "注册 Hackers' Pub" -#: src/routes/sign/index.tsx:190 +#: src/routes/sign/index.tsx:203 msgid "Something went wrong—please try again." msgstr "出现错误,请重试。" diff --git a/web-next/src/locales/zh-TW/messages.po b/web-next/src/locales/zh-TW/messages.po index b3696da6..48c7cdd8 100644 --- a/web-next/src/locales/zh-TW/messages.po +++ b/web-next/src/locales/zh-TW/messages.po @@ -128,7 +128,7 @@ msgstr "{0}的貼文" msgid "{0}'s shares" msgstr "{0}的轉貼" -#: src/routes/sign/index.tsx:180 +#: src/routes/sign/index.tsx:193 msgid "A sign-in link has been sent to your email. Please check your inbox (or spam folder)." msgstr "登入連結已傳送至您的郵箱。請檢查收件匣。(或垃圾郵件資料夾)" @@ -172,7 +172,7 @@ msgstr "創建帳戶…" msgid "Display name" msgstr "暱稱" -#: src/routes/sign/index.tsx:272 +#: src/routes/sign/index.tsx:285 msgid "Do you need an account? Hackers' Pub is invite-only—please ask a friend to invite you." msgstr "需要建立帳戶嗎?Hackers' Pub 僅限邀請,請聯繫朋友邀請您。" @@ -180,11 +180,11 @@ msgstr "需要建立帳戶嗎?Hackers' Pub 僅限邀請,請聯繫朋友邀 msgid "Email address" msgstr "電子郵件地址" -#: src/routes/sign/index.tsx:250 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "電子郵件或使用者名稱" -#: src/routes/sign/index.tsx:188 +#: src/routes/sign/index.tsx:201 msgid "Enter your email or username below to sign in." msgstr "請在下方輸入您的電子郵件或使用者名稱以登入。" @@ -246,7 +246,7 @@ msgstr "GitHub 儲存庫" #: src/components/AppSidebar.tsx:151 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/markdown.tsx:53 -#: src/routes/sign.tsx:19 +#: src/routes/sign.tsx:14 msgid "Hackers' Pub" msgstr "Hackers' Pub" @@ -346,7 +346,7 @@ msgstr "未找到內容" #~ msgid "No posts found." #~ msgstr "未找到內容。" -#: src/routes/sign/index.tsx:185 +#: src/routes/sign/index.tsx:198 msgid "No such account in Hackers' Pub—please try again." msgstr "Hackers' Pub 中無此帳戶,請重試。" @@ -355,7 +355,7 @@ msgstr "Hackers' Pub 中無此帳戶,請重試。" msgid "Notes" msgstr "貼文" -#: src/routes/sign/index.tsx:283 +#: src/routes/sign/index.tsx:296 msgid "Or enter the code from the email" msgstr "或輸入郵件中的驗證碼" @@ -389,7 +389,7 @@ msgid "Shares" msgstr "轉貼" #: src/components/AppSidebar.tsx:241 -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "登入" @@ -401,11 +401,11 @@ msgstr "登出" msgid "Sign up" msgstr "註冊" -#: src/routes/sign/index.tsx:233 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "登入 Hackers' Pub" -#: src/routes/sign/index.tsx:266 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "登入中…" @@ -413,7 +413,7 @@ msgstr "登入中…" msgid "Signing up for Hackers' Pub" msgstr "註冊 Hackers' Pub" -#: src/routes/sign/index.tsx:190 +#: src/routes/sign/index.tsx:203 msgid "Something went wrong—please try again." msgstr "發生錯誤,請重試。"