From 1fd89f8e727e91be9ea85f8e9b8cd76169cdc362 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sat, 27 Jun 2026 13:37:42 +0900 Subject: [PATCH 1/6] Allow organization admins to manage account migration aliases Account migration (advertising `alsoKnownAs` aliases so an account can be the destination of a Mastodon-style Move) was authorized only for the signed-in account itself. Since organizations cannot sign in, their accepted admins could never manage the organization's migration aliases. Make `getAuthorizedMigrationAccount` reuse `viewerCanManageAccountSettings`, which already authorizes the personal account holder and accepted admins of an organization account, matching how `updateAccount` and `deleteAccount` are gated. A nonexistent account now returns `NotAuthorizedError` (rather than leaking existence via `InvalidInputError`), consistent with the mutation input descriptions. Assisted-by: Claude Code:claude-opus-4-8 Assisted-by: Codex:gpt-5.5 --- graphql/account.test.ts | 178 ++++++++++++++++++++++++++++++++++++++++ graphql/account.ts | 30 +++++-- graphql/schema.graphql | 4 +- 3 files changed, 203 insertions(+), 9 deletions(-) diff --git a/graphql/account.test.ts b/graphql/account.test.ts index 8e5a47f7..2c9dc8ef 100644 --- a/graphql/account.test.ts +++ b/graphql/account.test.ts @@ -1676,6 +1676,31 @@ test("addAccountMigrationAlias returns typed errors", async () => { "NotAuthorizedError", ); + // A well-formed but nonexistent account id is also rejected with + // NotAuthorizedError, so the error does not leak whether the id exists. + const missing = await execute({ + schema, + document: addAccountMigrationAliasMutation, + variableValues: { + input: { + accountId: encodeGlobalID( + "Account", + "00000000-0000-4000-8000-000000000000", + ), + actor: "@old@remote.example", + }, + }, + contextValue: makeUserContext(tx, owner.account), + onError: "NO_PROPAGATE", + }); + assert.equal(missing.errors, undefined); + assert.equal( + (toPlainJson(missing.data) as { + addAccountMigrationAlias?: { __typename?: string }; + }).addAccountMigrationAlias?.__typename, + "NotAuthorizedError", + ); + const invalid = await execute({ schema, document: addAccountMigrationAliasMutation, @@ -1778,6 +1803,159 @@ test("removeAccountMigrationAlias removes one alias idempotently", async () => { }); }); +test("account migration aliases are manageable by organization admins", async () => { + await withRollback(async (tx) => { + const admin = await insertAccountWithActor(tx, { + username: "orgmigadmin", + name: "Org Migration Admin", + email: "orgmigadmin@example.com", + }); + const member = await insertAccountWithActor(tx, { + username: "orgmigmember", + name: "Org Migration Member", + email: "orgmigmember@example.com", + }); + const outsider = await insertAccountWithActor(tx, { + username: "orgmigoutsider", + name: "Org Migration Outsider", + email: "orgmigoutsider@example.com", + }); + const organization = await insertAccountWithActor(tx, { + username: "orgmigration", + name: "Org Migration", + email: "orgmigration@example.com", + kind: "organization", + type: "Organization", + }); + await tx.insert(organizationMembershipTable).values([ + { + organizationAccountId: organization.account.id, + memberAccountId: admin.account.id, + role: "admin", + invitedById: admin.account.id, + accepted: new Date("2026-04-15T00:00:00.000Z"), + }, + { + organizationAccountId: organization.account.id, + memberAccountId: member.account.id, + role: "member", + invitedById: admin.account.id, + accepted: new Date("2026-04-15T00:00:00.000Z"), + }, + ]); + await insertRemoteActor(tx, { + username: "orgoldalias", + name: "Org Old Alias", + host: "old.example", + iri: "https://old.example/users/orgoldalias", + url: "https://old.example/@orgoldalias", + }); + + const fedCtx = createFedCtx(tx); + fedCtx.getActor = (identifier: string) => + Promise.resolve( + new vocab.Organization({ id: fedCtx.getActorUri(identifier) }), + ); + const sentActivities: unknown[][] = []; + fedCtx.sendActivity = ((...args: unknown[]) => { + sentActivities.push(args); + return Promise.resolve(undefined); + }) as typeof fedCtx.sendActivity; + + // An accepted admin can add an alias to the organization account. + const added = await execute({ + schema, + document: addAccountMigrationAliasMutation, + variableValues: { + input: { + accountId: encodeGlobalID("Account", organization.account.id), + actor: "@orgoldalias@old.example", + }, + }, + contextValue: makeUserContext(tx, admin.account, { fedCtx }), + onError: "NO_PROPAGATE", + }); + assert.equal(added.errors, undefined); + assert.deepEqual(toPlainJson(added.data), { + addAccountMigrationAlias: { + __typename: "AddAccountMigrationAliasPayload", + account: { + actor: { + aliases: ["https://old.example/users/orgoldalias"], + }, + }, + }, + }); + + // ... and remove it again. + const removed = await execute({ + schema, + document: removeAccountMigrationAliasMutation, + variableValues: { + input: { + accountId: encodeGlobalID("Account", organization.account.id), + alias: "https://old.example/users/orgoldalias", + }, + }, + contextValue: makeUserContext(tx, admin.account, { fedCtx }), + onError: "NO_PROPAGATE", + }); + assert.equal(removed.errors, undefined); + assert.deepEqual(toPlainJson(removed.data), { + removeAccountMigrationAlias: { + __typename: "RemoveAccountMigrationAliasPayload", + account: { + actor: { + aliases: [], + }, + }, + }, + }); + + // A non-admin member cannot manage the organization's migration aliases. + const byMember = await execute({ + schema, + document: addAccountMigrationAliasMutation, + variableValues: { + input: { + accountId: encodeGlobalID("Account", organization.account.id), + actor: "@orgoldalias@old.example", + }, + }, + contextValue: makeUserContext(tx, member.account, { fedCtx }), + onError: "NO_PROPAGATE", + }); + assert.equal(byMember.errors, undefined); + assert.equal( + (toPlainJson(byMember.data) as { + addAccountMigrationAlias?: { __typename?: string }; + }).addAccountMigrationAlias?.__typename, + "NotAuthorizedError", + ); + + // Neither can an account that is not a member at all. + const byOutsider = await execute({ + schema, + document: addAccountMigrationAliasMutation, + variableValues: { + input: { + accountId: encodeGlobalID("Account", organization.account.id), + actor: "@orgoldalias@old.example", + }, + }, + contextValue: makeUserContext(tx, outsider.account, { fedCtx }), + onError: "NO_PROPAGATE", + }); + assert.equal(byOutsider.errors, undefined); + assert.equal( + (toPlainJson(byOutsider.data) as { + addAccountMigrationAlias?: { __typename?: string }; + }).addAccountMigrationAlias?.__typename, + "NotAuthorizedError", + ); + }); +}); + test("deleteAccount deletes the viewer account and session", async () => { await withRollback(async (tx) => { const { kv } = createTestKv(); diff --git a/graphql/account.ts b/graphql/account.ts index e6813b61..a854ce4a 100644 --- a/graphql/account.ts +++ b/graphql/account.ts @@ -218,12 +218,25 @@ async function getAuthorizedMigrationAccount( throw new NotAuthenticatedError(); } if (!validateUuid(accountId)) throw new InvalidInputError("accountId"); - if (session.accountId !== accountId) throw new NotAuthorizedError(); const account = await ctx.db.query.accountTable.findFirst({ where: { id: accountId }, with: { actor: true }, }); - if (account?.actor == null) throw new InvalidInputError("accountId"); + // Treat a nonexistent account the same as an unauthorized one so the error + // does not leak whether the id exists (the input description promises that + // any account id the viewer cannot manage returns `NotAuthorizedError`). + if ( + account == null || + !(await viewerCanManageAccountSettings( + ctx, + account.id, + account.kind, + session.accountId, + )) + ) { + throw new NotAuthorizedError(); + } + if (account.actor == null) throw new InvalidInputError("accountId"); return account; } @@ -1454,8 +1467,10 @@ builder.relayMutationField( for: Account, required: true, description: - "The `Account` global id owned by the authenticated viewer. " + - "Passing another account id returns `NotAuthorizedError`.", + "The `Account` global id to add the alias to. The authenticated " + + "viewer must be able to manage that account: its personal holder, " + + "or an accepted `admin` of an `ORGANIZATION` account. Any other " + + "account id returns `NotAuthorizedError`.", }), actor: t.string({ required: true, @@ -1514,9 +1529,10 @@ builder.relayMutationField( accountId: t.globalID({ for: Account, required: true, - description: - "The `Account` global id owned by the authenticated viewer. " + - "Passing another account id returns `NotAuthorizedError`.", + description: "The `Account` global id to remove the alias from. The " + + "authenticated viewer must be able to manage that account: its " + + "personal holder, or an accepted `admin` of an `ORGANIZATION` " + + "account. Any other account id returns `NotAuthorizedError`.", }), alias: t.field({ type: "URL", diff --git a/graphql/schema.graphql b/graphql/schema.graphql index f3b7b05a..cb272229 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -876,7 +876,7 @@ Add a previous account actor as an ActivityPub migration alias for the authentic """ input AddAccountMigrationAliasInput { """ - The `Account` global id owned by the authenticated viewer. Passing another account id returns `NotAuthorizedError`. + The `Account` global id to add the alias to. The authenticated viewer must be able to manage that account: its personal holder, or an accepted `admin` of an `ORGANIZATION` account. Any other account id returns `NotAuthorizedError`. """ accountId: ID! @@ -5291,7 +5291,7 @@ Remove one ActivityPub migration alias from the authenticated viewer's local acc """ input RemoveAccountMigrationAliasInput { """ - The `Account` global id owned by the authenticated viewer. Passing another account id returns `NotAuthorizedError`. + The `Account` global id to remove the alias from. The authenticated viewer must be able to manage that account: its personal holder, or an accepted `admin` of an `ORGANIZATION` account. Any other account id returns `NotAuthorizedError`. """ accountId: ID! From 6bddecced055921f5136719b57c64384be934e83 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sat, 27 Jun 2026 13:38:00 +0900 Subject: [PATCH 2/6] web-next: Show account migration card for organization accounts The account settings page rendered the "Account migration" card only in the personal-account branch. Add the same card to the organization branch so an organization's accepted admins can prepare it as a Move destination. It reuses the existing AccountMigrationAliasesForm and the aliases already selected by the page query, so no new query or strings are needed. Assisted-by: Claude Code:claude-opus-4-8 --- .../routes/(root)/[handle]/settings/account.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/web-next/src/routes/(root)/[handle]/settings/account.tsx b/web-next/src/routes/(root)/[handle]/settings/account.tsx index 4f0728ef..6153882f 100644 --- a/web-next/src/routes/(root)/[handle]/settings/account.tsx +++ b/web-next/src/routes/(root)/[handle]/settings/account.tsx @@ -577,6 +577,22 @@ export default function AccountSettingsPage() { account={account} viewerId={data.viewer?.id ?? null} /> + + + + {t`Account migration`} + + + {t`Prepare this account as the destination for a Mastodon-style move.`} + + + + + + {t`Delete organization`} From a32d00471b3ca8118bc918e90399a21b8ff03c92 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sat, 27 Jun 2026 14:15:02 +0900 Subject: [PATCH 3/6] Document organization admins in migration mutation docs The accountId input descriptions already note that organization admins may manage migration aliases, but the mutation-level descriptions for addAccountMigrationAlias and removeAccountMigrationAlias still said the alias belonged to the authenticated viewer's own local account. That left the generated schema docs implying organization-admin calls are unsupported even though the resolvers now allow them. Reword both descriptions to cover any account the viewer can manage and regenerate graphql/schema.graphql. https://github.com/hackers-pub/hackerspub/pull/321#discussion_r3485282081 Assisted-by: Claude Code:claude-opus-4-8 --- graphql/account.ts | 19 +++++++++++-------- graphql/schema.graphql | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/graphql/account.ts b/graphql/account.ts index a854ce4a..4c0294d7 100644 --- a/graphql/account.ts +++ b/graphql/account.ts @@ -1458,10 +1458,12 @@ builder.relayMutationField( "addAccountMigrationAlias", { description: - "Add a previous account actor as an ActivityPub migration alias for " + - "the authenticated viewer's local account. The stored value is the " + - "resolved actor IRI, which is then advertised as `alsoKnownAs` on " + - "the local actor document so a later remote `Move` can be validated.", + "Add a previous account actor as an ActivityPub migration alias for a " + + "local account the authenticated viewer can manage: their own personal " + + "account, or an `ORGANIZATION` account they are an accepted `admin` of. " + + "The stored value is the resolved actor IRI, which is then advertised " + + "as `alsoKnownAs` on the local actor document so a later remote `Move` " + + "can be validated.", inputFields: (t) => ({ accountId: t.globalID({ for: Account, @@ -1521,10 +1523,11 @@ builder.relayMutationField( "removeAccountMigrationAlias", { description: - "Remove one ActivityPub migration alias from the authenticated " + - "viewer's local account. Removing an alias only changes the new " + - "account's `alsoKnownAs` list; it does not send or retract a remote " + - "`Move` activity.", + "Remove one ActivityPub migration alias from a local account the " + + "authenticated viewer can manage: their own personal account, or an " + + "`ORGANIZATION` account they are an accepted `admin` of. Removing an " + + "alias only changes the account's `alsoKnownAs` list; it does not send " + + "or retract a remote `Move` activity.", inputFields: (t) => ({ accountId: t.globalID({ for: Account, diff --git a/graphql/schema.graphql b/graphql/schema.graphql index cb272229..0e64651a 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -872,7 +872,7 @@ type ActorViewerInteractionsConnectionEdge { } """ -Add a previous account actor as an ActivityPub migration alias for the authenticated viewer's local account. The stored value is the resolved actor IRI, which is then advertised as `alsoKnownAs` on the local actor document so a later remote `Move` can be validated. +Add a previous account actor as an ActivityPub migration alias for a local account the authenticated viewer can manage: their own personal account, or an `ORGANIZATION` account they are an accepted `admin` of. The stored value is the resolved actor IRI, which is then advertised as `alsoKnownAs` on the local actor document so a later remote `Move` can be validated. """ input AddAccountMigrationAliasInput { """ @@ -5287,7 +5287,7 @@ type RelaySubscription implements Node { } """ -Remove one ActivityPub migration alias from the authenticated viewer's local account. Removing an alias only changes the new account's `alsoKnownAs` list; it does not send or retract a remote `Move` activity. +Remove one ActivityPub migration alias from a local account the authenticated viewer can manage: their own personal account, or an `ORGANIZATION` account they are an accepted `admin` of. Removing an alias only changes the account's `alsoKnownAs` list; it does not send or retract a remote `Move` activity. """ input RemoveAccountMigrationAliasInput { """ From 4897eac03072d5c32a1a38b1abb9b4e5eeea1b95 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sat, 27 Jun 2026 14:16:34 +0900 Subject: [PATCH 4/6] web-next: Dedupe the account migration card Adding the migration card to the organization branch of the account settings Switch left it duplicated verbatim with the personal branch. Extract it into a shared AccountMigrationCard component used by both branches. Keeping it as a component (rather than splitting the Switch into two around a shared card) preserves the personal branch's existing viewer gating without rendering the card outside that boundary. https://github.com/hackers-pub/hackerspub/pull/321#discussion_r3485267186 Assisted-by: Claude Code:claude-opus-4-8 --- .../(root)/[handle]/settings/account.tsx | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/web-next/src/routes/(root)/[handle]/settings/account.tsx b/web-next/src/routes/(root)/[handle]/settings/account.tsx index 6153882f..49eda031 100644 --- a/web-next/src/routes/(root)/[handle]/settings/account.tsx +++ b/web-next/src/routes/(root)/[handle]/settings/account.tsx @@ -538,22 +538,7 @@ export default function AccountSettingsPage() { account={account} viewer={viewer} /> - - - - {t`Account migration`} - - - {t`Prepare this account as the destination for a Mastodon-style move.`} - - - - - - + {t`Delete account`} @@ -577,22 +562,7 @@ export default function AccountSettingsPage() { account={account} viewerId={data.viewer?.id ?? null} /> - - - - {t`Account migration`} - - - {t`Prepare this account as the destination for a Mastodon-style move.`} - - - - - - + {t`Delete organization`} @@ -1625,6 +1595,26 @@ function OrganizationMemberManagementCard(props: { ); } +function AccountMigrationCard(props: { account: AccountPageAccount }) { + const { t } = useLingui(); + return ( + + + {t`Account migration`} + + {t`Prepare this account as the destination for a Mastodon-style move.`} + + + + + + + ); +} + interface AccountMigrationAliasesFormProps { aliases: readonly string[]; id: string; From 44ae75c8ac80f17a6022ed68d382608325949aea Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sat, 27 Jun 2026 14:24:14 +0900 Subject: [PATCH 5/6] web-next: Fix migration auth error copy for organization admins The account migration form is now shared by organization accounts, but its NotAuthorizedError toast still read "only for your own account" in both the add and remove paths. That is misleading for organization admins, who can manage migration for accounts they do not own. Reword it to "accounts you can manage" and update the locale catalogs. https://github.com/hackers-pub/hackerspub/pull/321#discussion_r3485270313 Assisted-by: Claude Code:claude-opus-4-8 --- web-next/src/locales/en-US/messages.po | 8 ++++---- web-next/src/locales/ja-JP/messages.po | 8 ++++---- web-next/src/locales/ko-KR/messages.po | 8 ++++---- web-next/src/locales/zh-CN/messages.po | 8 ++++---- web-next/src/locales/zh-TW/messages.po | 8 ++++---- web-next/src/routes/(root)/[handle]/settings/account.tsx | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/web-next/src/locales/en-US/messages.po b/web-next/src/locales/en-US/messages.po index 1d49af97..e724b09e 100644 --- a/web-next/src/locales/en-US/messages.po +++ b/web-next/src/locales/en-US/messages.po @@ -5459,10 +5459,10 @@ msgstr "You can only view your own bookmarks" msgid "You can only view your own drafts" msgstr "You can only view your own drafts" -#: src/routes/(root)/[handle]/settings/account.tsx:1670 -#: src/routes/(root)/[handle]/settings/account.tsx:1721 -msgid "You can prepare migration only for your own account." -msgstr "You can prepare migration only for your own account." +#: src/routes/(root)/[handle]/settings/account.tsx:1676 +#: src/routes/(root)/[handle]/settings/account.tsx:1727 +msgid "You can prepare migration only for accounts you can manage." +msgstr "You can prepare migration only for accounts you can manage." #: src/routes/(root)/[handle]/settings/account.tsx:979 msgid "You do not belong to any organizations yet." diff --git a/web-next/src/locales/ja-JP/messages.po b/web-next/src/locales/ja-JP/messages.po index 699bb6a2..2dce5d9b 100644 --- a/web-next/src/locales/ja-JP/messages.po +++ b/web-next/src/locales/ja-JP/messages.po @@ -5455,10 +5455,10 @@ msgstr "自分のブックマークのみ表示できます" msgid "You can only view your own drafts" msgstr "自分の下書きのみ表示できます" -#: src/routes/(root)/[handle]/settings/account.tsx:1670 -#: src/routes/(root)/[handle]/settings/account.tsx:1721 -msgid "You can prepare migration only for your own account." -msgstr "自分のアカウントでのみ移行を準備できます。" +#: src/routes/(root)/[handle]/settings/account.tsx:1676 +#: src/routes/(root)/[handle]/settings/account.tsx:1727 +msgid "You can prepare migration only for accounts you can manage." +msgstr "管理できるアカウントでのみ移行を準備できます。" #: src/routes/(root)/[handle]/settings/account.tsx:979 msgid "You do not belong to any organizations yet." diff --git a/web-next/src/locales/ko-KR/messages.po b/web-next/src/locales/ko-KR/messages.po index 9e726bea..2438f01b 100644 --- a/web-next/src/locales/ko-KR/messages.po +++ b/web-next/src/locales/ko-KR/messages.po @@ -5455,10 +5455,10 @@ msgstr "자신의 북마크만 볼 수 있습니다" msgid "You can only view your own drafts" msgstr "자신의 초고만 볼 수 있습니다" -#: src/routes/(root)/[handle]/settings/account.tsx:1670 -#: src/routes/(root)/[handle]/settings/account.tsx:1721 -msgid "You can prepare migration only for your own account." -msgstr "자신의 계정에서만 이전을 준비할 수 있습니다." +#: src/routes/(root)/[handle]/settings/account.tsx:1676 +#: src/routes/(root)/[handle]/settings/account.tsx:1727 +msgid "You can prepare migration only for accounts you can manage." +msgstr "관리할 수 있는 계정에서만 이전을 준비할 수 있습니다." #: src/routes/(root)/[handle]/settings/account.tsx:979 msgid "You do not belong to any organizations yet." diff --git a/web-next/src/locales/zh-CN/messages.po b/web-next/src/locales/zh-CN/messages.po index cd941203..49cc7274 100644 --- a/web-next/src/locales/zh-CN/messages.po +++ b/web-next/src/locales/zh-CN/messages.po @@ -5455,10 +5455,10 @@ msgstr "您只能查看自己的收藏" msgid "You can only view your own drafts" msgstr "您只能查看自己的草稿" -#: src/routes/(root)/[handle]/settings/account.tsx:1670 -#: src/routes/(root)/[handle]/settings/account.tsx:1721 -msgid "You can prepare migration only for your own account." -msgstr "你只能为自己的账户准备迁移。" +#: src/routes/(root)/[handle]/settings/account.tsx:1676 +#: src/routes/(root)/[handle]/settings/account.tsx:1727 +msgid "You can prepare migration only for accounts you can manage." +msgstr "你只能为你能管理的账户准备迁移。" #: src/routes/(root)/[handle]/settings/account.tsx:979 msgid "You do not belong to any organizations yet." diff --git a/web-next/src/locales/zh-TW/messages.po b/web-next/src/locales/zh-TW/messages.po index 10784ee8..fef9257b 100644 --- a/web-next/src/locales/zh-TW/messages.po +++ b/web-next/src/locales/zh-TW/messages.po @@ -5455,10 +5455,10 @@ msgstr "您只能檢視自己的收藏" msgid "You can only view your own drafts" msgstr "您只能檢視自己的草稿" -#: src/routes/(root)/[handle]/settings/account.tsx:1670 -#: src/routes/(root)/[handle]/settings/account.tsx:1721 -msgid "You can prepare migration only for your own account." -msgstr "你只能為自己的帳戶準備遷移。" +#: src/routes/(root)/[handle]/settings/account.tsx:1676 +#: src/routes/(root)/[handle]/settings/account.tsx:1727 +msgid "You can prepare migration only for accounts you can manage." +msgstr "你只能為你能管理的帳戶準備遷移。" #: src/routes/(root)/[handle]/settings/account.tsx:979 msgid "You do not belong to any organizations yet." diff --git a/web-next/src/routes/(root)/[handle]/settings/account.tsx b/web-next/src/routes/(root)/[handle]/settings/account.tsx index 49eda031..442cc7d1 100644 --- a/web-next/src/routes/(root)/[handle]/settings/account.tsx +++ b/web-next/src/routes/(root)/[handle]/settings/account.tsx @@ -1673,7 +1673,7 @@ function AccountMigrationAliasesForm( if (result?.__typename === "NotAuthorizedError") { showMutationError( t`Cannot update this account`, - t`You can prepare migration only for your own account.`, + t`You can prepare migration only for accounts you can manage.`, ); return; } @@ -1724,7 +1724,7 @@ function AccountMigrationAliasesForm( if (result?.__typename === "NotAuthorizedError") { showMutationError( t`Cannot update this account`, - t`You can prepare migration only for your own account.`, + t`You can prepare migration only for accounts you can manage.`, ); return; } From 23349881fd36bcf4a423c1f0448ebb2b6f1118d2 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sat, 27 Jun 2026 14:42:51 +0900 Subject: [PATCH 6/6] web-next: Use a named props interface for AccountMigrationCard Give AccountMigrationCard a named AccountMigrationCardProps interface instead of an inline object type, matching the style guide and the other prop interfaces in this file (AccountMigrationAliasesFormProps, AccountDeletionFormProps). Props are still accessed via props.account rather than destructured, since destructuring breaks reactivity in Solid. https://github.com/hackers-pub/hackerspub/pull/321#discussion_r3485333251 Assisted-by: Claude Code:claude-opus-4-8 --- web-next/src/routes/(root)/[handle]/settings/account.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web-next/src/routes/(root)/[handle]/settings/account.tsx b/web-next/src/routes/(root)/[handle]/settings/account.tsx index 442cc7d1..703fc4d8 100644 --- a/web-next/src/routes/(root)/[handle]/settings/account.tsx +++ b/web-next/src/routes/(root)/[handle]/settings/account.tsx @@ -1595,7 +1595,11 @@ function OrganizationMemberManagementCard(props: { ); } -function AccountMigrationCard(props: { account: AccountPageAccount }) { +interface AccountMigrationCardProps { + account: AccountPageAccount; +} + +function AccountMigrationCard(props: AccountMigrationCardProps) { const { t } = useLingui(); return (