Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions apps/dashboard/src/@/hooks/useApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<RotateSecretKeyAPIReturnType>({
body: JSON.stringify({}),
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export function ProjectFTUX(props: {
}) {
return (
<div className="flex flex-col gap-14">
<IntegrateAPIKeySection project={props.project} />
<IntegrateAPIKeySection
project={props.project}
teamSlug={props.teamSlug}
/>
{props.projectWalletSection}
<GetStartedSection project={props.project} />
<ProductsSection
Expand All @@ -47,7 +50,13 @@ export function ProjectFTUX(props: {

// Integrate API key section ------------------------------------------------------------

function IntegrateAPIKeySection({ project }: { project: Project }) {
function IntegrateAPIKeySection({
project,
teamSlug,
}: {
project: Project;
teamSlug: string;
}) {
const secretKeyMasked = project.secretKeys[0]?.masked;
const clientId = project.publishableKey;

Expand Down Expand Up @@ -81,6 +90,7 @@ function IntegrateAPIKeySection({ project }: { project: Project }) {
<SecretKeySection
project={project}
secretKeyMasked={secretKeyMasked}
vaultConfigUrl={`/team/${teamSlug}/${project.slug}/wallets/server-wallets/configuration`}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -34,6 +35,7 @@ export function SecretKeySection(props: {
project: props.project,
});
}}
vaultConfigUrl={props.vaultConfigUrl}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -100,6 +102,7 @@ type ProjectSettingPaths = {
inAppConfig: string;
aaConfig: string;
payConfig: string;
vaultConfig: string;
afterDeleteRedirectTo: string;
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -343,6 +347,7 @@ export function ProjectGeneralSettingsPageUI(props: {
<ProjectKeyDetails
project={project}
rotateSecretKey={props.rotateSecretKey}
vaultConfigUrl={paths.vaultConfig}
/>
<ProjectIdCard project={project} />
<AllowedDomainsSetting
Expand Down Expand Up @@ -845,9 +850,11 @@ function EnabledServicesSetting(props: {
function ProjectKeyDetails({
project,
rotateSecretKey,
vaultConfigUrl,
}: {
rotateSecretKey: RotateSecretKey;
project: Project;
vaultConfigUrl: string;
}) {
// currently only showing the first secret key
const { createdAt, updatedAt, lastAccessedAt } = project;
Expand Down Expand Up @@ -893,6 +900,7 @@ function ProjectKeyDetails({
setSecretKeyMasked(data.data.secretMasked);
}}
rotateSecretKey={rotateSecretKey}
vaultConfigUrl={vaultConfigUrl}
/>
</div>
</div>
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1011,6 +1020,7 @@ export function RotateSecretKeyButton(props: {
disableModalClose={() => setIsModalCloseAllowed(false)}
onSuccess={props.onSuccess}
rotateSecretKey={props.rotateSecretKey}
vaultConfigUrl={props.vaultConfigUrl}
/>
</DialogContent>
</Dialog>
Expand All @@ -1026,6 +1036,7 @@ function RotateSecretKeyModalContent(props: {
closeModal: () => void;
disableModalClose: () => void;
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
vaultConfigUrl: string;
}) {
const [screen, setScreen] = useState<RotateSecretKeyScreen>({
id: "initial",
Expand All @@ -1050,6 +1061,7 @@ function RotateSecretKeyModalContent(props: {
setScreen({ id: "save-newkey", secretKey: data.data.secret });
}}
rotateSecretKey={props.rotateSecretKey}
vaultConfigUrl={props.vaultConfigUrl}
/>
);
}
Expand All @@ -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);
Expand Down
Loading