diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx index ebcecf9150..02680083c3 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/new/client.tsx @@ -46,11 +46,18 @@ export const dynamic = "force-dynamic"; type Props = { apiId: string; keyAuthId: string; + storeEncryptedKeys: boolean; defaultBytes: number | null; defaultPrefix: string | null; }; -export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Props) => { +export const CreateKey = ({ + apiId, + keyAuthId, + storeEncryptedKeys, + defaultBytes, + defaultPrefix, +}: Props) => { const router = useRouter(); const form = useForm>({ resolver: async (data, context, options) => { @@ -67,6 +74,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro expireEnabled: false, limitEnabled: false, metaEnabled: false, + recoverEnabled: false, ratelimitEnabled: false, }, }); @@ -105,6 +113,9 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro if (refill?.interval === "monthly" && !refill.refillDay) { refill.refillDay = 1; } + if (!values.recoverEnabled) { + setRecoverable(false); + } await key.mutateAsync({ keyAuthId, @@ -114,6 +125,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro ownerId: values.ownerId ?? undefined, remaining: values.limit?.remaining ?? undefined, refill: refill, + recoverEnabled: values.recoverEnabled, enabled: true, }); @@ -133,6 +145,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro : "*".repeat(split.at(0)?.length ?? 0); const [showKey, setShowKey] = useState(false); const [showKeyInSnippet, setShowKeyInSnippet] = useState(false); + const [recoverable, setRecoverable] = useState(false); const resetRateLimit = () => { // set them to undefined so the form resets properly. @@ -170,9 +183,36 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro - This key is only shown once and can not be recovered + + {recoverable + ? "This key can be recovered" + : "This key is only shown once and cannot be recovered"} + - Please pass it on to your user or store it somewhere safe. + {recoverable ? ( + <> + It can be recovered using endpoints{" "} + + getKey + {" "} + and{" "} + + listKeys + + . Although we still recommend you to pass it on to your user or store it + somewhere safe. + + ) : ( + "Please pass it on to your user or store it somewhere safe." + )} @@ -209,6 +249,7 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro form.setValue("expireEnabled", false); form.setValue("ratelimitEnabled", false); form.setValue("metaEnabled", false); + form.setValue("recoverEnabled", false); form.setValue("limitEnabled", false); router.refresh(); }} @@ -750,7 +791,57 @@ export const CreateKey = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }: Pro ) : null} + {storeEncryptedKeys && ( + + +
+ Recoverable + + ( + + Recoverable + + { + field.onChange(e); + setRecoverable(e); + if (field.value === false) { + resetLimited(); + } + }} + /> + + + )} + /> +
+ {form.watch("recoverEnabled") ? ( + <> + {form.formState.errors.ratelimit && ( +

+ {form.formState.errors.ratelimit.message} +

+ )} + + ) : null} +

+ You can choose to recover and display plaintext keys later, though it's + not recommended. Recoverable keys are securely stored in an encrypted + vault. For more, visit{" "} + + unkey.com/docs/security/recovering-keys. + +

+
+
+ )}