Neon api keys UI improvement (#398)

This commit is contained in:
Zai Shi 2025-01-23 07:05:34 +01:00 committed by GitHub
parent 05e7b5fa1b
commit 77cce58f92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 24 additions and 17 deletions

View File

@ -18,6 +18,7 @@ export const POST = createSmartRouteHandler({
body: yupObject({
interaction_uid: yupString().defined(),
project_id: yupString().defined(),
neon_project_name: yupString().optional(),
}).defined(),
}),
response: yupObject({
@ -32,7 +33,7 @@ export const POST = createSmartRouteHandler({
const set = await prismaClient.apiKeySet.create({
data: {
projectId: req.body.project_id,
description: "Auto-generated for Neon",
description: `Auto-generated for Neon${req.body.neon_project_name ? ` (${req.body.neon_project_name})` : ""}`,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100),
superSecretAdminKey: `sak_${generateSecureRandomString()}`,
},

View File

@ -373,13 +373,13 @@ export async function createOidcProvider(options: { id: string, baseUrl: string
if (typeof state !== 'string') {
throwErr(`state is not a string`);
}
let neonProjectDisplayName: string | undefined;
let neonProjectName: string | undefined;
try {
const base64Decoded = new TextDecoder().decode(decodeBase64OrBase64Url(state));
const json = JSON.parse(base64Decoded);
neonProjectDisplayName = json?.details?.neon_project_name;
if (typeof neonProjectDisplayName !== 'string') {
throwErr(`neon_project_name is not a string`, { type: typeof neonProjectDisplayName, neonProjectDisplayName });
neonProjectName = json?.details?.neon_project_name;
if (typeof neonProjectName !== 'string') {
throwErr(`neon_project_name is not a string`, { type: typeof neonProjectName, neonProjectName });
}
} catch (e) {
// this probably shouldn't happen, because it means Neon messed up the configuration
@ -391,8 +391,8 @@ export async function createOidcProvider(options: { id: string, baseUrl: string
const uid = ctx.path.split('/')[2];
const interactionUrl = new URL(`/integrations/neon/confirm`, getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL"));
interactionUrl.searchParams.set("interaction_uid", uid);
if (neonProjectDisplayName) {
interactionUrl.searchParams.set("neon_project_display_name", neonProjectDisplayName);
if (neonProjectName) {
interactionUrl.searchParams.set("neon_project_name", neonProjectName);
}
return ctx.redirect(interactionUrl.toString());
}

View File

@ -42,7 +42,7 @@ export const POST = createSmartRouteHandler({
const set = await createApiKeySet({
projectId: createdProject.id,
description: "Auto-generated for Neon",
description: `Auto-generated for Neon (${req.body.display_name})`,
expires_at_millis: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100).getTime(),
has_publishable_client_key: false,
has_secret_server_key: false,

View File

@ -8,7 +8,7 @@ import { useSearchParams } from "next/navigation";
import { useState } from "react";
import NeonLogo from "../../../../../../public/neon.png";
export default function NeonConfirmCard(props: { onContinue: (options: { projectId: string }) => Promise<{ error: string } | undefined> }) {
export default function NeonConfirmCard(props: { onContinue: (options: { projectId: string, neonProjectName?: string }) => Promise<{ error: string } | undefined> }) {
const user = useUser({ or: "redirect", projectIdMustMatch: "internal" });
const projects = user.useOwnedProjects();
const searchParams = useSearchParams();
@ -64,7 +64,7 @@ export default function NeonConfirmCard(props: { onContinue: (options: { project
<Typography className="mb-3">
Which projects would you like to connect?
</Typography>
<Input type="text" disabled prefixItem={<Image src={NeonLogo} alt="Neon" width={15} />} value={searchParams.get("neon_project_display_name") || "Neon project connected!"} />
<Input type="text" disabled prefixItem={<Image src={NeonLogo} alt="Neon" width={15} />} value={searchParams.get("neon_project_name") || "Neon project connected!"} />
<div className="flex flex-row items-center">
<div className={'flex self-stretch justify-center items-center text-muted-foreground pl-3 select-none bg-muted/70 pr-3 border-r border-input rounded-l-md'}>
<Logo noLink width={15} height={15} />
@ -75,7 +75,7 @@ export default function NeonConfirmCard(props: { onContinue: (options: { project
if (p === "create-new") {
const createSearchParams = new URLSearchParams();
createSearchParams.set("redirect_to_neon_confirm_with", searchParams.toString());
const neonDisplayName = searchParams.get("neon_project_display_name");
const neonDisplayName = searchParams.get("neon_project_name");
if (neonDisplayName) {
createSearchParams.set("display_name", neonDisplayName);
}
@ -120,7 +120,7 @@ export default function NeonConfirmCard(props: { onContinue: (options: { project
<Button
disabled={!selectedProject}
onClick={async () => {
const error = await props.onContinue({ projectId: selectedProject!.id });
const error = await props.onContinue({ projectId: selectedProject!.id, neonProjectName: searchParams.get("neon_project_name") ?? undefined });
if (error) {
throw new Error(error.error);
}

View File

@ -16,7 +16,7 @@ export default async function NeonIntegrationConfirmPage(props: { searchParams:
</>;
}
const onContinue = async (options: { projectId: string }) => {
const onContinue = async (options: { projectId: string, neonProjectName?: string }) => {
"use server";
const user = await stackServerApp.getUser();
@ -39,6 +39,7 @@ export default async function NeonIntegrationConfirmPage(props: { searchParams:
body: JSON.stringify({
project_id: options.projectId,
interaction_uid: props.searchParams.interaction_uid,
neon_project_name: options.neonProjectName,
}),
});
if (!response.ok) {

View File

@ -88,7 +88,12 @@ const columns: ColumnDef<ExtendedApiKey>[] = [
{
accessorKey: "expiresAt",
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Expires At" />,
cell: ({ row }) => <DateCell date={row.original.expiresAt} ignoreAfterYears={100} />
cell: ({ row }) => <DateCell date={row.original.expiresAt} ignoreAfterYears={50} />
},
{
accessorKey: "createdAt",
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Created At" />,
cell: ({ row }) => <DateCell date={row.original.createdAt} ignoreAfterYears={50} />
},
{
id: "actions",
@ -102,10 +107,10 @@ export function ApiKeyTable(props: { apiKeys: ApiKey[] }) {
...apiKey,
status: ({ 'valid': 'valid', 'manually-revoked': 'revoked', 'expired': 'expired' } as const)[apiKey.whyInvalid() || 'valid'],
} satisfies ExtendedApiKey));
// first soft based on status, then by expiresAt
// first soft based on status, then by createdAt
return keys.sort((a, b) => {
if (a.status === b.status) {
return a.expiresAt < b.expiresAt ? 1 : -1;
return a.createdAt < b.createdAt ? 1 : -1;
}
return a.status === 'valid' ? -1 : 1;
});

View File

@ -94,7 +94,7 @@ async function authorize(projectId: string) {
"status": 307,
"body": "http://localhost:8101/integrations/neon/confirm?interaction_uid=%3Cstripped+query+param%3E&amp=",
"headers": Headers {
"location": "http://localhost:8101/integrations/neon/confirm?interaction_uid=%3Cstripped+query+param%3E&neon_project_display_name=neon-project",
"location": "http://localhost:8101/integrations/neon/confirm?interaction_uid=%3Cstripped+query+param%3E&neon_project_name=neon-project",
<some fields may have been hidden>,
},
},