This commit is contained in:
Zai Shi 2025-05-20 17:29:44 -07:00
parent 2191e1c023
commit 9bbd3b6816
15 changed files with 36 additions and 35 deletions

View File

@ -5,3 +5,4 @@ ALTER TYPE "VerificationCodeType" RENAME VALUE 'NEON_INTEGRATION_PROJECT_TRANSFE
ALTER TABLE "NeonProvisionedProject" RENAME TO "ProvisionedProject";
ALTER TABLE "ProvisionedProject" RENAME CONSTRAINT "NeonProvisionedProject_pkey" TO "ProvisionedProject_pkey";
ALTER TABLE "ProvisionedProject" RENAME CONSTRAINT "NeonProvisionedProject_projectId_fkey" TO "ProvisionedProject_projectId_fkey";
ALTER TABLE "ProvisionedProject" RENAME COLUMN "neonClientId" TO "externalProjectId";

View File

@ -641,7 +641,7 @@ model ProvisionedProject {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
neonClientId String?
externalProjectId String
}
//#region Events

View File

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

View File

@ -54,7 +54,7 @@ export const POST = createSmartRouteHandler({
await prismaClient.provisionedProject.create({
data: {
projectId: createdProject.id,
neonClientId: clientId,
externalProjectId: clientId,
},
});

View File

@ -1,3 +1,3 @@
import { neonIntegrationProjectTransferCodeHandler } from "../verification-code-handler";
import { integrationProjectTransferCodeHandler } from "../verification-code-handler";
export const POST = neonIntegrationProjectTransferCodeHandler.checkHandler;
export const POST = integrationProjectTransferCodeHandler.checkHandler;

View File

@ -1,3 +1,3 @@
import { neonIntegrationProjectTransferCodeHandler } from "./verification-code-handler";
import { integrationProjectTransferCodeHandler } from "./verification-code-handler";
export const POST = neonIntegrationProjectTransferCodeHandler.postHandler;
export const POST = integrationProjectTransferCodeHandler.postHandler;

View File

@ -5,7 +5,7 @@ import { KnownErrors } from "@stackframe/stack-shared";
import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeHandler({
export const integrationProjectTransferCodeHandler = createVerificationCodeHandler({
metadata: {
post: {
hidden: true,
@ -16,7 +16,7 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH
},
type: VerificationCodeType.INTEGRATION_PROJECT_TRANSFER,
data: yupObject({
neon_client_id: yupString().defined(),
external_project_id: yupString().defined(),
project_id: yupString().defined(),
}).defined(),
method: yupObject({}),
@ -34,7 +34,7 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH
const provisionedProjects = await prismaClient.provisionedProject.findMany({
where: {
projectId: data.project_id,
neonClientId: data.neon_client_id,
externalProjectId: data.external_project_id,
},
});
if (provisionedProjects.length === 0) throw new StatusError(400, "The project to transfer was not provisioned by Neon or has already been transferred.");
@ -48,7 +48,7 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH
const provisionedProject = await tx.provisionedProject.deleteMany({
where: {
projectId: data.project_id,
neonClientId: data.neon_client_id,
externalProjectId: data.external_project_id,
},
});

View File

@ -6,7 +6,7 @@ import { neonAuthorizationHeaderSchema, urlSchema, yupNumber, yupObject, yupStri
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { decodeBasicAuthorizationHeader } from "@stackframe/stack-shared/dist/utils/http";
import { neonIntegrationProjectTransferCodeHandler } from "./confirm/verification-code-handler";
import { integrationProjectTransferCodeHandler } from "./confirm/verification-code-handler";
async function validateAndGetTransferInfo(authorizationHeader: string, projectId: string) {
const [clientId, clientSecret] = decodeBasicAuthorizationHeader(authorizationHeader)!;
@ -15,7 +15,7 @@ async function validateAndGetTransferInfo(authorizationHeader: string, projectId
const provisionedProject = await prismaClient.provisionedProject.findUnique({
where: {
projectId,
neonClientId: clientId,
externalProjectId: clientId,
},
});
if (!provisionedProject) {
@ -84,12 +84,12 @@ export const POST = createSmartRouteHandler({
handler: async (req) => {
const { provisionedProject } = await validateAndGetTransferInfo(req.headers.authorization[0], req.body.project_id);
const transferCodeObj = await neonIntegrationProjectTransferCodeHandler.createCode({
const transferCodeObj = await integrationProjectTransferCodeHandler.createCode({
tenancy: await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID),
method: {},
data: {
project_id: provisionedProject.projectId,
neon_client_id: provisionedProject.neonClientId,
external_project_id: provisionedProject.externalProjectId,
},
callbackUrl: new URL("/integrations/custom/projects/transfer/confirm", getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL")),
expiresInMs: 1000 * 60 * 60,

View File

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

View File

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

View File

@ -54,7 +54,7 @@ export const POST = createSmartRouteHandler({
await prismaClient.provisionedProject.create({
data: {
projectId: createdProject.id,
neonClientId: clientId,
externalProjectId: clientId,
},
});

View File

@ -34,7 +34,7 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH
const provisionedProjects = await prismaClient.provisionedProject.findMany({
where: {
projectId: data.project_id,
neonClientId: data.neon_client_id,
externalProjectId: data.neon_client_id,
},
});
if (provisionedProjects.length === 0) throw new StatusError(400, "The project to transfer was not provisioned by Neon or has already been transferred.");
@ -48,7 +48,7 @@ export const neonIntegrationProjectTransferCodeHandler = createVerificationCodeH
const provisionedProject = await tx.provisionedProject.deleteMany({
where: {
projectId: data.project_id,
neonClientId: data.neon_client_id,
externalProjectId: data.neon_client_id,
},
});

View File

@ -15,7 +15,7 @@ async function validateAndGetTransferInfo(authorizationHeader: string, projectId
const provisionedProject = await prismaClient.provisionedProject.findUnique({
where: {
projectId,
neonClientId: clientId,
externalProjectId: clientId,
},
});
if (!provisionedProject) {
@ -89,7 +89,7 @@ export const POST = createSmartRouteHandler({
method: {},
data: {
project_id: provisionedProject.projectId,
neon_client_id: provisionedProject.neonClientId,
neon_client_id: provisionedProject.externalProjectId,
},
callbackUrl: new URL("/integrations/neon/projects/transfer/confirm", getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL")),
expiresInMs: 1000 * 60 * 60,

View File

@ -12,7 +12,7 @@ async function authorizePart1(redirectUri: string = "http://localhost:30000/api/
response_type: "code",
client_id: "custom-local",
redirect_uri: redirectUri,
state: encodeBase64Url(new TextEncoder().encode(JSON.stringify({ details: { neon_project_name: 'custom-project' } }))),
state: encodeBase64Url(new TextEncoder().encode(JSON.stringify({ details: { external_project_name: 'custom-project' } }))),
code_challenge: "xf6HY7PIgoaCf_eMniSt-45brYE2J_05C9BnfIbueik",
code_challenge_method: "S256",
},
@ -95,7 +95,7 @@ async function authorize(projectId: string) {
"status": 307,
"body": "http://localhost:8101/integrations/custom/confirm?interaction_uid=%3Cstripped+query+param%3E&amp=",
"headers": Headers {
"location": "http://localhost:8101/integrations/custom/confirm?interaction_uid=%3Cstripped+query+param%3E&neon_project_name=custom-project",
"location": "http://localhost:8101/integrations/custom/confirm?interaction_uid=%3Cstripped+query+param%3E&external_project_name=custom-project",
<some fields may have been hidden>,
},
},
@ -179,7 +179,7 @@ it(`should not redirect to the incorrect callback URL`, async ({}) => {
"error": "invalid_redirect_uri",
"error_description": "redirect_uri did not match any of the client's registered redirect_uris",
"iss": "http://localhost:8102/api/v1/integrations/custom/oauth/idp",
"state": "eyJkZXRhaWxzIjp7Im5lb25fcHJvamVjdF9uYW1lIjoiY3VzdG9tLXByb2plY3QifX0",
"state": "eyJkZXRhaWxzIjp7ImV4dGVybmFsX3Byb2plY3RfbmFtZSI6ImN1c3RvbS1wcm9qZWN0In19",
},
"headers": Headers { <some fields may have been hidden> },
},
@ -233,7 +233,7 @@ it(`should exchange the authorization code for an admin API key that works`, asy
"items": [
{
"created_at_millis": <stripped field 'created_at_millis'>,
"description": "Auto-generated for Neon",
"description": "Auto-generated for an external project",
"expires_at_millis": <stripped field 'expires_at_millis'>,
"id": "<stripped UUID>",
"super_secret_admin_key": { "last_four": <stripped field 'last_four'> },

View File

@ -95,7 +95,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_name=neon-project",
"location": "http://localhost:8101/integrations/neon/confirm?interaction_uid=%3Cstripped+query+param%3E&external_project_name=neon-project",
<some fields may have been hidden>,
},
},
@ -233,7 +233,7 @@ it(`should exchange the authorization code for an admin API key that works`, asy
"items": [
{
"created_at_millis": <stripped field 'created_at_millis'>,
"description": "Auto-generated for Neon",
"description": "Auto-generated for an external project",
"expires_at_millis": <stripped field 'expires_at_millis'>,
"id": "<stripped UUID>",
"super_secret_admin_key": { "last_four": <stripped field 'last_four'> },