-
Notifications
You must be signed in to change notification settings - Fork 3.5k
feat(mothership): #4090
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
feat(mothership): #4090
Changes from all commits
2d2448f
1fc84b8
e2de4d2
b6e1df4
acc00df
cac100a
0d09d11
0b3f3ed
0a41b8b
d85775e
b74cf28
87ff68c
4ee6fa8
33433b1
ca2afaa
8dfbe8a
c29941e
2da0cbe
c52d633
0dd1ee0
d25632c
5c47c1f
ebc030b
c31ae46
a6fbb51
1a18ebb
674695e
75d5d13
ed2dad0
2b799f3
5b22f1f
5be55d2
89f8842
3893afd
ce1f00c
fd4fa1c
d22f367
b49d67e
f2fcfe7
817833c
2ba4228
81ac66f
2abf6ac
a738a6d
69d69ee
d25c243
6f04c48
b1caeb0
c77f204
e610df6
fe5baf7
f0d3819
0638604
9da574a
649ee9c
3ef87e5
2d2f782
485dce7
f509e33
e321e99
8f3c8e4
7835df4
24abd87
9272b15
3252736
c61cbb0
33d1342
c026ce7
5b94db6
2156f49
c74c4a9
ca361a3
949601c
91301df
734a4d1
e2b4eb3
386d0aa
3690dc2
da28f8a
ac84c62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { db } from '@sim/db' | ||
| import { user } from '@sim/db/schema' | ||
| import { eq } from 'drizzle-orm' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { getSession } from '@/lib/auth' | ||
| import { env } from '@/lib/core/config/env' | ||
|
|
||
| const ENV_URLS: Record<string, string | undefined> = { | ||
| dev: env.MOTHERSHIP_DEV_URL, | ||
| staging: env.MOTHERSHIP_STAGING_URL, | ||
| prod: env.MOTHERSHIP_PROD_URL, | ||
| } | ||
|
|
||
| function getMothershipUrl(environment: string): string | null { | ||
| return ENV_URLS[environment] ?? null | ||
| } | ||
|
|
||
| async function isAdminRequestAuthorized() { | ||
| const session = await getSession() | ||
| if (!session?.user?.id) return false | ||
|
|
||
| const [currentUser] = await db | ||
| .select({ role: user.role }) | ||
| .from(user) | ||
| .where(eq(user.id, session.user.id)) | ||
| .limit(1) | ||
|
|
||
| return currentUser?.role === 'admin' | ||
| } | ||
|
|
||
| /** | ||
| * Proxy to the mothership admin API. | ||
| * | ||
| * Query params: | ||
| * env - "dev" | "staging" | "prod" | ||
| * endpoint - the admin endpoint path, e.g. "requests", "licenses", "traces" | ||
| * | ||
| * The request body (for POST) is forwarded as-is. Additional query params | ||
| * (e.g. requestId for GET /traces) are forwarded. | ||
| */ | ||
| export async function POST(req: NextRequest) { | ||
| if (!(await isAdminRequestAuthorized())) { | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| } | ||
|
|
||
| const adminKey = env.MOTHERSHIP_API_ADMIN_KEY | ||
| if (!adminKey) { | ||
| return NextResponse.json({ error: 'MOTHERSHIP_API_ADMIN_KEY not configured' }, { status: 500 }) | ||
| } | ||
|
|
||
| const { searchParams } = new URL(req.url) | ||
| const environment = searchParams.get('env') || 'dev' | ||
| const endpoint = searchParams.get('endpoint') | ||
|
|
||
| if (!endpoint) { | ||
| return NextResponse.json({ error: 'endpoint query param required' }, { status: 400 }) | ||
| } | ||
|
|
||
| const baseUrl = getMothershipUrl(environment) | ||
| if (!baseUrl) { | ||
| return NextResponse.json( | ||
| { error: `No URL configured for environment: ${environment}` }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const targetUrl = `${baseUrl}/api/admin/${endpoint}` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Endpoint parameter allows path traversal in proxy URLMedium Severity The Additional Locations (1)Reviewed by Cursor Bugbot for commit 949601c. Configure here. |
||
|
|
||
| try { | ||
| const body = await req.text() | ||
| const upstream = await fetch(targetUrl, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'x-api-key': adminKey, | ||
| }, | ||
| ...(body ? { body } : {}), | ||
| }) | ||
|
|
||
| const data = await upstream.json() | ||
| return NextResponse.json(data, { status: upstream.status }) | ||
| } catch (error) { | ||
| return NextResponse.json( | ||
| { | ||
| error: `Failed to reach mothership (${environment}): ${error instanceof Error ? error.message : 'Unknown error'}`, | ||
| }, | ||
| { status: 502 } | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| export async function GET(req: NextRequest) { | ||
| if (!(await isAdminRequestAuthorized())) { | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| } | ||
|
|
||
| const adminKey = env.MOTHERSHIP_API_ADMIN_KEY | ||
| if (!adminKey) { | ||
| return NextResponse.json({ error: 'MOTHERSHIP_API_ADMIN_KEY not configured' }, { status: 500 }) | ||
| } | ||
|
|
||
| const { searchParams } = new URL(req.url) | ||
| const environment = searchParams.get('env') || 'dev' | ||
| const endpoint = searchParams.get('endpoint') | ||
|
|
||
| if (!endpoint) { | ||
| return NextResponse.json({ error: 'endpoint query param required' }, { status: 400 }) | ||
| } | ||
|
|
||
| const baseUrl = getMothershipUrl(environment) | ||
| if (!baseUrl) { | ||
| return NextResponse.json( | ||
| { error: `No URL configured for environment: ${environment}` }, | ||
| { status: 400 } | ||
| ) | ||
| } | ||
|
|
||
| const forwardParams = new URLSearchParams() | ||
| searchParams.forEach((value, key) => { | ||
| if (key !== 'env' && key !== 'endpoint') { | ||
| forwardParams.set(key, value) | ||
| } | ||
| }) | ||
|
|
||
| const qs = forwardParams.toString() | ||
| const targetUrl = `${baseUrl}/api/admin/${endpoint}${qs ? `?${qs}` : ''}` | ||
|
|
||
| try { | ||
| const upstream = await fetch(targetUrl, { | ||
| method: 'GET', | ||
| headers: { 'x-api-key': adminKey }, | ||
| }) | ||
|
|
||
| const data = await upstream.json() | ||
| return NextResponse.json(data, { status: upstream.status }) | ||
| } catch (error) { | ||
| return NextResponse.json( | ||
| { | ||
| error: `Failed to reach mothership (${environment}): ${error instanceof Error ? error.message : 'Unknown error'}`, | ||
| }, | ||
| { status: 502 } | ||
| ) | ||
| } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dev branch skips CI tests before building and deploying
Medium Severity
The
test-buildjob is skipped for pushes to thedevbranch (line 19), andbuild-devonly depends ondetect-version, nottest-build. Combined withmigrate-devrunning migrations after the build, this means untested code gets built into Docker images and database migrations are applied to the dev environment without any test validation.Additional Locations (1)
.github/workflows/ci.yml#L49-L53Reviewed by Cursor Bugbot for commit 949601c. Configure here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bugbot Autofix determined this is a false positive.
This is intentional design: PRs to dev still run tests (via the pull_request condition), only direct pushes to dev skip tests for faster iteration cycles in the dev environment.
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.