Skip to content
Open
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
43 changes: 24 additions & 19 deletions packages/auth-server/src/app/api/cloud-dev/create/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,34 @@ export async function POST(req: NextRequest) {
throw new Error(`IPv4 allocation failed — machine would be unreachable`);
}

// 4. Set secrets (env vars) — staged so they apply when machine starts
const secrets = {
// 4. Insert into dev_machines + dev_machine_members (before secrets so we can include WORKSPACE_ID)
const insertResult = await db.query(
`INSERT INTO dev_machines (app_name, fly_app_name, app_url, created_by, ssh_private_key)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[appName, flyAppName, appUrl, userId, sshKeypair.privateKey],
);

const machineDbId = insertResult.rows[0].id as number;

await db.query(
`INSERT INTO dev_machine_members (machine_id, user_id, role, linux_user)
VALUES ($1, $2, 'owner', $3)`,
[machineDbId, userId, linuxUser],
);

// 5. Set secrets (env vars) — staged so they apply when machine starts
const secrets: Record<string, string> = {
...envVars,
APP_NAME: appName,
DEV_USER: linuxUser,
SSH_PUBLIC_KEY: sshKeypair.publicKey,
WORKSPACE_ID: String(machineDbId),
};
// Inject auth-server's public URL so the cloud machine calls back to us
if (process.env.PUBLIC_URL) {
secrets.OPFLOW_SERVER_URL = process.env.PUBLIC_URL;
}
const secretsFile = join(tmpdir(), `secrets-${flyAppName}.env`);
const secretsContent = Object.entries(secrets)
.map(([k, v]) => `${k}=${v}`)
Expand All @@ -219,7 +240,7 @@ export async function POST(req: NextRequest) {
try { unlinkSync(secretsFile); } catch { /* ignore */ }
}

// 5. Create machine via Fly Machines API
// 6. Create machine via Fly Machines API
const machineConfig: CreateMachineConfig = {
image: CLOUD_DEV_IMAGE,
services: [
Expand Down Expand Up @@ -261,22 +282,6 @@ export async function POST(req: NextRequest) {
`[cloud-dev/create] Machine created: ${machine.id} for ${flyAppName}`,
);

// 6. Insert into dev_machines + dev_machine_members
const insertResult = await db.query(
`INSERT INTO dev_machines (app_name, fly_app_name, app_url, created_by, ssh_private_key)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[appName, flyAppName, appUrl, userId, sshKeypair.privateKey],
);

const machineDbId = insertResult.rows[0].id as number;

await db.query(
`INSERT INTO dev_machine_members (machine_id, user_id, role, linux_user)
VALUES ($1, $2, 'owner', $3)`,
[machineDbId, userId, linuxUser],
);

return NextResponse.json({
data: { appUrl, flyAppName, sshPrivateKey: sshKeypair.privateKey },
});
Expand Down
87 changes: 87 additions & 0 deletions packages/auth-server/src/app/api/connections/credentials/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { type NextRequest, NextResponse } from "next/server";
import { authenticateRequest } from "@/lib/auth";
import { getNango } from "@/lib/nango";
import { verifyWorkspaceMembership } from "@/lib/workspace";

/**
* GET /api/connections/credentials?integration_id=X&connection_id=Y
* Fetch actual credentials from Nango for a connection.
* Authorization is based on workspace membership via connection tags.
* Returns { token, connectionConfig, raw }.
*/
export async function GET(req: NextRequest) {
const auth = await authenticateRequest(req);
if (auth instanceof NextResponse) return auth;

const integrationId = req.nextUrl.searchParams.get("integration_id");
const connectionId = req.nextUrl.searchParams.get("connection_id");

if (!integrationId) {
return NextResponse.json(
{ error: "integration_id query parameter is required" },
{ status: 400 },
);
}

if (!connectionId) {
return NextResponse.json(
{ error: "connection_id query parameter is required" },
{ status: 400 },
);
}

try {
const nango = getNango();
const connection = await nango.getConnection(integrationId, connectionId);

// Extract workspace-id from connection tags and verify membership
const connAny = connection as unknown as {
tags?: Record<string, string>;
};
const workspaceId = connAny.tags?.["workspace-id"];

if (!workspaceId) {
return NextResponse.json(
{ error: "Connection not found" },
{ status: 404 },
);
}

const membership = await verifyWorkspaceMembership(
auth.userId,
workspaceId,
);
if (!membership) {
return NextResponse.json(
{ error: "Connection not found" },
{ status: 404 },
);
}

const creds = (connection.credentials ?? {}) as Record<string, unknown>;
const token =
(creds.access_token ??
creds.api_key ??
creds.apiKey ??
creds.token ??
"") as string;

return NextResponse.json({
data: {
token,
connectionConfig: connection.connection_config ?? {},
raw: creds,
},
});
} catch (err) {
return NextResponse.json(
{
error:
err instanceof Error
? err.message
: "Failed to fetch credentials",
},
{ status: 500 },
);
}
}

This file was deleted.

This file was deleted.

40 changes: 0 additions & 40 deletions packages/auth-server/src/app/api/nango/connect-session/route.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { type NextRequest, NextResponse } from "next/server";
import { authenticateRequest } from "@/lib/auth";
import { getNango, listConnectionsByTags } from "@/lib/nango";
import { verifyWorkspaceMembership } from "@/lib/workspace";

type RouteContext = { params: Promise<{ workspaceId: string }> };

/**
* GET /api/workspaces/{workspaceId}/connections
* List all connections for a workspace across all integrations.
*/
export async function GET(req: NextRequest, { params }: RouteContext) {
const auth = await authenticateRequest(req);
if (auth instanceof NextResponse) return auth;

const { workspaceId } = await params;

const membership = await verifyWorkspaceMembership(auth.userId, workspaceId);
if (!membership) {
return NextResponse.json(
{ error: "Not a member of this workspace" },
{ status: 403 },
);
}

try {
const connections = await listConnectionsByTags({
"workspace-id": workspaceId,
});

return NextResponse.json({ data: connections });
} catch (err) {
return NextResponse.json(
{
error:
err instanceof Error ? err.message : "Failed to list connections",
},
{ status: 500 },
);
}
}

/**
* POST /api/workspaces/{workspaceId}/connections
* Create a new connection via Nango Connect OAuth session.
* Body: { integration_id: string }
* Returns: { token: string }
*/
export async function POST(req: NextRequest, { params }: RouteContext) {
const auth = await authenticateRequest(req);
if (auth instanceof NextResponse) return auth;

const { workspaceId } = await params;

const membership = await verifyWorkspaceMembership(auth.userId, workspaceId);
if (!membership) {
return NextResponse.json(
{ error: "Not a member of this workspace" },
{ status: 403 },
);
}

const body = (await req.json()) as { integration_id?: string };

if (!body.integration_id) {
return NextResponse.json(
{ error: "integration_id is required" },
{ status: 400 },
);
}

try {
const nango = getNango();
const session = await nango.createConnectSession({
tags: {
"user-id": auth.userId,
"workspace-id": workspaceId,
},
allowed_integrations: [body.integration_id],
});

return NextResponse.json({
data: { token: session.data.token },
});
} catch (err) {
return NextResponse.json(
{
error:
err instanceof Error
? err.message
: "Failed to create connect session",
},
{ status: 500 },
);
}
}
Loading