Skip to content

Commit a87eec4

Browse files
committed
Fix migration
1 parent 9c898ae commit a87eec4

8 files changed

Lines changed: 1269 additions & 16174 deletions

File tree

apps/sim/app/api/admin/mothership/route.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ const ENV_URLS: Record<string, string | undefined> = {
1616
prod: env.MOTHERSHIP_PROD_URL,
1717
}
1818

19-
async function getMothershipUrl(environment: string): Promise<string | null> {
19+
async function getMothershipUrl(environment: string, userId: string): Promise<string | null> {
2020
const parsedEnvironment = mothershipEnvironmentSchema.safeParse(environment)
2121
if (!parsedEnvironment.success) return ENV_URLS[environment] ?? null
2222

2323
return getMothershipBaseURL({
24+
userId,
2425
environment: parsedEnvironment.data,
2526
fallbackUrl: ENV_URLS[environment],
2627
})
@@ -34,9 +35,9 @@ function isValidEndpoint(endpoint: string): boolean {
3435
return ENDPOINT_PATTERN.test(endpoint)
3536
}
3637

37-
async function isAdminRequestAuthorized() {
38+
async function getAuthorizedAdminUserId() {
3839
const session = await getSession()
39-
if (!session?.user?.id) return false
40+
if (!session?.user?.id) return null
4041

4142
const [currentUser] = await db
4243
.select({
@@ -48,7 +49,8 @@ async function isAdminRequestAuthorized() {
4849
.where(eq(user.id, session.user.id))
4950
.limit(1)
5051

51-
return currentUser?.role === 'admin' && (currentUser.superUserModeEnabled ?? false)
52+
const authorized = currentUser?.role === 'admin' && (currentUser.superUserModeEnabled ?? false)
53+
return authorized ? session.user.id : null
5254
}
5355

5456
/**
@@ -62,7 +64,8 @@ async function isAdminRequestAuthorized() {
6264
* (e.g. requestId for GET /traces) are forwarded.
6365
*/
6466
export const POST = withRouteHandler(async (req: NextRequest) => {
65-
if (!(await isAdminRequestAuthorized())) {
67+
const userId = await getAuthorizedAdminUserId()
68+
if (!userId) {
6669
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
6770
}
6871

@@ -80,7 +83,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
8083
return NextResponse.json({ error: 'invalid endpoint' }, { status: 400 })
8184
}
8285

83-
const baseUrl = await getMothershipUrl(environment)
86+
const baseUrl = await getMothershipUrl(environment, userId)
8487
if (!baseUrl) {
8588
return NextResponse.json(
8689
{ error: `No URL configured for environment: ${environment}` },
@@ -114,7 +117,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
114117
})
115118

116119
export const GET = withRouteHandler(async (req: NextRequest) => {
117-
if (!(await isAdminRequestAuthorized())) {
120+
const userId = await getAuthorizedAdminUserId()
121+
if (!userId) {
118122
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
119123
}
120124

@@ -132,7 +136,7 @@ export const GET = withRouteHandler(async (req: NextRequest) => {
132136
return NextResponse.json({ error: 'invalid endpoint' }, { status: 400 })
133137
}
134138

135-
const baseUrl = await getMothershipUrl(environment)
139+
const baseUrl = await getMothershipUrl(environment, userId)
136140
if (!baseUrl) {
137141
return NextResponse.json(
138142
{ error: `No URL configured for environment: ${environment}` },
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { getMothershipBaseURL } from './agent-url'
3+
4+
const { dbMock, mockRows } = vi.hoisted(() => {
5+
const mockRows: any[] = []
6+
const dbMock = {
7+
select: vi.fn(() => ({
8+
from: vi.fn(() => ({
9+
leftJoin: vi.fn(() => ({
10+
where: vi.fn(() => ({
11+
limit: vi.fn(async () => mockRows),
12+
})),
13+
})),
14+
})),
15+
})),
16+
}
17+
return { dbMock, mockRows }
18+
})
19+
20+
vi.mock('@sim/db', () => ({ db: dbMock }))
21+
vi.mock('@sim/db/schema', () => ({
22+
settings: {
23+
userId: 'settings.userId',
24+
superUserModeEnabled: 'settings.superUserModeEnabled',
25+
mothershipEnvironment: 'settings.mothershipEnvironment',
26+
},
27+
user: {
28+
id: 'user.id',
29+
role: 'user.role',
30+
},
31+
}))
32+
vi.mock('drizzle-orm', () => ({
33+
eq: vi.fn(() => ({})),
34+
}))
35+
vi.mock('@/lib/api/contracts', () => ({
36+
mothershipEnvironmentSchema: {
37+
safeParse: (value: unknown) =>
38+
['default', 'dev', 'staging', 'prod'].includes(String(value))
39+
? { success: true, data: value }
40+
: { success: false },
41+
},
42+
}))
43+
vi.mock('@/lib/copilot/constants', () => ({
44+
SIM_AGENT_API_URL: 'https://default.mothership.test',
45+
SIM_AGENT_API_URL_DEFAULT: 'https://fallback.mothership.test',
46+
}))
47+
vi.mock('@/lib/core/config/env', () => ({
48+
env: {
49+
COPILOT_DEV_URL: 'https://dev.mothership.test',
50+
COPILOT_STAGING_URL: 'https://staging.mothership.test',
51+
COPILOT_PROD_URL: 'https://prod.mothership.test',
52+
},
53+
}))
54+
55+
describe('getMothershipBaseURL', () => {
56+
beforeEach(() => {
57+
mockRows.length = 0
58+
dbMock.select.mockClear()
59+
})
60+
61+
it('uses the default URL when there is no user context', async () => {
62+
await expect(getMothershipBaseURL()).resolves.toBe('https://default.mothership.test')
63+
await expect(getMothershipBaseURL({ environment: 'dev' })).resolves.toBe(
64+
'https://default.mothership.test'
65+
)
66+
})
67+
68+
it('ignores stored and explicit environments for non-admin users', async () => {
69+
mockRows.push({
70+
role: 'user',
71+
superUserModeEnabled: true,
72+
mothershipEnvironment: 'dev',
73+
})
74+
75+
await expect(getMothershipBaseURL({ userId: 'user-1', environment: 'staging' })).resolves.toBe(
76+
'https://default.mothership.test'
77+
)
78+
})
79+
80+
it('ignores stored and explicit environments when super user mode is off', async () => {
81+
mockRows.push({
82+
role: 'admin',
83+
superUserModeEnabled: false,
84+
mothershipEnvironment: 'dev',
85+
})
86+
87+
await expect(getMothershipBaseURL({ userId: 'admin-1', environment: 'prod' })).resolves.toBe(
88+
'https://default.mothership.test'
89+
)
90+
})
91+
92+
it('uses default for super admins until they select a concrete environment', async () => {
93+
mockRows.push({
94+
role: 'admin',
95+
superUserModeEnabled: true,
96+
mothershipEnvironment: 'default',
97+
})
98+
99+
await expect(getMothershipBaseURL({ userId: 'admin-1' })).resolves.toBe(
100+
'https://default.mothership.test'
101+
)
102+
})
103+
104+
it('allows effective super admins to use a selected environment', async () => {
105+
mockRows.push({
106+
role: 'admin',
107+
superUserModeEnabled: true,
108+
mothershipEnvironment: 'dev',
109+
})
110+
111+
await expect(getMothershipBaseURL({ userId: 'admin-1' })).resolves.toBe(
112+
'https://dev.mothership.test'
113+
)
114+
await expect(getMothershipBaseURL({ userId: 'admin-1', environment: 'staging' })).resolves.toBe(
115+
'https://staging.mothership.test'
116+
)
117+
})
118+
})

apps/sim/lib/copilot/server/agent-url.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface GetMothershipBaseURLOptions {
1414
type ConcreteMothershipEnvironment = Exclude<MothershipEnvironment, 'default'>
1515

1616
const ENVIRONMENT_URLS: Record<ConcreteMothershipEnvironment, string | undefined> = {
17-
// env vars
1817
dev: env.COPILOT_DEV_URL,
1918
staging: env.COPILOT_STAGING_URL,
2019
prod: env.COPILOT_PROD_URL,
@@ -40,10 +39,6 @@ export async function getMothershipBaseURL(
4039
): Promise<string> {
4140
const defaultUrl = getDefaultMothershipBaseURL(options.fallbackUrl)
4241

43-
if (options.environment) {
44-
return getConfiguredEnvironmentUrl(options.environment) ?? defaultUrl
45-
}
46-
4742
const { userId } = options
4843
if (!userId) return defaultUrl
4944

@@ -61,7 +56,8 @@ export async function getMothershipBaseURL(
6156
const effectiveSuperUser = row?.role === 'admin' && (row.superUserModeEnabled ?? false)
6257
if (!effectiveSuperUser) return defaultUrl
6358

64-
const parsedEnvironment = mothershipEnvironmentSchema.safeParse(row.mothershipEnvironment)
59+
const selectedEnvironment = options.environment ?? row.mothershipEnvironment
60+
const parsedEnvironment = mothershipEnvironmentSchema.safeParse(selectedEnvironment)
6561
const environment = parsedEnvironment.success ? parsedEnvironment.data : 'default'
6662

6763
return getConfiguredEnvironmentUrl(environment) ?? defaultUrl
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ALTER TABLE "settings" ADD COLUMN "mothership_environment" text DEFAULT 'prod' NOT NULL;
1+
ALTER TABLE "settings" ADD COLUMN "mothership_environment" text DEFAULT 'default' NOT NULL;

packages/db/migrations/0206_amazing_maximus.sql

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)