diff --git a/apps/dashboard/src/@/hooks/useApi.ts b/apps/dashboard/src/@/hooks/useApi.ts index faf7d93c048..9bf41fa537a 100644 --- a/apps/dashboard/src/@/hooks/useApi.ts +++ b/apps/dashboard/src/@/hooks/useApi.ts @@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useActiveAccount } from "thirdweb/react"; import { apiServerProxy } from "@/actions/proxies"; import type { Project } from "@/api/project/projects"; -import { rotateVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client"; import { accountKeys, authorizedWallets } from "../query-keys/cache-keys"; // FIXME: We keep repeating types, API server should provide them @@ -316,6 +315,20 @@ export type RotateSecretKeyAPIReturnType = { }; }; +export const MANAGED_VAULT_BLOCKS_ROTATION_CODE = + "MANAGED_VAULT_BLOCKS_ROTATION"; + +export class RotateSecretKeyError extends Error { + code: string | undefined; + status: number; + constructor(message: string, status: number, code?: string) { + super(message); + this.name = "RotateSecretKeyError"; + this.status = status; + this.code = code; + } +} + export async function rotateSecretKeyClient(params: { project: Project }) { const res = await apiServerProxy({ body: JSON.stringify({}), @@ -327,19 +340,24 @@ export async function rotateSecretKeyClient(params: { project: Project }) { }); if (!res.ok) { - throw new Error(res.error); - } - - // if the project has an encrypted vault admin key, rotate it as well - const service = params.project.services.find( - (service) => service.name === "engineCloud", - ); - if (service?.encryptedAdminKey) { - await rotateVaultAccountAndAccessToken({ - project: params.project, - projectSecretKey: res.data.data.secret, - projectSecretHash: res.data.data.secretHash, - }); + // The error body is a JSON-serialized `{ error: { code, message, ... } }` + // payload from the api-server. Try to extract the structured code so the + // UI can react (e.g. redirect users with a managed vault to the vault + // configuration page so they can eject first). + let code: string | undefined; + let message = res.error; + try { + const parsed = JSON.parse(res.error) as { + error?: { code?: string; message?: string }; + }; + code = parsed.error?.code; + if (parsed.error?.message) { + message = parsed.error.message; + } + } catch { + // not JSON, fall through with raw text + } + throw new RotateSecretKeyError(message, res.status, code); } return res.data; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx index c1099a5243a..99b217535ef 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx @@ -32,7 +32,10 @@ export function ProjectFTUX(props: { }) { return (
- + {props.projectWalletSection} )}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx index 7db372715a5..400a59f2854 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx @@ -8,6 +8,7 @@ import { RotateSecretKeyButton } from "../../settings/ProjectGeneralSettingsPage export function SecretKeySection(props: { secretKeyMasked: string; project: Project; + vaultConfigUrl: string; }) { const [secretKeyMasked, setSecretKeyMasked] = useState(props.secretKeyMasked); @@ -34,6 +35,7 @@ export function SecretKeySection(props: { project: props.project, }); }} + vaultConfigUrl={props.vaultConfigUrl} /> diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx index 3fa9646708e..1b31742e57a 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx @@ -62,6 +62,8 @@ import { ToolTipLabel } from "@/components/ui/tooltip"; import type { RotateSecretKeyAPIReturnType } from "@/hooks/useApi"; import { deleteProjectClient, + MANAGED_VAULT_BLOCKS_ROTATION_CODE, + RotateSecretKeyError, rotateSecretKeyClient, updateProjectClient, } from "@/hooks/useApi"; @@ -100,6 +102,7 @@ type ProjectSettingPaths = { inAppConfig: string; aaConfig: string; payConfig: string; + vaultConfig: string; afterDeleteRedirectTo: string; }; @@ -224,6 +227,7 @@ export function ProjectGeneralSettingsPageUI(props: { afterDeleteRedirectTo: `/team/${props.teamSlug}`, inAppConfig: `${projectLayout}/wallets/user-wallets/configuration`, payConfig: `${projectLayout}/bridge/configuration`, + vaultConfig: `${projectLayout}/wallets/server-wallets/configuration`, }; const { project } = props; @@ -343,6 +347,7 @@ export function ProjectGeneralSettingsPageUI(props: { @@ -975,6 +983,7 @@ function DeleteProject(props: { export function RotateSecretKeyButton(props: { rotateSecretKey: RotateSecretKey; onSuccess: (data: RotateSecretKeyAPIReturnType) => void; + vaultConfigUrl: string; }) { const [isOpen, setIsOpen] = useState(false); const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true); @@ -1011,6 +1020,7 @@ export function RotateSecretKeyButton(props: { disableModalClose={() => setIsModalCloseAllowed(false)} onSuccess={props.onSuccess} rotateSecretKey={props.rotateSecretKey} + vaultConfigUrl={props.vaultConfigUrl} /> @@ -1026,6 +1036,7 @@ function RotateSecretKeyModalContent(props: { closeModal: () => void; disableModalClose: () => void; onSuccess: (data: RotateSecretKeyAPIReturnType) => void; + vaultConfigUrl: string; }) { const [screen, setScreen] = useState({ id: "initial", @@ -1050,6 +1061,7 @@ function RotateSecretKeyModalContent(props: { setScreen({ id: "save-newkey", secretKey: data.data.secret }); }} rotateSecretKey={props.rotateSecretKey} + vaultConfigUrl={props.vaultConfigUrl} /> ); } @@ -1061,13 +1073,29 @@ function RotateSecretKeyInitialScreen(props: { rotateSecretKey: RotateSecretKey; onSuccess: (data: RotateSecretKeyAPIReturnType) => void; closeModal: () => void; + vaultConfigUrl: string; }) { + const router = useDashboardRouter(); const [isConfirmed, setIsConfirmed] = useState(false); const rotateKeyMutation = useMutation({ mutationFn: props.rotateSecretKey, onError: (err) => { console.error(err); - toast.error("Failed to rotate secret key"); + if ( + err instanceof RotateSecretKeyError && + err.code === MANAGED_VAULT_BLOCKS_ROTATION_CODE + ) { + toast.error("Eject your server-wallet vault first", { + description: + "This project has a managed vault. Redirecting you to the vault configuration page so you can eject it before rotating the secret key.", + }); + props.closeModal(); + router.push(props.vaultConfigUrl); + return; + } + toast.error("Failed to rotate secret key", { + description: err instanceof Error ? err.message : undefined, + }); }, onSuccess: (data) => { props.onSuccess(data);