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`.
|
// Enable calling `getCloudflareContext()` in `next dev`.
|
||||||
// See https://opennext.js.org/cloudflare/bindings#local-access-to-bindings.
|
// See https://opennext.js.org/cloudflare/bindings#local-access-to-bindings.
|
||||||
// Only init in dev -- build and lint don't need the wrangler proxy.
|
// 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) =>
|
import("@opennextjs/cloudflare").then((mod) =>
|
||||||
mod.initOpenNextCloudflareForDev()
|
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" }
|
if (!user) return { success: false, error: "not authenticated" }
|
||||||
|
|
||||||
const { env } = await getCloudflareContext()
|
const { env } = await getCloudflareContext()
|
||||||
|
if (!env?.DB) return { success: true, data: [] }
|
||||||
const db = getDb(env.DB)
|
const db = getDb(env.DB)
|
||||||
|
|
||||||
const dashboards = await db.query.customDashboards.findMany({
|
const dashboards = await db.query.customDashboards.findMany({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Sora, IBM_Plex_Mono, Playfair_Display } from "next/font/google";
|
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 { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import { AuthWrapper } from "@/components/auth-wrapper";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const sora = Sora({
|
const sora = Sora({
|
||||||
@ -44,11 +44,11 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${sora.variable} ${ibmPlexMono.variable} ${playfair.variable} font-sans antialiased`}>
|
<body className={`${sora.variable} ${ibmPlexMono.variable} ${playfair.variable} font-sans antialiased`}>
|
||||||
<AuthKitProvider>
|
<AuthWrapper>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AuthKitProvider>
|
</AuthWrapper>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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,
|
...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
|
// Legacy function - kept for backwards compatibility
|
||||||
// Prefer using the provider interface from ./provider for new code
|
// Prefer using the provider interface from ./provider for new code
|
||||||
export function getDb(d1: D1Database) {
|
export function getDb(d1: D1Database) {
|
||||||
|
if (!d1) return createNullDb()
|
||||||
return drizzle(d1, { schema: allSchemas })
|
return drizzle(d1, { schema: allSchemas })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { withAuth, signOut } from "@workos-inc/authkit-nextjs"
|
|
||||||
import { getCloudflareContext } from "@opennextjs/cloudflare"
|
import { getCloudflareContext } from "@opennextjs/cloudflare"
|
||||||
import { getDb } from "@/db"
|
import { getDb } from "@/db"
|
||||||
import { users, organizations, organizationMembers } from "@/db/schema"
|
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
|
// WorkOS is configured -- try real auth first
|
||||||
|
const { withAuth } = await import("@workos-inc/authkit-nextjs")
|
||||||
const session = await withAuth()
|
const session = await withAuth()
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
@ -281,6 +281,7 @@ export async function ensureUserExists(workosUser: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleSignOut() {
|
export async function handleSignOut() {
|
||||||
|
const { signOut } = await import("@workos-inc/authkit-nextjs")
|
||||||
await signOut()
|
await signOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +303,7 @@ export async function requireEmailVerified(): Promise<AuthUser> {
|
|||||||
!process.env.WORKOS_API_KEY.includes("placeholder")
|
!process.env.WORKOS_API_KEY.includes("placeholder")
|
||||||
|
|
||||||
if (isWorkOSConfigured) {
|
if (isWorkOSConfigured) {
|
||||||
|
const { withAuth } = await import("@workos-inc/authkit-nextjs")
|
||||||
const session = await withAuth()
|
const session = await withAuth()
|
||||||
if (session?.user && !session.user.emailVerified) {
|
if (session?.user && !session.user.emailVerified) {
|
||||||
throw new Error("Email not verified")
|
throw new Error("Email not verified")
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { NextRequest } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import { authkit, handleAuthkitHeaders } from "@workos-inc/authkit-nextjs"
|
|
||||||
|
|
||||||
// public routes that don't require authentication
|
// public routes that don't require authentication
|
||||||
const publicPaths = [
|
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) {
|
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
|
const { pathname } = request.nextUrl
|
||||||
|
|
||||||
// get session and headers from authkit (handles token refresh automatically)
|
// get session and headers from authkit (handles token refresh automatically)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user