Compass mock mode - runs locally without WorkOS auth
Some checks failed
Tests / E2E Web (chromium) (push) Has been cancelled
Tests / Coverage (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
Tests / Unit Tests (push) Has been cancelled
Tests / E2E Web (firefox) (push) Has been cancelled
Tests / E2E Web (webkit) (push) Has been cancelled
Tests / E2E Desktop (macos-latest) (push) Has been cancelled
Tests / E2E Desktop (ubuntu-latest) (push) Has been cancelled
Tests / E2E Desktop (windows-latest) (push) Has been cancelled
Some checks failed
Tests / E2E Web (chromium) (push) Has been cancelled
Tests / Coverage (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
Tests / Unit Tests (push) Has been cancelled
Tests / E2E Web (firefox) (push) Has been cancelled
Tests / E2E Web (webkit) (push) Has been cancelled
Tests / E2E Desktop (macos-latest) (push) Has been cancelled
Tests / E2E Desktop (ubuntu-latest) (push) Has been cancelled
Tests / E2E Desktop (windows-latest) (push) Has been cancelled
- Middleware bypasses WorkOS when API keys not configured - AuthWrapper conditionally loads AuthKitProvider - getCurrentUser() returns mock dev user when no auth - Demo mode (/demo) sets cookie for dashboard access - Memory DB provider fallback when D1 unavailable - CF dev proxy gated behind ENABLE_CF_DEV=1 - All changes conditional - real auth works if keys provided
This commit is contained in:
parent
0198898979
commit
b80ffee3f7
@ -27,8 +27,19 @@ export default nextConfig;
|
||||
// Enable calling `getCloudflareContext()` in `next dev`.
|
||||
// See https://opennext.js.org/cloudflare/bindings#local-access-to-bindings.
|
||||
// Only init in dev -- build and lint don't need the wrangler proxy.
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
// Disabled for local dev without Cloudflare account access:
|
||||
if (process.env.NODE_ENV === "development" && process.env.ENABLE_CF_DEV === "1") {
|
||||
import("@opennextjs/cloudflare").then((mod) =>
|
||||
mod.initOpenNextCloudflareForDev()
|
||||
);
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
// When Cloudflare dev proxy is not enabled, set a mock context so
|
||||
// getCloudflareContext() doesn't throw. Actions check `env?.DB` and
|
||||
// gracefully return empty data when it's missing.
|
||||
const sym = Symbol.for("__cloudflare-context__");
|
||||
(globalThis as Record<symbol, unknown>)[sym] = {
|
||||
env: {}, // no DB binding — actions will short-circuit
|
||||
cf: {},
|
||||
ctx: { waitUntil: () => {}, passThroughOnException: () => {} },
|
||||
};
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ export async function getCustomDashboards(): Promise<
|
||||
if (!user) return { success: false, error: "not authenticated" }
|
||||
|
||||
const { env } = await getCloudflareContext()
|
||||
if (!env?.DB) return { success: true, data: [] }
|
||||
const db = getDb(env.DB)
|
||||
|
||||
const dashboards = await db.query.customDashboards.findMany({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Sora, IBM_Plex_Mono, Playfair_Display } from "next/font/google";
|
||||
import { AuthKitProvider } from "@workos-inc/authkit-nextjs/components";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { AuthWrapper } from "@/components/auth-wrapper";
|
||||
import "./globals.css";
|
||||
|
||||
const sora = Sora({
|
||||
@ -44,11 +44,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${sora.variable} ${ibmPlexMono.variable} ${playfair.variable} font-sans antialiased`}>
|
||||
<AuthKitProvider>
|
||||
<AuthWrapper>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</AuthKitProvider>
|
||||
</AuthWrapper>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
26
src/components/auth-wrapper.tsx
Normal file
26
src/components/auth-wrapper.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
/**
|
||||
* Server component that conditionally renders the real AuthKitProvider
|
||||
* (when WorkOS is configured) or a simple passthrough (demo/local mode).
|
||||
*
|
||||
* This avoids importing @workos-inc/authkit-nextjs/components when
|
||||
* WORKOS_API_KEY is empty, which would throw NoApiKeyProvidedException.
|
||||
*/
|
||||
|
||||
const isWorkOSConfigured =
|
||||
!!process.env.WORKOS_API_KEY &&
|
||||
!!process.env.WORKOS_CLIENT_ID &&
|
||||
!process.env.WORKOS_API_KEY.includes("placeholder")
|
||||
|
||||
export async function AuthWrapper({ children }: { children: ReactNode }) {
|
||||
if (isWorkOSConfigured) {
|
||||
// Dynamic import so the module is never loaded when WorkOS is absent
|
||||
const { AuthKitProvider } = await import(
|
||||
"@workos-inc/authkit-nextjs/components"
|
||||
)
|
||||
return <AuthKitProvider>{children}</AuthKitProvider>
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@ -23,9 +23,36 @@ const allSchemas = {
|
||||
...conversationsSchema,
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe stub returned when no D1 binding is available (local dev without CF).
|
||||
* Every property access returns a chainable proxy that resolves to empty results,
|
||||
* so server actions that forget to check `env?.DB` won't crash.
|
||||
*/
|
||||
function createNullDb(): ReturnType<typeof drizzle> {
|
||||
const handler: ProxyHandler<object> = {
|
||||
get(_target, prop) {
|
||||
// .then — make the proxy non-thenable so `await proxy` returns the proxy itself
|
||||
if (prop === "then") return undefined
|
||||
// Common drizzle terminal methods — resolve to empty/noop
|
||||
if (prop === "all" || prop === "values") return async () => []
|
||||
if (prop === "get") return async () => undefined
|
||||
if (prop === "run") return async () => ({ changes: 0 })
|
||||
if (prop === "execute") return async () => []
|
||||
// findMany / findFirst on the relational query builder
|
||||
if (prop === "findMany") return async () => []
|
||||
if (prop === "findFirst") return async () => undefined
|
||||
// Everything else returns another proxy so chaining works:
|
||||
// db.select().from(t).where(...) etc.
|
||||
return new Proxy((..._args: unknown[]) => new Proxy({}, handler), handler)
|
||||
},
|
||||
}
|
||||
return new Proxy({}, handler) as ReturnType<typeof drizzle>
|
||||
}
|
||||
|
||||
// Legacy function - kept for backwards compatibility
|
||||
// Prefer using the provider interface from ./provider for new code
|
||||
export function getDb(d1: D1Database) {
|
||||
if (!d1) return createNullDb()
|
||||
return drizzle(d1, { schema: allSchemas })
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { withAuth, signOut } from "@workos-inc/authkit-nextjs"
|
||||
import { getCloudflareContext } from "@opennextjs/cloudflare"
|
||||
import { getDb } from "@/db"
|
||||
import { users, organizations, organizationMembers } from "@/db/schema"
|
||||
@ -91,6 +90,7 @@ export async function getCurrentUser(): Promise<AuthUser | null> {
|
||||
}
|
||||
|
||||
// WorkOS is configured -- try real auth first
|
||||
const { withAuth } = await import("@workos-inc/authkit-nextjs")
|
||||
const session = await withAuth()
|
||||
|
||||
if (!session || !session.user) {
|
||||
@ -281,6 +281,7 @@ export async function ensureUserExists(workosUser: {
|
||||
}
|
||||
|
||||
export async function handleSignOut() {
|
||||
const { signOut } = await import("@workos-inc/authkit-nextjs")
|
||||
await signOut()
|
||||
}
|
||||
|
||||
@ -302,6 +303,7 @@ export async function requireEmailVerified(): Promise<AuthUser> {
|
||||
!process.env.WORKOS_API_KEY.includes("placeholder")
|
||||
|
||||
if (isWorkOSConfigured) {
|
||||
const { withAuth } = await import("@workos-inc/authkit-nextjs")
|
||||
const session = await withAuth()
|
||||
if (session?.user && !session.user.emailVerified) {
|
||||
throw new Error("Email not verified")
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { NextRequest } from "next/server"
|
||||
import { authkit, handleAuthkitHeaders } from "@workos-inc/authkit-nextjs"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
// public routes that don't require authentication
|
||||
const publicPaths = [
|
||||
@ -31,7 +30,48 @@ function isPublicPath(pathname: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* When WorkOS is not configured, skip authkit entirely and passthrough.
|
||||
* Protected routes require the demo cookie; otherwise redirect to login.
|
||||
*/
|
||||
function handleNoWorkOS(request: NextRequest): NextResponse {
|
||||
const { pathname } = request.nextUrl
|
||||
|
||||
if (isPublicPath(pathname)) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const hasDemoCookie =
|
||||
request.cookies.get("compass-demo")?.value === "true"
|
||||
|
||||
if (hasDemoCookie) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// no session & not demo — redirect to login
|
||||
const loginUrl = new URL("/login", request.url)
|
||||
loginUrl.searchParams.set("from", pathname)
|
||||
return NextResponse.redirect(loginUrl)
|
||||
}
|
||||
|
||||
export default async function middleware(request: NextRequest) {
|
||||
// When WorkOS is not configured, skip authkit entirely to avoid
|
||||
// NoApiKeyProvidedException. The app falls back to demo/dev user
|
||||
// via src/lib/auth.ts.
|
||||
const isWorkOSConfigured =
|
||||
process.env.WORKOS_API_KEY &&
|
||||
process.env.WORKOS_CLIENT_ID &&
|
||||
!process.env.WORKOS_API_KEY.includes("placeholder")
|
||||
|
||||
if (!isWorkOSConfigured) {
|
||||
return handleNoWorkOS(request)
|
||||
}
|
||||
|
||||
// --- WorkOS IS configured: use authkit as normal ---
|
||||
const { authkit, handleAuthkitHeaders } = await import(
|
||||
"@workos-inc/authkit-nextjs"
|
||||
)
|
||||
|
||||
const { pathname } = request.nextUrl
|
||||
|
||||
// get session and headers from authkit (handles token refresh automatically)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user