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
2 changes: 1 addition & 1 deletion console/e2e-tests/platform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ for (const region of REGIONS) {
await page.getByRole("textbox", { name: "Name" }).fill(apiKeyName);
await page.getByRole("button", { name: "Create password" }).click();
await page.getByText(`New password "${apiKeyName}"`).waitFor();
await page.getByRole("button", { name: "visibility" }).click();
await page.getByRole("button", { name: "visibility" }).first().click();
const passwordField = page.getByLabel("clientId");
const password = await passwordField.evaluate((e) => e.textContent);
assert(!!password, "Expected a password to be created");
Expand Down
51 changes: 40 additions & 11 deletions console/src/access/AppPasswordsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ import {
formatDate,
FRIENDLY_DATETIME_FORMAT_NO_SECONDS,
} from "~/utils/dateFormat";
import { toBase64 } from "~/utils/format";
import { obfuscateSecret } from "~/utils/format";

const AppPasswordsPage = ({ user }: { user: User }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
Expand Down Expand Up @@ -143,6 +145,7 @@ const AppPasswordsInner = (props: {
});

const [newPasswordClosed, setNewPasswordClosed] = useState("");
const [newPasswordUser, setNewPasswordUser] = useState<string | null>(null);

const watchType = watch("type");

Expand All @@ -159,6 +162,7 @@ const AppPasswordsInner = (props: {
name={newPassword.description}
password={newPassword.password}
obfuscatedContent={newPassword.obfuscatedPassword}
userEmail={newPasswordUser ?? user.email}
onClose={() => setNewPasswordClosed(newPassword.clientId)}
/>
)}
Expand All @@ -175,6 +179,7 @@ const AppPasswordsInner = (props: {
<ModalContent>
<form
onSubmit={handleSubmit((data) => {
setNewPasswordUser(data.type === "service" ? data.user : null);
createAppPassword({
type: data.type,
description: data.name,
Expand Down Expand Up @@ -462,16 +467,22 @@ type SecretBoxProps = {
name: string;
password: string;
obfuscatedContent: string;
userEmail: string;
onClose: () => void;
};

const SecretBox = ({
name,
password,
obfuscatedContent,
userEmail,
onClose,
}: SecretBoxProps) => {
const { colors } = useTheme<MaterializeTheme>();

const base64Token = toBase64(`${userEmail}:${password}`);
const obfuscatedBase64Token = obfuscateSecret(base64Token);

return (
<ChakraAlert
status="info"
Expand All @@ -484,18 +495,36 @@ const SecretBox = ({
>
<VStack alignItems="flex-start" width="100%">
<AlertDescription width="100%" px={2}>
<VStack alignItems="start">
<Text fontSize="md" fontWeight="500">
New password {`"${name}"`}:
</Text>
<SecretCopyableBox
label="clientId"
contents={password}
obfuscatedContent={obfuscatedContent}
/>
<VStack alignItems="start" spacing="3">
<VStack alignItems="start" spacing="1" width="100%">
<Text fontSize="md" fontWeight="500">
New password {`"${name}"`}:
</Text>
<SecretCopyableBox
label="clientId"
contents={password}
obfuscatedContent={obfuscatedContent}
/>
</VStack>
<VStack alignItems="start" spacing="1" width="100%">
<Text fontSize="md" fontWeight="500">
MCP Token
</Text>
<SecretCopyableBox
label="mcpToken"
contents={base64Token}
obfuscatedContent={obfuscatedBase64Token}
overflow="hidden"
minWidth={0}
/>
<Text fontSize="xs" color={colors.foreground.secondary}>
Base64-encoded <code>{userEmail}:&lt;password&gt;</code> for MCP
configuration.
</Text>
</VStack>
</VStack>
<Text pt={1} textStyle="text-base" color={colors.foreground.primary}>
Write this down; you will not be able to see your app password again
<Text pt={2} textStyle="text-base" color={colors.foreground.primary}>
Write this down; you will not be able to see your credentials again
after you reload!
</Text>
</AlertDescription>
Expand Down
19 changes: 19 additions & 0 deletions console/src/components/ConnectInstructions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import { useParams } from "react-router-dom";
import { User } from "~/external-library-wrappers/frontegg";
import { ClusterDetailParams } from "~/platform/clusters/ClusterRoutes";
import { currentEnvironmentState } from "~/store/environments";
import ConnectionIcon from "~/svg/ConnectionIcon";
import { MonitorIcon } from "~/svg/Monitor";
import TerminalIcon from "~/svg/Terminal";

import { CopyableBox, TabbedCodeBlock } from "./copyableComponents";
import McpConnectInstructions from "./McpConnectInstructions";

export interface ConnectInstructionsProps extends BoxProps {
/** The user string to display. Falls back to user.email if not provided. */
Expand All @@ -29,10 +31,16 @@ export interface ConnectInstructionsProps extends BoxProps {
environmentdAddress?: string;
/** Override the query params in the psql connection string. */
psqlQueryParams?: string;
/** Pre-computed Base64 token for MCP configuration (cloud only). */
mcpBase64Token?: string;
/** Called when the active tab changes. */
onTabChange?: (title: string) => void;
}

const ConnectInstructions = ({
user,
onTabChange,
mcpBase64Token,
...props
}: ConnectInstructionsProps): JSX.Element => {
const [currentEnvironment] = useAtom(currentEnvironmentState);
Expand Down Expand Up @@ -115,7 +123,18 @@ const ConnectInstructions = ({
contents: psqlCopyString,
icon: <TerminalIcon w="4" h="4" />,
},
{
title: "MCP Server",
children: (
<McpConnectInstructions
userStr={userStr}
mcpBase64Token={mcpBase64Token}
/>
),
icon: <ConnectionIcon w="4" h="4" />,
},
]}
onTabChange={onTabChange}
minHeight="208px"
{...props}
/>
Expand Down
78 changes: 65 additions & 13 deletions console/src/components/ConnectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
useTheme,
VStack,
} from "@chakra-ui/react";
import React from "react";
import React, { useState } from "react";

import ConnectInstructions from "~/components/ConnectInstructions";
import { Modal } from "~/components/Modal";
Expand All @@ -31,6 +31,7 @@ import docUrls from "~/mz-doc-urls.json";
import { useCreateApiToken } from "~/queries/frontegg";
import { useListApiTokens } from "~/queries/frontegg";
import { MaterializeTheme } from "~/theme";
import { obfuscateSecret, toBase64 } from "~/utils/format";

import { SecretCopyableBox } from "./copyableComponents";
import SupportLink from "./SupportLink";
Expand All @@ -56,6 +57,17 @@ const ConnectModal = ({
}) => {
const { colors } = useTheme<MaterializeTheme>();
const showCreateAppPassword = !forAppPassword;
const [activeTab, setActiveTab] = useState("External tools");
const {
mutate: createAppPassword,
isPending: createInProgress,
data: newPassword,
} = useCreateApiToken();

const mcpUser = forAppPassword?.user ?? user.email;
const mcpBase64Token = newPassword?.password
? toBase64(`${mcpUser}:${newPassword.password}`)
: undefined;

return (
<Modal size="3xl" isOpen={isOpen} onClose={onClose}>
Expand Down Expand Up @@ -83,11 +95,20 @@ const ConnectModal = ({
<ConnectInstructions
user={user}
userStr={forAppPassword?.user}
mcpBase64Token={mcpBase64Token}
onTabChange={setActiveTab}
mt="4"
/>
{showCreateAppPassword && (
<Box mt="6">
<CreateAppPassword user={user} />
<CreateAppPassword
user={user}
createAppPassword={createAppPassword}
createInProgress={createInProgress}
newPassword={newPassword}
mcpBase64Token={mcpBase64Token}
showMcpToken={activeTab === "MCP Server"}
/>
</Box>
)}
</ModalBody>
Expand All @@ -96,7 +117,16 @@ const ConnectModal = ({
);
};

const CreateAppPassword = ({ user }: { user: User }) => {
interface CreateAppPasswordProps {
user: User;
createAppPassword: ReturnType<typeof useCreateApiToken>["mutate"];
createInProgress: boolean;
newPassword: ReturnType<typeof useCreateApiToken>["data"];
mcpBase64Token?: string;
showMcpToken: boolean;
}

const CreateAppPassword = (props: CreateAppPasswordProps) => {
const { colors } = useTheme<MaterializeTheme>();

return (
Expand All @@ -107,19 +137,21 @@ const CreateAppPassword = ({ user }: { user: User }) => {
</Flex>
}
>
<CreateAppPasswordInner user={user} />
<CreateAppPasswordInner {...props} />
</React.Suspense>
);
};

const CreateAppPasswordInner = ({ user }: { user: User }) => {
const CreateAppPasswordInner = ({
user,
createAppPassword,
createInProgress,
newPassword,
mcpBase64Token,
showMcpToken,
}: CreateAppPasswordProps) => {
const { data: appPasswords } = useListApiTokens({ user });
const { colors } = useTheme<MaterializeTheme>();
const {
mutate: createAppPassword,
isPending: createInProgress,
data: newPassword,
} = useCreateApiToken();

if (createInProgress) {
return (
Expand All @@ -133,6 +165,26 @@ const CreateAppPasswordInner = ({ user }: { user: User }) => {
if (newPassword?.password) {
return (
<>
{showMcpToken && mcpBase64Token && (
<VStack alignItems="stretch" mb="3">
<Text
as="span"
fontSize="sm"
lineHeight="16px"
fontWeight={500}
color={colors.foreground.primary}
>
MCP token
</Text>
<SecretCopyableBox
label="mcpToken"
contents={mcpBase64Token}
obfuscatedContent={obfuscateSecret(mcpBase64Token)}
overflow="hidden"
minWidth={0}
/>
</VStack>
)}
<VStack alignItems="stretch">
<Text
as="span"
Expand All @@ -141,7 +193,7 @@ const CreateAppPasswordInner = ({ user }: { user: User }) => {
fontWeight={500}
color={colors.foreground.primary}
>
New app password
App password
</Text>
<SecretCopyableBox
label="clientId"
Expand All @@ -156,8 +208,8 @@ const CreateAppPasswordInner = ({ user }: { user: User }) => {
fontWeight={400}
color={colors.foreground.secondary}
>
Copy this app password to somewhere safe. App passwords cannot be
displayed after initial creation.
Copy this somewhere safe. App passwords cannot be displayed after
initial creation.
</Text>
</>
);
Expand Down
Loading
Loading