Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ next-env.d.ts
# fuma-docs
/.source

# llm
CLAUDE.md

# tooling
Expand Down
98 changes: 47 additions & 51 deletions bun.lock

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ const config = {
reactStrictMode: true,
experimental: {
reactCompiler: true,
ppr: true,
staleTimes: {
dynamic: 180,
static: 180,
},
useCache: true,
serverActions: {
bodySizeLimit: '5mb',
},
authInterrupts: true,
cacheLife: {
teamSettings: {
stale: 180,
revalidate: 3600,
expire: 86400,
},
}
},
logging: {
fetches: {
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"scripts": {
"<<<<<<< Next.js": "",
"dev": "bun scripts:check-app-env && next dev --turbopack | pino-pretty --colorize",
"build": "next build",
"start": "next start",
"preview": "next build && next start | pino-pretty --colorize",
"build": "next build --turbopack",
"start": "next start | pino-pretty --colorize",
"preview": "next build --turbopack && next start | pino-pretty --colorize",
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --write .",
Expand Down Expand Up @@ -107,7 +107,7 @@
"micromatch": "^4.0.8",
"motion": "^12.18.1",
"nanoid": "^5.0.9",
"next": "15.3.0-canary.23",
"next": "^15.5.5",
"next-safe-action": "^8.0.11",
"next-themes": "^0.4.6",
"nuqs": "^2.7.0",
Expand All @@ -116,9 +116,9 @@
"pino": "^9.7.0",
"postgres": "^3.4.5",
"posthog-js": "^1.268.1",
"react": "^19.1.0",
"react": "^19.1.1",
"react-day-picker": "^9.9.0",
"react-dom": "^19.1.0",
"react-dom": "^19.1.1",
"react-error-boundary": "^5.0.0",
"react-hook-form": "^7.65.0",
"react-icons": "^5.4.0",
Expand Down Expand Up @@ -152,11 +152,10 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/bun": "^1.2.5",
"pino-pretty": "^13.1.1",
"@types/node": "22.10.10",
"@types/pg": "^8.11.11",
"@types/react": "^19.0.8",
"@types/react-dom": "19.0.3",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@types/semver": "^7.7.0",
"@vitest/coverage-v8": "^3.0.7",
"@vitest/ui": "3.0.7",
Expand All @@ -169,6 +168,7 @@
"eslint-plugin-prettier": "^5.2.3",
"node-loader": "^2.1.0",
"openapi-typescript": "^7.8.0",
"pino-pretty": "^13.1.1",
"postcss": "8.5.1",
"postcss-import": "^16.1.0",
"prettier": "^3.4.2",
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/integration/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { COOKIE_KEYS } from '@/configs/keys'
import { COOKIE_KEYS } from '@/configs/cookies'
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { kv } from '@/lib/clients/kv'
import { supabaseAdmin } from '@/lib/clients/supabase/admin'
Expand Down Expand Up @@ -213,7 +213,7 @@

// Verify: Check that user is redirected to their default team
const redirectCalls = vi.mocked(NextResponse.redirect).mock.calls
expect(redirectCalls.length).toBeGreaterThan(0)

Check failure on line 216 in src/__test__/integration/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Integration Tests

src/__test__/integration/middleware.test.ts > Middleware Integration Tests > Authentication Flow > allows authenticated users to access protected routes

AssertionError: expected 0 to be greater than 0 ❯ src/__test__/integration/middleware.test.ts:216:36
if (redirectCalls.length > 0) {
expect(redirectCalls[0]?.[0]?.toString()).toContain('default-team')
}
Expand Down Expand Up @@ -242,7 +242,7 @@

// Verify: User is redirected away from the tampered team
const redirectCalls = vi.mocked(NextResponse.redirect).mock.calls
expect(redirectCalls.length).toBeGreaterThan(0)

Check failure on line 245 in src/__test__/integration/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Integration Tests

src/__test__/integration/middleware.test.ts > Middleware Integration Tests > Team Access Control > handles tampered team cookies securely

AssertionError: expected 0 to be greater than 0 ❯ src/__test__/integration/middleware.test.ts:245:36
if (redirectCalls.length > 0) {
const url = redirectCalls[0]?.[0]?.toString()
expect(url).toContain(PROTECTED_URLS.DASHBOARD)
Expand Down Expand Up @@ -317,7 +317,7 @@

// Verify: User is redirected to their default team
const redirectCalls = vi.mocked(NextResponse.redirect).mock.calls
expect(redirectCalls.length).toBeGreaterThan(0)

Check failure on line 320 in src/__test__/integration/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Integration Tests

src/__test__/integration/middleware.test.ts > Middleware Integration Tests > Team Resolution and Routing > redirects to default team when no team is specified

AssertionError: expected 0 to be greater than 0 ❯ src/__test__/integration/middleware.test.ts:320:36
if (redirectCalls.length > 0) {
expect(redirectCalls[0]?.[0]?.toString()).toContain('default-team')
}
Expand Down Expand Up @@ -353,7 +353,7 @@

// Verify: User is redirected to the new team page
const redirectCalls = vi.mocked(NextResponse.redirect).mock.calls
expect(redirectCalls.length).toBeGreaterThan(0)

Check failure on line 356 in src/__test__/integration/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Integration Tests

src/__test__/integration/middleware.test.ts > Middleware Integration Tests > Team Resolution and Routing > redirects to new team page when user has no teams

AssertionError: expected 0 to be greater than 0 ❯ src/__test__/integration/middleware.test.ts:356:36
if (redirectCalls.length > 0) {
expect(redirectCalls[0]?.[0]?.toString()).toContain(
PROTECTED_URLS.NEW_TEAM
Expand Down Expand Up @@ -393,7 +393,7 @@

// Verify: User is redirected to home on error
const redirectCalls = vi.mocked(NextResponse.redirect).mock.calls
expect(redirectCalls.length).toBeGreaterThan(0)

Check failure on line 396 in src/__test__/integration/middleware.test.ts

View workflow job for this annotation

GitHub Actions / Integration Tests

src/__test__/integration/middleware.test.ts > Middleware Integration Tests > Error Handling > handles database errors gracefully

AssertionError: expected 0 to be greater than 0 ❯ src/__test__/integration/middleware.test.ts:396:36
if (redirectCalls.length > 0) {
expect(redirectCalls[0]?.[0]?.toString()).toContain('/')
}
Expand Down
7 changes: 1 addition & 6 deletions src/app/(auth)/auth/cli/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { l } from '@/lib/clients/logger/logger'
import { createClient } from '@/lib/clients/supabase/server'
import { encodedRedirect } from '@/lib/utils/auth'
import { bailOutFromPPR, generateE2BUserAccessToken } from '@/lib/utils/server'
import { generateE2BUserAccessToken } from '@/lib/utils/server'
import { getDefaultTeamRelation } from '@/server/auth/get-default-team'
import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert'
import { CloudIcon, LaptopIcon, Link2Icon } from 'lucide-react'
import { redirect } from 'next/navigation'
import { Suspense } from 'react'
import { serializeError } from 'serialize-error'

// Mark route as dynamic to prevent static optimization
export const dynamic = 'force-dynamic'

// Types
type CLISearchParams = Promise<{
next?: string
Expand Down Expand Up @@ -89,8 +86,6 @@ export default async function CLIAuthPage({
}: {
searchParams: CLISearchParams
}) {
bailOutFromPPR()

const { next, state, error } = await searchParams
const supabase = await createClient()

Expand Down
6 changes: 3 additions & 3 deletions src/app/(rewrites)/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sitemap from '@/app/sitemap'
import { constructSitemap } from '@/app/sitemap'
import { ALLOW_SEO_INDEXING } from '@/configs/flags'
import { ROUTE_REWRITE_CONFIG } from '@/configs/rewrites'
import { BASE_URL } from '@/configs/urls'
Expand All @@ -10,8 +10,8 @@ import {
import { NextRequest } from 'next/server'
import { serializeError } from 'serialize-error'

export const revalidate = 900
export const dynamic = 'force-static'
export const revalidate = 900

const REVALIDATE_TIME = 900 // 15 minutes ttl

Expand Down Expand Up @@ -103,7 +103,7 @@ export async function GET(request: NextRequest): Promise<Response> {
}

export async function generateStaticParams() {
const sitemapEntries = await sitemap()
const sitemapEntries = await constructSitemap()

const slugs = sitemapEntries
.filter((entry) => {
Expand Down
6 changes: 3 additions & 3 deletions src/app/(rewrites)/not-found/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import NotFound from '@/ui/not-found'
import { Metadata } from 'next'

export const dynamic = 'force-static'

export const metadata: Metadata = {
title: '404 - Page Not Found',
description:
'The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.',
robots: 'noindex, nofollow',
}

export default function NotFoundShell() {
export default async function NotFoundShell() {
'use cache'

return <NotFound />
}
9 changes: 2 additions & 7 deletions src/app/api/sandbox/details/polling/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use server'

import { COOKIE_KEYS } from '@/configs/keys'
import { COOKIE_KEYS, COOKIE_OPTIONS } from '@/configs/cookies'
import { cookies } from 'next/headers'
import { z } from 'zod'

Expand All @@ -14,12 +14,7 @@ export async function POST(request: Request) {
cookieStore.set(
COOKIE_KEYS.SANDBOX_INSPECT_POLLING_INTERVAL,
body.interval.toString(),
{
path: '/',
maxAge: 60 * 60 * 24 * 7, // 7 days
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
}
COOKIE_OPTIONS[COOKIE_KEYS.SANDBOX_INSPECT_POLLING_INTERVAL]
)

return Response.json({ interval: body.interval })
Expand Down
12 changes: 2 additions & 10 deletions src/app/api/sandbox/inspect/root-path/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
'use server'

import { COOKIE_KEYS } from '@/configs/keys'
import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import { COOKIE_KEYS, COOKIE_OPTIONS } from '@/configs/cookies'
import { cookies } from 'next/headers'
import { z } from 'zod'

const BodySchema = z.object({ path: z.string() })

const COOKIE_SETTINGS: Partial<ResponseCookie> = {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
}

export async function POST(request: Request) {
try {
const body = BodySchema.parse(await request.json())
Expand All @@ -22,7 +14,7 @@ export async function POST(request: Request) {
cookieStore.set(
COOKIE_KEYS.SANDBOX_INSPECT_ROOT_PATH,
body.path,
COOKIE_SETTINGS
COOKIE_OPTIONS[COOKIE_KEYS.SANDBOX_INSPECT_ROOT_PATH]
)

return Response.json({ path: body.path })
Expand Down
12 changes: 2 additions & 10 deletions src/app/api/sidebar/state/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { COOKIE_KEYS } from '@/configs/keys'
import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import { COOKIE_KEYS, COOKIE_OPTIONS } from '@/configs/cookies'
import { cookies } from 'next/headers'
import { z } from 'zod'

const SidebarStateSchema = z.object({
state: z.boolean(),
})

const COOKIE_SETTINGS: Partial<ResponseCookie> = {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
}

export async function POST(request: Request) {
try {
const body = SidebarStateSchema.parse(await request.json())
Expand All @@ -22,7 +14,7 @@ export async function POST(request: Request) {
cookieStore.set(
COOKIE_KEYS.SIDEBAR_STATE,
body.state.toString(),
COOKIE_SETTINGS
COOKIE_OPTIONS[COOKIE_KEYS.SIDEBAR_STATE]
)

return Response.json({ state: body.state })
Expand Down
18 changes: 7 additions & 11 deletions src/app/api/team/state/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { COOKIE_KEYS } from '@/configs/keys'
import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import { COOKIE_KEYS, COOKIE_OPTIONS } from '@/configs/cookies'
import { cookies } from 'next/headers'
import { z } from 'zod'

Expand All @@ -8,24 +7,21 @@ const TeamStateSchema = z.object({
teamSlug: z.string(),
})

const COOKIE_SETTINGS: Partial<ResponseCookie> = {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
}

export async function POST(request: Request) {
try {
const body = TeamStateSchema.parse(await request.json())

const cookieStore = await cookies()

cookieStore.set(COOKIE_KEYS.SELECTED_TEAM_ID, body.teamId, COOKIE_SETTINGS)
cookieStore.set(
COOKIE_KEYS.SELECTED_TEAM_ID,
body.teamId,
COOKIE_OPTIONS[COOKIE_KEYS.SELECTED_TEAM_ID]
)
cookieStore.set(
COOKIE_KEYS.SELECTED_TEAM_SLUG,
body.teamSlug,
COOKIE_SETTINGS
COOKIE_OPTIONS[COOKIE_KEYS.SELECTED_TEAM_SLUG]
)

return Response.json({ success: true })
Expand Down
25 changes: 25 additions & 0 deletions src/app/api/teams/[teamId]/sandboxes/list/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'server-cli-only'

import { getTeamSandboxes } from '@/server/sandboxes/get-team-sandboxes'
import { SandboxesListResponse } from './types'

export async function GET(
_req: Request,
{ params }: RouteContext<'/api/teams/[teamId]/sandboxes/list'>
) {
try {
const { teamId } = await params

const response = await getTeamSandboxes({ teamIdOrSlug: teamId })

if (response?.serverError) {
throw response?.serverError || new Error('Failed to load sandboxes')
}

const data = response?.data || { sandboxes: [] }

return Response.json(data satisfies SandboxesListResponse)
} catch (error) {
return Response.json({ error: 'Internal server error' }, { status: 500 })
}
}
13 changes: 13 additions & 0 deletions src/app/api/teams/[teamId]/sandboxes/list/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SandboxWithMetrics } from '@/features/dashboard/sandboxes/list/table-config'
import { TeamIdOrSlugSchema } from '@/lib/schemas/team'
import { z } from 'zod'

export const SandboxesListRequestSchema = z.object({
teamId: TeamIdOrSlugSchema,
})

export type SandboxesListRequest = z.infer<typeof SandboxesListRequestSchema>

export type SandboxesListResponse = {
sandboxes: SandboxWithMetrics[]
}
31 changes: 31 additions & 0 deletions src/app/api/teams/user/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createClient } from '@/lib/clients/supabase/server'
import getUserTeamsMemo from '@/server/team/get-user-teams-memo'
import { UserTeamsResponse } from './types'

export async function GET() {
try {
const supabase = await createClient()
const {
data: { user },
error,
} = await supabase.auth.getUser()

if (error || !user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}

const teams = await getUserTeamsMemo(user)

return Response.json({ teams } satisfies UserTeamsResponse)
} catch (error) {
if (
error instanceof Error &&
error.message.includes('During prerendering')
) {
throw error
}

console.error('Error fetching user teams:', error)
return Response.json({ error: 'Internal server error' }, { status: 500 })
}
}
3 changes: 3 additions & 0 deletions src/app/api/teams/user/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ClientTeam } from '@/types/dashboard.types'

export type UserTeamsResponse = { teams: ClientTeam[] }
1 change: 1 addition & 0 deletions src/app/layout.client.tsx → src/app/body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export function Body({

export function useMode(): string | undefined {
const { slug } = useParams()

return Array.isArray(slug) && slug.length > 0 ? slug[0] : undefined
}
16 changes: 0 additions & 16 deletions src/app/dashboard/@header/[teamIdOrSlug]/[...slug]/page.tsx

This file was deleted.

4 changes: 0 additions & 4 deletions src/app/dashboard/@header/[teamIdOrSlug]/default.tsx

This file was deleted.

3 changes: 0 additions & 3 deletions src/app/dashboard/@header/account/page.tsx

This file was deleted.

3 changes: 0 additions & 3 deletions src/app/dashboard/@header/default.tsx

This file was deleted.

Loading
Loading