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..3d543c15 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, @@ -148,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 49c0aeee..b2dad329 100644 --- a/graphql/login.ts +++ b/graphql/login.ts @@ -20,11 +20,24 @@ 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 { EMAIL_FROM } from "./email.ts"; +import { SessionRef } from "./session.ts"; const logger = getLogger(["hackerspub", "graphql", "login"]); +class AccountNotFoundError extends Error { + public constructor(public readonly query: string) { + super(`Account not found`); + } +} + +builder.objectType(AccountNotFoundError, { + name: "AccountNotFoundError", + fields: (t) => ({ + query: t.exposeString("query"), + }), +}); + interface LoginChallenge { accountId: Uuid; token: Uuid; @@ -54,7 +67,15 @@ LoginChallengeRef.implement({ builder.mutationFields((t) => ({ loginByUsername: t.field({ type: LoginChallengeRef, - nullable: true, + errors: { + types: [AccountNotFoundError], + union: { + name: "LoginResult", + }, + result: { + name: "LoginSuccess", + }, + }, args: { username: t.arg.string({ required: true, @@ -80,7 +101,9 @@ builder.mutationFields((t) => ({ with: { emails: true }, where: { username: args.username }, }); - if (account == null) return null; + if (account == null) { + throw new AccountNotFoundError(args.username); + } const token = await createSigninToken(ctx.kv, account.id); const messages: Message[] = []; for (const { email } of account.emails) { @@ -110,7 +133,15 @@ builder.mutationFields((t) => ({ loginByEmail: t.field({ type: LoginChallengeRef, - nullable: true, + errors: { + types: [AccountNotFoundError], + union: { + name: "LoginResult", + }, + result: { + name: "LoginSuccess", + }, + }, args: { email: t.arg.string({ required: true, @@ -150,7 +181,9 @@ builder.mutationFields((t) => ({ with: { emails: true }, }); } - if (account == null) return null; + if (account == null) { + throw new AccountNotFoundError(args.email); + } const token = await createSigninToken(ctx.kv, account.id); const messages: Message[] = []; for (const { email } of account.emails) { diff --git a/graphql/post.ts b/graphql/post.ts index d40755f8..a08eb86c 100644 --- a/graphql/post.ts +++ b/graphql/post.ts @@ -11,6 +11,7 @@ import { Account } from "./account.ts"; import { Actor } from "./actor.ts"; import { builder, Node } from "./builder.ts"; import { Reactable } from "./reactable.ts"; +import { NotAuthenticatedError } from "./session.ts"; const PostVisibility = builder.enumType("PostVisibility", { values: [ @@ -22,6 +23,19 @@ const PostVisibility = builder.enumType("PostVisibility", { ] as const, }); +class InvalidInputError extends Error { + public constructor(public readonly inputPath: string) { + super(`Invalid input - ${inputPath}`); + } +} + +builder.objectType(InvalidInputError, { + name: "InvalidInputError", + fields: (t) => ({ + inputPath: t.expose("inputPath", { type: "String" }), + }), +}); + export const Post = builder.drizzleInterface("postTable", { variant: "Post", interfaces: [Reactable, Node], @@ -458,10 +472,16 @@ builder.relayMutationField( }), }, { + errors: { + types: [ + NotAuthenticatedError, + InvalidInputError, + ], + }, async resolve(_root, args, ctx) { const session = await ctx.session; if (session == null) { - throw new Error("Not authenticated."); + throw new NotAuthenticatedError(); } const { visibility, content, language, replyTargetId, quotedPostId } = args.input; @@ -472,7 +492,7 @@ builder.relayMutationField( where: { id: replyTargetId.id }, }); if (replyTarget == null) { - throw new Error("Reply target not found."); + throw new InvalidInputError("replyTargetId"); } } let quotedPost: schema.Post & { actor: schema.Actor } | undefined; @@ -482,7 +502,7 @@ builder.relayMutationField( where: { id: quotedPostId.id }, }); if (quotedPost == null) { - throw new Error("Quoted post not found."); + throw new InvalidInputError("quotedPostId"); } } return await withTransaction(ctx.fedCtx, async (context) => { @@ -511,7 +531,7 @@ builder.relayMutationField( { replyTarget, quotedPost }, ); if (note == null) { - throw new Error("Failed to create note."); + throw new Error("Failed to create note"); } return note; }); diff --git a/graphql/schema.graphql b/graphql/schema.graphql index a66b140a..26284cf5 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! @@ -284,6 +288,8 @@ type CreateNotePayload { note: Note! } +union CreateNoteResult = CreateNotePayload | InvalidInputError | NotAuthenticatedError + type CustomEmoji implements Node { id: ID! imageUrl: String! @@ -360,6 +366,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). """ @@ -375,6 +385,8 @@ type LoginChallenge { token: UUID! } +union LoginResult = AccountNotFoundError | LoginChallenge + """A Hackers' Pub-flavored Markdown text.""" scalar Markdown @@ -409,7 +421,7 @@ type Mutation { """The signup token.""" token: UUID! ): SignupResult! - createNote(input: CreateNoteInput!): CreateNotePayload! + createNote(input: CreateNoteInput!): CreateNoteResult! loginByEmail( """The email of the account to sign in.""" email: String! @@ -421,7 +433,7 @@ type Mutation { The RFC 6570-compliant URI Template for the verification link. Available variabvles: `{token}` and `{code}`. """ verifyUrl: URITemplate! - ): LoginChallenge + ): LoginResult! 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 + ): LoginResult! """Revoke a session by its ID.""" revokeSession( @@ -447,6 +459,10 @@ 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/locales/en-US/messages.po b/web-next/src/locales/en-US/messages.po index b98ecf81..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:194 +#: 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:230 +#: 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:208 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "Email or username" -#: src/routes/sign/index.tsx:193 +#: 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:192 +#: 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:241 +#: 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:224 +#: 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:187 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "Signing in Hackers' Pub" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:279 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:203 +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..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:194 +#: 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:230 +#: 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:208 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "メールアドレスまたはユーザー名" -#: src/routes/sign/index.tsx:193 +#: 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:192 +#: 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:241 +#: 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:224 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "ログイン" @@ -401,11 +401,11 @@ msgstr "ログアウト" msgid "Sign up" msgstr "登録" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "Hackers' Pubにログイン" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "ログイン中…" @@ -413,6 +413,10 @@ msgstr "ログイン中…" msgid "Signing up for Hackers' Pub" msgstr "Hackers' Pubに登録" +#: src/routes/sign/index.tsx:203 +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..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:194 +#: 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:230 +#: 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:208 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "이메일 또는 아이디" -#: src/routes/sign/index.tsx:193 +#: 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:192 +#: 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:241 +#: 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:224 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "로그인" @@ -401,11 +401,11 @@ msgstr "로그아웃" msgid "Sign up" msgstr "가입" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "Hackers' Pub에 로그인" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "로그인 중…" @@ -413,6 +413,10 @@ msgstr "로그인 중…" msgid "Signing up for Hackers' Pub" msgstr "Hackers' Pub 가입" +#: src/routes/sign/index.tsx:203 +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..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:194 +#: 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:230 +#: 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:208 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "邮箱或用户名" -#: src/routes/sign/index.tsx:193 +#: 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:192 +#: 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:241 +#: 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:224 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "登录" @@ -401,11 +401,11 @@ msgstr "登出" msgid "Sign up" msgstr "注册" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "登录 Hackers' Pub" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "登录中…" @@ -413,6 +413,10 @@ msgstr "登录中…" msgid "Signing up for Hackers' Pub" msgstr "注册 Hackers' Pub" +#: src/routes/sign/index.tsx:203 +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..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:194 +#: 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:230 +#: 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:208 +#: src/routes/sign/index.tsx:263 msgid "Email or username" msgstr "電子郵件或使用者名稱" -#: src/routes/sign/index.tsx:193 +#: 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:192 +#: 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:241 +#: 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:224 +#: src/routes/sign/index.tsx:279 msgid "Sign in" msgstr "登入" @@ -401,11 +401,11 @@ msgstr "登出" msgid "Sign up" msgstr "註冊" -#: src/routes/sign/index.tsx:187 +#: src/routes/sign/index.tsx:246 msgid "Signing in Hackers' Pub" msgstr "登入 Hackers' Pub" -#: src/routes/sign/index.tsx:224 +#: src/routes/sign/index.tsx:279 msgid "Signing in…" msgstr "登入中…" @@ -413,6 +413,10 @@ msgstr "登入中…" msgid "Signing up for Hackers' Pub" msgstr "註冊 Hackers' Pub" +#: src/routes/sign/index.tsx:203 +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..3a057819 100644 --- a/web-next/src/routes/sign/index.tsx +++ b/web-next/src/routes/sign/index.tsx @@ -20,7 +20,9 @@ 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 { + signByEmailMutation, +} from "./__generated__/signByEmailMutation.graphql.ts"; import type { signByUsernameMutation, signByUsernameMutation$data, @@ -30,12 +32,18 @@ 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 + ... on LoginChallenge { + __typename + account { + name + handle + avatarUrl + } + token + } + ... on AccountNotFoundError { + __typename } - token } } `; @@ -43,12 +51,18 @@ 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 + ... on LoginChallenge { + __typename + account { + name + handle + avatarUrl + } + token + } + ... on AccountNotFoundError { + __typename } - token } } `; @@ -74,13 +88,23 @@ 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; let codeInput: HTMLInputElement | undefined; const [challenging, setChallenging] = createSignal(false); const [email, setEmail] = createSignal(""); - const [invalid, setInvalid] = createSignal(false); + const [errorCode, setErrorCode] = createSignal< + | LoginError + | undefined + >( + undefined, + ); const [token, setToken] = createSignal(undefined); const [loginByEmail] = createMutation( signByEmailMutation, @@ -96,7 +120,7 @@ export default function SignPage() { function onInput() { if (emailInput == null) return; setEmail(emailInput.value.trim()); - setInvalid(false); + setErrorCode(undefined); } function onChallengeSubmit(event: SubmitEvent) { @@ -120,6 +144,9 @@ export default function SignPage() { onCompleted(response) { onCompleted(response.loginByEmail); }, + onError(_error) { + onError(); + }, }); } else { loginByUsername({ @@ -131,17 +158,49 @@ export default function SignPage() { onCompleted(response) { onCompleted(response.loginByUsername); }, + onError(_error) { + onError(); + }, }); } } function onCompleted(data: signByUsernameMutation$data["loginByUsername"]) { setChallenging(false); - if (data == null) { - setInvalid(true); - } else { + if (data.__typename === "LoginChallenge") { setToken(data.token); codeInput?.focus(); + } else if ( + data.__typename === "AccountNotFoundError" + ) { + setErrorCode(LoginError.ACCOUNT_NOT_FOUND); + } else { + onError(); + } + } + + function onError() { + setChallenging(false); + setErrorCode(LoginError.UNKNOWN); + setToken(undefined); + } + + 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 LoginError.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 +246,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 +256,7 @@ export default function SignPage() { >