Skip to content

Commit dc07145

Browse files
authored
Handle managed vault rotation and redirect (#8747)
1 parent 9017899 commit dc07145

4 files changed

Lines changed: 75 additions & 17 deletions

File tree

apps/dashboard/src/@/hooks/useApi.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22
import { useActiveAccount } from "thirdweb/react";
33
import { apiServerProxy } from "@/actions/proxies";
44
import type { Project } from "@/api/project/projects";
5-
import { rotateVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
65
import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
76

87
// FIXME: We keep repeating types, API server should provide them
@@ -316,6 +315,20 @@ export type RotateSecretKeyAPIReturnType = {
316315
};
317316
};
318317

318+
export const MANAGED_VAULT_BLOCKS_ROTATION_CODE =
319+
"MANAGED_VAULT_BLOCKS_ROTATION";
320+
321+
export class RotateSecretKeyError extends Error {
322+
code: string | undefined;
323+
status: number;
324+
constructor(message: string, status: number, code?: string) {
325+
super(message);
326+
this.name = "RotateSecretKeyError";
327+
this.status = status;
328+
this.code = code;
329+
}
330+
}
331+
319332
export async function rotateSecretKeyClient(params: { project: Project }) {
320333
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
321334
body: JSON.stringify({}),
@@ -327,19 +340,24 @@ export async function rotateSecretKeyClient(params: { project: Project }) {
327340
});
328341

329342
if (!res.ok) {
330-
throw new Error(res.error);
331-
}
332-
333-
// if the project has an encrypted vault admin key, rotate it as well
334-
const service = params.project.services.find(
335-
(service) => service.name === "engineCloud",
336-
);
337-
if (service?.encryptedAdminKey) {
338-
await rotateVaultAccountAndAccessToken({
339-
project: params.project,
340-
projectSecretKey: res.data.data.secret,
341-
projectSecretHash: res.data.data.secretHash,
342-
});
343+
// The error body is a JSON-serialized `{ error: { code, message, ... } }`
344+
// payload from the api-server. Try to extract the structured code so the
345+
// UI can react (e.g. redirect users with a managed vault to the vault
346+
// configuration page so they can eject first).
347+
let code: string | undefined;
348+
let message = res.error;
349+
try {
350+
const parsed = JSON.parse(res.error) as {
351+
error?: { code?: string; message?: string };
352+
};
353+
code = parsed.error?.code;
354+
if (parsed.error?.message) {
355+
message = parsed.error.message;
356+
}
357+
} catch {
358+
// not JSON, fall through with raw text
359+
}
360+
throw new RotateSecretKeyError(message, res.status, code);
343361
}
344362

345363
return res.data;

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/ProjectFTUX.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ export function ProjectFTUX(props: {
3232
}) {
3333
return (
3434
<div className="flex flex-col gap-14">
35-
<IntegrateAPIKeySection project={props.project} />
35+
<IntegrateAPIKeySection
36+
project={props.project}
37+
teamSlug={props.teamSlug}
38+
/>
3639
{props.projectWalletSection}
3740
<GetStartedSection project={props.project} />
3841
<ProductsSection
@@ -47,7 +50,13 @@ export function ProjectFTUX(props: {
4750

4851
// Integrate API key section ------------------------------------------------------------
4952

50-
function IntegrateAPIKeySection({ project }: { project: Project }) {
53+
function IntegrateAPIKeySection({
54+
project,
55+
teamSlug,
56+
}: {
57+
project: Project;
58+
teamSlug: string;
59+
}) {
5160
const secretKeyMasked = project.secretKeys[0]?.masked;
5261
const clientId = project.publishableKey;
5362

@@ -81,6 +90,7 @@ function IntegrateAPIKeySection({ project }: { project: Project }) {
8190
<SecretKeySection
8291
project={project}
8392
secretKeyMasked={secretKeyMasked}
93+
vaultConfigUrl={`/team/${teamSlug}/${project.slug}/wallets/server-wallets/configuration`}
8494
/>
8595
)}
8696
</div>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectFTUX/SecretKeySection.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { RotateSecretKeyButton } from "../../settings/ProjectGeneralSettingsPage
88
export function SecretKeySection(props: {
99
secretKeyMasked: string;
1010
project: Project;
11+
vaultConfigUrl: string;
1112
}) {
1213
const [secretKeyMasked, setSecretKeyMasked] = useState(props.secretKeyMasked);
1314

@@ -34,6 +35,7 @@ export function SecretKeySection(props: {
3435
project: props.project,
3536
});
3637
}}
38+
vaultConfigUrl={props.vaultConfigUrl}
3739
/>
3840
</div>
3941
</div>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
6262
import type { RotateSecretKeyAPIReturnType } from "@/hooks/useApi";
6363
import {
6464
deleteProjectClient,
65+
MANAGED_VAULT_BLOCKS_ROTATION_CODE,
66+
RotateSecretKeyError,
6567
rotateSecretKeyClient,
6668
updateProjectClient,
6769
} from "@/hooks/useApi";
@@ -100,6 +102,7 @@ type ProjectSettingPaths = {
100102
inAppConfig: string;
101103
aaConfig: string;
102104
payConfig: string;
105+
vaultConfig: string;
103106
afterDeleteRedirectTo: string;
104107
};
105108

@@ -224,6 +227,7 @@ export function ProjectGeneralSettingsPageUI(props: {
224227
afterDeleteRedirectTo: `/team/${props.teamSlug}`,
225228
inAppConfig: `${projectLayout}/wallets/user-wallets/configuration`,
226229
payConfig: `${projectLayout}/bridge/configuration`,
230+
vaultConfig: `${projectLayout}/wallets/server-wallets/configuration`,
227231
};
228232

229233
const { project } = props;
@@ -343,6 +347,7 @@ export function ProjectGeneralSettingsPageUI(props: {
343347
<ProjectKeyDetails
344348
project={project}
345349
rotateSecretKey={props.rotateSecretKey}
350+
vaultConfigUrl={paths.vaultConfig}
346351
/>
347352
<ProjectIdCard project={project} />
348353
<AllowedDomainsSetting
@@ -845,9 +850,11 @@ function EnabledServicesSetting(props: {
845850
function ProjectKeyDetails({
846851
project,
847852
rotateSecretKey,
853+
vaultConfigUrl,
848854
}: {
849855
rotateSecretKey: RotateSecretKey;
850856
project: Project;
857+
vaultConfigUrl: string;
851858
}) {
852859
// currently only showing the first secret key
853860
const { createdAt, updatedAt, lastAccessedAt } = project;
@@ -893,6 +900,7 @@ function ProjectKeyDetails({
893900
setSecretKeyMasked(data.data.secretMasked);
894901
}}
895902
rotateSecretKey={rotateSecretKey}
903+
vaultConfigUrl={vaultConfigUrl}
896904
/>
897905
</div>
898906
</div>
@@ -975,6 +983,7 @@ function DeleteProject(props: {
975983
export function RotateSecretKeyButton(props: {
976984
rotateSecretKey: RotateSecretKey;
977985
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
986+
vaultConfigUrl: string;
978987
}) {
979988
const [isOpen, setIsOpen] = useState(false);
980989
const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true);
@@ -1011,6 +1020,7 @@ export function RotateSecretKeyButton(props: {
10111020
disableModalClose={() => setIsModalCloseAllowed(false)}
10121021
onSuccess={props.onSuccess}
10131022
rotateSecretKey={props.rotateSecretKey}
1023+
vaultConfigUrl={props.vaultConfigUrl}
10141024
/>
10151025
</DialogContent>
10161026
</Dialog>
@@ -1026,6 +1036,7 @@ function RotateSecretKeyModalContent(props: {
10261036
closeModal: () => void;
10271037
disableModalClose: () => void;
10281038
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
1039+
vaultConfigUrl: string;
10291040
}) {
10301041
const [screen, setScreen] = useState<RotateSecretKeyScreen>({
10311042
id: "initial",
@@ -1050,6 +1061,7 @@ function RotateSecretKeyModalContent(props: {
10501061
setScreen({ id: "save-newkey", secretKey: data.data.secret });
10511062
}}
10521063
rotateSecretKey={props.rotateSecretKey}
1064+
vaultConfigUrl={props.vaultConfigUrl}
10531065
/>
10541066
);
10551067
}
@@ -1061,13 +1073,29 @@ function RotateSecretKeyInitialScreen(props: {
10611073
rotateSecretKey: RotateSecretKey;
10621074
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
10631075
closeModal: () => void;
1076+
vaultConfigUrl: string;
10641077
}) {
1078+
const router = useDashboardRouter();
10651079
const [isConfirmed, setIsConfirmed] = useState(false);
10661080
const rotateKeyMutation = useMutation({
10671081
mutationFn: props.rotateSecretKey,
10681082
onError: (err) => {
10691083
console.error(err);
1070-
toast.error("Failed to rotate secret key");
1084+
if (
1085+
err instanceof RotateSecretKeyError &&
1086+
err.code === MANAGED_VAULT_BLOCKS_ROTATION_CODE
1087+
) {
1088+
toast.error("Eject your server-wallet vault first", {
1089+
description:
1090+
"This project has a managed vault. Redirecting you to the vault configuration page so you can eject it before rotating the secret key.",
1091+
});
1092+
props.closeModal();
1093+
router.push(props.vaultConfigUrl);
1094+
return;
1095+
}
1096+
toast.error("Failed to rotate secret key", {
1097+
description: err instanceof Error ? err.message : undefined,
1098+
});
10711099
},
10721100
onSuccess: (data) => {
10731101
props.onSuccess(data);

0 commit comments

Comments
 (0)