Skip to content

Commit 0150d1e

Browse files
rotate admin button
1 parent c8262fc commit 0150d1e

File tree

3 files changed

+251
-9
lines changed

3 files changed

+251
-9
lines changed

apps/dashboard/src/app/team/[team_slug]/[project_slug]/transactions/server-wallets/components/key-management.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Button } from "@/components/ui/button";
22
import { RefreshCcwIcon } from "lucide-react";
33
import ListAccessTokensButton from "./list-access-tokens.client";
4+
import RotateAdminKeyButton from "./rotate-admin-key.client";
45

56
export function KeyManagement({
67
maskedAdminKey,
@@ -26,13 +27,7 @@ export function KeyManagement({
2627
<h3 className="font-medium text-sm">Vault Admin Key</h3>
2728
<p className="text-muted-foreground text-sm">{maskedAdminKey}</p>
2829
</div>
29-
<Button
30-
variant="outline"
31-
className="h-auto gap-2 rounded-lg bg-background px-4 py-3"
32-
>
33-
<RefreshCcwIcon className="size-4" />
34-
Rotate Admin Key
35-
</Button>
30+
<RotateAdminKeyButton />
3631
</div>
3732
<div className="flex flex-row justify-end gap-4 border-border border-t px-6 pt-4 pb-4">
3833
<ListAccessTokensButton projectId={projectId} teamId={teamId} />

apps/dashboard/src/app/team/[team_slug]/[project_slug]/transactions/server-wallets/components/list-access-tokens.client.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ export default function ListAccessTokensButton(props: {
368368
</p>
369369
<Input
370370
type="password"
371-
placeholder="Enter your Vault Admin Key"
371+
placeholder="sa_adm_ABCD_1234..."
372372
value={typedAdminKey}
373373
onChange={(e) => setTypedAdminKey(e.target.value)}
374374
onKeyDown={(e) => {
@@ -401,7 +401,7 @@ export default function ListAccessTokensButton(props: {
401401
Loading...
402402
</>
403403
) : (
404-
"List Tokens"
404+
"Manage Access Tokens"
405405
)}
406406
</Button>
407407
</div>
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
"use client";
2+
3+
import { CopyTextButton } from "@/components/ui/CopyTextButton";
4+
import { Spinner } from "@/components/ui/Spinner/Spinner";
5+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
6+
import { Button } from "@/components/ui/button";
7+
import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
8+
import {
9+
Dialog,
10+
DialogContent,
11+
DialogDescription,
12+
DialogHeader,
13+
DialogTitle,
14+
} from "@/components/ui/dialog";
15+
import { Input } from "@/components/ui/input";
16+
import { cn } from "@/lib/utils";
17+
import { useMutation } from "@tanstack/react-query";
18+
import {
19+
createVaultClient,
20+
rotateServiceAccount,
21+
} from "@thirdweb-dev/vault-sdk";
22+
import { Loader2, RefreshCcwIcon } from "lucide-react";
23+
import { useState } from "react";
24+
import { toast } from "sonner";
25+
import { THIRDWEB_VAULT_URL } from "../../../../../../../@/constants/env";
26+
27+
export default function RotateAdminKeyButton() {
28+
const [modalOpen, setModalOpen] = useState(false);
29+
const [rotationCode, setRotationCode] = useState("");
30+
const [keysConfirmed, setKeysConfirmed] = useState(false);
31+
32+
const rotateAdminKeyMutation = useMutation({
33+
mutationFn: async () => {
34+
if (!rotationCode) {
35+
throw new Error("Rotation code is required");
36+
}
37+
await new Promise((resolve) => setTimeout(resolve, 1000));
38+
39+
const client = await createVaultClient({
40+
baseUrl: THIRDWEB_VAULT_URL,
41+
});
42+
43+
const rotateServiceAccountRes = await rotateServiceAccount({
44+
client,
45+
request: {
46+
auth: {
47+
rotationCode,
48+
},
49+
},
50+
});
51+
52+
if (rotateServiceAccountRes.error) {
53+
throw new Error(rotateServiceAccountRes.error.message);
54+
}
55+
56+
return {
57+
success: true,
58+
adminKey: rotateServiceAccountRes.data.newAdminKey,
59+
rotationKey: rotateServiceAccountRes.data.newRotationCode,
60+
};
61+
},
62+
onError: (error) => {
63+
toast.error(error.message);
64+
},
65+
});
66+
67+
const handleCloseModal = () => {
68+
if (!keysConfirmed) {
69+
return;
70+
}
71+
setModalOpen(false);
72+
setRotationCode("");
73+
setKeysConfirmed(false);
74+
};
75+
76+
const isLoading = rotateAdminKeyMutation.isPending;
77+
78+
return (
79+
<>
80+
<Button
81+
variant="outline"
82+
onClick={() => setModalOpen(true)}
83+
disabled={isLoading}
84+
className="h-auto gap-2 rounded-lg bg-background px-4 py-3"
85+
>
86+
{isLoading && <Loader2 className="animate-spin" />}
87+
{!isLoading && <RefreshCcwIcon className="size-4" />}
88+
Rotate Admin Key
89+
</Button>
90+
91+
<Dialog open={modalOpen} onOpenChange={handleCloseModal} modal={true}>
92+
<DialogContent
93+
className="overflow-hidden p-0"
94+
dialogCloseClassName={cn(!keysConfirmed && "hidden")}
95+
>
96+
{rotateAdminKeyMutation.isPending ? (
97+
<>
98+
<DialogHeader className="p-6">
99+
<DialogTitle>Generating new keys...</DialogTitle>
100+
</DialogHeader>
101+
<div className="flex flex-col items-center justify-center gap-4 p-10">
102+
<Spinner className="size-8" />
103+
<p className="text-muted-foreground text-xs">
104+
This may take a few seconds.
105+
</p>
106+
</div>
107+
</>
108+
) : rotateAdminKeyMutation.data ? (
109+
<div>
110+
<DialogHeader className="p-6">
111+
<DialogTitle>New Vault Keys</DialogTitle>
112+
</DialogHeader>
113+
114+
<div className="space-y-6 p-6 pt-0">
115+
<div className="space-y-4">
116+
<div>
117+
<h3 className="mb-2 font-medium text-sm">
118+
New Vault Admin Key
119+
</h3>
120+
<div className="flex flex-col gap-2">
121+
<CopyTextButton
122+
textToCopy={rotateAdminKeyMutation.data.adminKey}
123+
className="!h-auto w-full justify-between bg-background px-3 py-3 font-mono text-xs"
124+
textToShow={maskSecret(
125+
rotateAdminKeyMutation.data.adminKey,
126+
)}
127+
copyIconPosition="right"
128+
tooltip="Copy Admin Key"
129+
/>
130+
<p className="text-muted-foreground text-xs">
131+
This key is used to create or revoke your access tokens.
132+
</p>
133+
</div>
134+
</div>
135+
136+
<div>
137+
<h3 className="mb-2 font-medium text-sm">
138+
New Rotation Key
139+
</h3>
140+
<div className="flex flex-col gap-2">
141+
<CopyTextButton
142+
textToCopy={rotateAdminKeyMutation.data.rotationKey}
143+
className="!h-auto w-full justify-between bg-background px-3 py-3 font-mono text-xs"
144+
textToShow={maskSecret(
145+
rotateAdminKeyMutation.data.rotationKey,
146+
)}
147+
copyIconPosition="right"
148+
tooltip="Copy Rotation Key"
149+
/>
150+
<p className="text-muted-foreground text-xs">
151+
This key is used to rotate your admin key in the future.
152+
</p>
153+
</div>
154+
</div>
155+
</div>
156+
<Alert variant="destructive">
157+
<AlertTitle>Secure your keys</AlertTitle>
158+
<AlertDescription>
159+
These keys will not be displayed again. Store them securely
160+
as they provide access to your server wallets.
161+
</AlertDescription>
162+
<div className="h-4" />
163+
<CheckboxWithLabel className="text-foreground">
164+
<Checkbox
165+
checked={keysConfirmed}
166+
onCheckedChange={(v) => setKeysConfirmed(!!v)}
167+
/>
168+
I confirm that I've securely stored these keys
169+
</CheckboxWithLabel>
170+
</Alert>
171+
</div>
172+
173+
<div className="flex justify-end gap-3 border-t bg-card px-6 py-4">
174+
<Button
175+
onClick={handleCloseModal}
176+
disabled={!keysConfirmed}
177+
variant="primary"
178+
>
179+
Close
180+
</Button>
181+
</div>
182+
</div>
183+
) : (
184+
<>
185+
<DialogHeader className="p-6">
186+
<DialogTitle>Rotate your Vault admin key</DialogTitle>
187+
<DialogDescription>
188+
This action will generate a new Vault admin key and rotation
189+
code. This will invalidate all existing access tokens.
190+
</DialogDescription>
191+
</DialogHeader>
192+
<div className="px-6 pb-6">
193+
<div className="flex flex-col gap-4">
194+
<p className="text-sm text-warning-text">
195+
This action requires your Vault rotation code.
196+
</p>
197+
<Input
198+
type="password"
199+
placeholder="sa_rot_ABCD_1234..."
200+
value={rotationCode}
201+
onChange={(e) => setRotationCode(e.target.value)}
202+
onKeyDown={(e) => {
203+
if (e.key === "Enter") {
204+
rotateAdminKeyMutation.mutate();
205+
}
206+
}}
207+
/>
208+
<div className="flex justify-end gap-3">
209+
<Button
210+
variant="outline"
211+
onClick={() => {
212+
setRotationCode("");
213+
setModalOpen(false);
214+
}}
215+
>
216+
Cancel
217+
</Button>
218+
<Button
219+
variant="primary"
220+
onClick={() => rotateAdminKeyMutation.mutate()}
221+
disabled={
222+
!rotationCode || rotateAdminKeyMutation.isPending
223+
}
224+
>
225+
{rotateAdminKeyMutation.isPending ? (
226+
<>
227+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
228+
Rotating...
229+
</>
230+
) : (
231+
"Rotate Admin Key"
232+
)}
233+
</Button>
234+
</div>
235+
</div>
236+
</div>
237+
</>
238+
)}
239+
</DialogContent>
240+
</Dialog>
241+
</>
242+
);
243+
}
244+
245+
function maskSecret(secret: string) {
246+
return `${secret.substring(0, 11)}...${secret.substring(secret.length - 5)}`;
247+
}

0 commit comments

Comments
 (0)