From 04130c0fe2fdcf44add63d60c7909e74f722082e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 15 May 2026 18:25:47 -0700 Subject: [PATCH 01/14] feat: local dev without third-party credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh clone can now boot the full dev stack (web + api + desktop + electric + electric-proxy + caddy) against a local Postgres in Docker, with no Neon, OAuth, Stripe, Resend, or other cloud-service keys required. Key changes: - packages/db/src/client.ts: driver swap to drizzle-orm/node-postgres for non-Neon DATABASE_URLs (@neondatabase/serverless only speaks Neon's HTTP/WS protocol, not vanilla Postgres TCP). - packages/auth/src/server.ts: emailAndPassword auth enabled + autoSignIn. - apps/desktop/src/main/lib/dev-auto-sign-in.ts: during app.whenReady(), if SKIP_ENV_VALIDATION=1 and no token on disk, auto-signs-in as admin@local.test and persists the token via the existing saveToken(). Renderer hydrates like a real OAuth user — no special renderer code. - 17 renderer files: flip MOCK_ORG_ID priority — prefer real session's activeOrganizationId, fall back to MOCK_ORG_ID only without a session. Fixes the "Host service not available" toast that fired because the renderer was looking up host-service connections by the fake org id while host-service spawned for the real admin@local.test org. - Lazy-init guards for Stripe (Proxy), Resend (Proxy), and PostHog (no-op surface) so they don't crash module load when keys are missing. - apps/web: dev-only email/password form on sign-in/sign-up, gated on NODE_ENV !== "production". - packages/db: bun db:seed:dev script to create admin@local.test; refuses prod + non-localhost DATABASE_URL. - turbo.jsonc: SKIP_ENV_VALIDATION added to globalPassThroughEnv. - docs/LOCAL_DEVELOPMENT.md: contributor-facing setup guide. - plans/20260515-oss-local-setup.md: full record of what shipped + what's deferred (env vars .optional(), docker-compose.dev.yml, bun setup orchestrator, CI fresh-clone smoke test). Verified end-to-end via Chrome DevTools Protocol (port 9333) — the renderer's LocalHostServiceContext.activeHostUrl resolves to a live host-service URL, so the import gate is open. --- README.md | 49 ++---- apps/api/src/lib/analytics.ts | 33 +++- apps/desktop/src/main/index.ts | 13 ++ apps/desktop/src/main/lib/dev-auto-sign-in.ts | 96 +++++++++++ .../OpenInWorkspaceV2/OpenInWorkspaceV2.tsx | 6 +- .../RunInWorkspacePopoverV2.tsx | 6 +- .../RunIssuesInWorkspacePopover.tsx | 6 +- .../useAccessibleV2Workspaces.ts | 6 +- .../useWorkspaceHostOptions.ts | 6 +- .../DashboardNewWorkspaceModalContent.tsx | 6 +- .../V1ImportModal/V1ImportModal.tsx | 6 +- .../renderer/routes/_authenticated/layout.tsx | 6 +- .../CollectionsProvider.tsx | 6 +- .../LocalHostServiceProvider.tsx | 6 +- .../HostsSettingsSidebar.tsx | 6 +- .../_authenticated/settings/hosts/page.tsx | 6 +- .../settings/projects/$projectId/page.tsx | 6 +- .../ProjectsSettingsSidebar.tsx | 6 +- .../_authenticated/settings/projects/page.tsx | 6 +- .../_authenticated/setup/project/page.tsx | 6 +- .../src/renderer/routes/sign-in/page.tsx | 4 +- .../components/DevAuthForm/DevAuthForm.tsx | 103 ++++++++++++ .../(auth)/components/DevAuthForm/index.ts | 1 + .../(auth)/sign-in/[[...sign-in]]/page.tsx | 4 + .../(auth)/sign-up/[[...sign-up]]/page.tsx | 6 + bun.lock | 6 +- docs/LOCAL_DEVELOPMENT.md | 154 ++++++++++++++++++ package.json | 1 + packages/auth/src/lib/resend.ts | 54 +++++- packages/auth/src/server.ts | 4 + packages/auth/src/stripe.ts | 16 +- packages/db/package.json | 3 + packages/db/src/client.ts | 48 ++++-- packages/db/src/seed-dev.ts | 73 +++++++++ packages/trpc/src/lib/analytics.ts | 32 +++- packages/trpc/src/router/support/support.ts | 19 ++- plans/20260515-oss-local-setup.md | 119 ++++++++++++++ turbo.jsonc | 3 +- 38 files changed, 822 insertions(+), 115 deletions(-) create mode 100644 apps/desktop/src/main/lib/dev-auto-sign-in.ts create mode 100644 apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx create mode 100644 apps/web/src/app/(auth)/components/DevAuthForm/index.ts create mode 100644 docs/LOCAL_DEVELOPMENT.md create mode 100644 packages/db/src/seed-dev.ts create mode 100644 plans/20260515-oss-local-setup.md diff --git a/README.md b/README.md index e1d935232ec..e2758e59ecf 100644 --- a/README.md +++ b/README.md @@ -85,57 +85,32 @@ If it runs in a terminal, it runs on Superset ### Build from Source -
-Click to expand build instructions +For a complete contributor workflow that boots a fresh clone with no third-party credentials (Neon / OAuth / Stripe / Resend keys are all optional), follow **[Local Development](docs/LOCAL_DEVELOPMENT.md)**. -**1. Clone the repository** +Short version: ```bash git clone https://github.com/superset-sh/superset.git cd superset -``` - -**2. Set up environment variables** (choose one): - -Option A: Full setup -```bash -cp .env.example .env -# Edit .env and fill in the values -``` - -Option B: Skip env validation (for quick local testing) -```bash -cp .env.example .env -echo 'SKIP_ENV_VALIDATION=1' >> .env -``` - -**3. Set up Caddy** (reverse proxy for Electric SQL streams): - -```bash -# Install caddy: brew install caddy (macOS) or see https://caddyserver.com/docs/install -cp Caddyfile.example Caddyfile - -# Without this, Chromium rejects https://localhost:* with ERR_CERT_AUTHORITY_INVALID. -# Prompts for sudo once. -caddy trust -``` - -**4. Install dependencies and run** - -```bash bun install -bun run dev +docker run -d --name superset-pg \ + -e POSTGRES_USER=superset -e POSTGRES_PASSWORD=superset -e POSTGRES_DB=superset \ + -p 5433:5432 postgres:16 -c wal_level=logical +cp .env.example .env # then edit DATABASE_URL + BETTER_AUTH_SECRET +bun run db:migrate +cp Caddyfile.example Caddyfile && caddy trust +SKIP_ENV_VALIDATION=1 bun dev ``` -**5. Build the desktop app** +The desktop window opens auto-signed-in as a seed admin (`admin@local.test`). See [Local Development](docs/LOCAL_DEVELOPMENT.md) for details, troubleshooting, and what's stubbed without integration keys. + +To build a distributable desktop app: ```bash bun run build open apps/desktop/release ``` -
- ## Keyboard Shortcuts All shortcuts are customizable via **Settings > Keyboard Shortcuts** (`⌘/`). See [full documentation](https://docs.superset.sh/keyboard-shortcuts). diff --git a/apps/api/src/lib/analytics.ts b/apps/api/src/lib/analytics.ts index aa8d66fce76..8f5536ff5d3 100644 --- a/apps/api/src/lib/analytics.ts +++ b/apps/api/src/lib/analytics.ts @@ -1,8 +1,33 @@ import { PostHog } from "posthog-node"; import { env } from "@/env"; -export const posthog = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { - host: env.NEXT_PUBLIC_POSTHOG_HOST, - flushAt: 1, - flushInterval: 0, +let client: PostHog | null = null; + +// Disabled stub — accepts any args, returns the right shape for each method. +const disabled = { + capture: (..._args: unknown[]) => {}, + identify: (..._args: unknown[]) => {}, + alias: (..._args: unknown[]) => {}, + groupIdentify: (..._args: unknown[]) => {}, + shutdown: (..._args: unknown[]) => Promise.resolve(), + flush: (..._args: unknown[]) => Promise.resolve(), + getFeatureFlag: (..._args: unknown[]) => Promise.resolve(undefined), + getFeatureFlagPayload: (..._args: unknown[]) => Promise.resolve(undefined), + isFeatureEnabled: (..._args: unknown[]) => Promise.resolve(undefined), +} as unknown as PostHog; + +// Lazy-init: if NEXT_PUBLIC_POSTHOG_KEY is missing, return a no-op surface +// so analytics calls don't crash the API at module load. +export const posthog = new Proxy({} as PostHog, { + get(_target, prop) { + if (!env.NEXT_PUBLIC_POSTHOG_KEY) return Reflect.get(disabled, prop); + if (!client) { + client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { + host: env.NEXT_PUBLIC_POSTHOG_HOST, + flushAt: 1, + flushInterval: 0, + }); + } + return Reflect.get(client, prop); + }, }); diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 03879a6d9ea..eca914a7d3b 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -28,6 +28,7 @@ import { initAppState } from "./lib/app-state"; import { requestAppleEventsAccess } from "./lib/apple-events-permission"; import { setupAutoUpdater } from "./lib/auto-updater"; import { installBundledCliShim } from "./lib/bundled-cli"; +import { ensureDevAuthToken } from "./lib/dev-auto-sign-in"; import { resolveDevWorkspaceName } from "./lib/dev-workspace-name"; import { setWorkspaceDockIcon } from "./lib/dock-icon"; import { loadWebviewBrowserExtension } from "./lib/extensions"; @@ -55,6 +56,12 @@ import { MainWindow } from "./windows/main"; console.log("[main] Local database ready:", !!localDb); const IS_DEV = process.env.NODE_ENV === "development"; +// Dev: expose Chrome DevTools Protocol for headless testing (e.g. import/host-service checks) +if (IS_DEV && process.env.SKIP_ENV_VALIDATION) { + app.commandLine.appendSwitch("remote-debugging-port", "9333"); + app.commandLine.appendSwitch("remote-allow-origins", "*"); +} + void applyShellEnvToProcess().catch((error) => { console.error("[main] Failed to apply shell environment:", error); }); @@ -416,6 +423,12 @@ if (!gotTheLock) { console.error("[main] Failed to install bundled CLI shim:", error); } + // Dev-only: auto-sign-in as the seed admin if SKIP_ENV_VALIDATION is + // set and no token is on disk. Lets fresh-clone contributors boot the + // desktop without OAuth. Runs before host-service discovery so the + // renderer's AuthProvider hydrates with a valid token on first paint. + await ensureDevAuthToken(); + // Discover and adopt host-services that survived a previous quit // before the tray initializes, so it shows accurate status immediately. await getHostServiceCoordinator().discoverAll(); diff --git a/apps/desktop/src/main/lib/dev-auto-sign-in.ts b/apps/desktop/src/main/lib/dev-auto-sign-in.ts new file mode 100644 index 00000000000..e5f1f5a0650 --- /dev/null +++ b/apps/desktop/src/main/lib/dev-auto-sign-in.ts @@ -0,0 +1,96 @@ +import { env as mainEnv } from "main/env.main"; +import { + loadToken, + saveToken, +} from "../../lib/trpc/routers/auth/utils/auth-functions"; + +const DEV_EMAIL = "admin@local.test"; +const DEV_PASSWORD = "supersetdev"; +const DEV_NAME = "Local Admin"; +const TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; + +interface SignInResponse { + token?: string; + user?: { id: string }; +} + +interface AuthErrorBody { + code?: string; + message?: string; +} + +async function postAuth( + path: string, + body: Record, +): Promise<{ ok: boolean; status: number; data: T | AuthErrorBody }> { + const res = await fetch(`${mainEnv.NEXT_PUBLIC_API_URL}${path}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Origin: mainEnv.NEXT_PUBLIC_API_URL, + }, + body: JSON.stringify(body), + }); + const data = (await res.json().catch(() => ({}))) as T | AuthErrorBody; + return { ok: res.ok, status: res.status, data }; +} + +/** + * Dev-only: if SKIP_ENV_VALIDATION is set and no usable token is on disk, + * sign in (or sign up) as the seed admin user and persist the token so + * the renderer's AuthProvider can hydrate normally — no special renderer code. + * Best-effort: failure is logged but doesn't crash boot. + */ +export async function ensureDevAuthToken(): Promise { + if ( + process.env.NODE_ENV !== "development" || + !process.env.SKIP_ENV_VALIDATION + ) + return; + + const stored = await loadToken(); + if (stored.token && stored.expiresAt) { + const isExpired = new Date(stored.expiresAt) < new Date(); + if (!isExpired) return; + } + + try { + let signIn = await postAuth("/api/auth/sign-in/email", { + email: DEV_EMAIL, + password: DEV_PASSWORD, + }); + + const errBody = signIn.data as AuthErrorBody; + if (!signIn.ok && errBody.code === "INVALID_EMAIL_OR_PASSWORD") { + const signUp = await postAuth("/api/auth/sign-up/email", { + email: DEV_EMAIL, + password: DEV_PASSWORD, + name: DEV_NAME, + }); + if (!signUp.ok) { + const e = signUp.data as AuthErrorBody; + throw new Error(`dev sign-up failed (${signUp.status}): ${e.message}`); + } + signIn = await postAuth("/api/auth/sign-in/email", { + email: DEV_EMAIL, + password: DEV_PASSWORD, + }); + } + + if (!signIn.ok) { + const e = signIn.data as AuthErrorBody; + throw new Error(`dev sign-in failed (${signIn.status}): ${e.message}`); + } + const token = (signIn.data as SignInResponse).token; + if (!token) throw new Error("dev sign-in: no token in response"); + + const expiresAt = new Date(Date.now() + TOKEN_TTL_MS).toISOString(); + await saveToken({ token, expiresAt }); + console.log(`[dev-auto-sign-in] signed in as ${DEV_EMAIL}`); + } catch (err) { + console.warn( + `[dev-auto-sign-in] failed (is the API up at ${mainEnv.NEXT_PUBLIC_API_URL}?):`, + err, + ); + } +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx index d853bf926d9..8b8b74465b2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx @@ -54,9 +54,9 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { const { machineId, activeHostUrl } = useLocalHostService(); const { otherHosts } = useWorkspaceHostOptions(); const { data: session } = authClient.useSession(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { submit } = useWorkspaceCreates(); const lastProjectId = useV2WorkspaceCreateDefaultsStore( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx index 6759919c96a..75843cbaf59 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx @@ -59,9 +59,9 @@ export function RunInWorkspacePopoverV2({ const collections = useCollections(); const { machineId, activeHostUrl } = useLocalHostService(); const { data: session } = authClient.useSession(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { otherHosts } = useWorkspaceHostOptions(); const { submit } = useWorkspaceCreates(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx index a05788c30f9..0c877cfed52 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx @@ -63,9 +63,9 @@ export function RunIssuesInWorkspacePopover({ const collections = useCollections(); const { machineId, activeHostUrl } = useLocalHostService(); const { data: session } = authClient.useSession(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { otherHosts } = useWorkspaceHostOptions(); const { submit } = useWorkspaceCreates(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts index eb904571c63..6a95ea646b5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts @@ -221,9 +221,9 @@ export function useAccessibleV2Workspaces( const collections = useCollections(); const { machineId } = useLocalHostService(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const currentUserId = session?.user?.id ?? null; const { data: rows = [] } = useLiveQuery( diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts index b96c7534db5..6014b9ee911 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts @@ -26,9 +26,9 @@ export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult { const collections = useCollections(); const { machineId, activeHostUrl } = useLocalHostService(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const currentUserId = session?.user?.id ?? null; const { data: accessibleHosts = [] } = useLiveQuery( diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx index da8399adba2..da6b96ba19d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx @@ -32,9 +32,9 @@ export function DashboardNewWorkspaceModalContent({ ); const collections = useCollections(); const { data: session } = authClient.useSession(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: v2Projects } = useLiveQuery( (q) => diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx index bd292fd0e90..8b127423c10 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx @@ -27,9 +27,9 @@ export function V1ImportModal() { const close = useCloseV1ImportModal(); const { data: session } = authClient.useSession(); const { activeHostUrl } = useLocalHostService(); - const organizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const organizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); if (!organizationId) return null; diff --git a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx index dbc28401f70..4717948ec01 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx @@ -62,9 +62,9 @@ function AuthenticatedLayout() { const isV2CloudEnabled = useIsV2CloudEnabled(); const isSignedIn = env.SKIP_ENV_VALIDATION || !!session?.user; - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : session?.session?.activeOrganizationId; + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); useAgentHookListener(); useUpdateListener(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx index f5562295086..518fb6b4495 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx @@ -33,9 +33,9 @@ export function preloadActiveOrganizationCollections( export function CollectionsProvider({ children }: { children: ReactNode }) { const { data: session, refetch: refetchSession } = authClient.useSession(); const [isSwitching, setIsSwitching] = useState(false); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : session?.session?.activeOrganizationId; + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const switchOrganization = useCallback( async (organizationId: string) => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx index 5ef6fdfd5cc..75276559f9d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx @@ -31,9 +31,9 @@ export function LocalHostServiceProvider({ const { mutate: startHostService } = electronTrpc.hostServiceCoordinator.start.useMutation(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: organizations } = useLiveQuery( (q) => q.from({ organizations: collections.organizations }), diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx index 0b4567a52e4..5a0c0454b7d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx @@ -30,9 +30,9 @@ export function HostsSettingsSidebar({ const collections = useCollections(); const { data: session } = authClient.useSession(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: hosts = [] } = useLiveQuery( (q) => diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx index 643c8e8adbd..3c50905ddd5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx @@ -16,9 +16,9 @@ function HostsIndexPage() { const { data: session } = authClient.useSession(); const navigate = useNavigate(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: hosts = [] } = useLiveQuery( (q) => diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx index c93c0d2e7ad..7f11dda2af9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx @@ -25,9 +25,9 @@ function ProjectDetailPage() { const { data: session } = authClient.useSession(); const searchQuery = useSettingsSearchQuery(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: v2Match = [] } = useLiveQuery( (q) => diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx index ce7d751ecc4..bedd6a8612b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx @@ -31,9 +31,9 @@ export function ProjectsSettingsSidebar({ const collections = useCollections(); const { data: session } = authClient.useSession(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: groups = [] } = electronTrpc.workspaces.getAllGrouped.useQuery(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx index 5db0608732d..60605248b49 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx @@ -17,9 +17,9 @@ function ProjectsIndexPage() { const { data: session } = authClient.useSession(); const navigate = useNavigate(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: groups = [] } = electronTrpc.workspaces.getAllGrouped.useQuery(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx index fe5b99a03f3..10626d1ce1f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx @@ -26,9 +26,9 @@ function OnboardingProjectPage() { const markSkipped = useOnboardingStore((s) => s.markSkipped); const { data: session } = authClient.useSession(); - const activeOrganizationId = env.SKIP_ENV_VALIDATION - ? MOCK_ORG_ID - : (session?.session?.activeOrganizationId ?? null); + const activeOrganizationId = + session?.session?.activeOrganizationId ?? + (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null); const { data: projects = [], isLoading } = useQuery({ queryKey: ["onboarding", "v2Projects", activeOrganizationId], diff --git a/apps/desktop/src/renderer/routes/sign-in/page.tsx b/apps/desktop/src/renderer/routes/sign-in/page.tsx index ddfc693a961..aa08c51ad5d 100644 --- a/apps/desktop/src/renderer/routes/sign-in/page.tsx +++ b/apps/desktop/src/renderer/routes/sign-in/page.tsx @@ -18,8 +18,8 @@ function SignInPage() { const signInMutation = electronTrpc.auth.signIn.useMutation(); const { hasLocalToken, isPending, session } = useSessionRecovery(); - // Dev bypass: skip sign-in entirely - if (env.SKIP_ENV_VALIDATION) { + // Dev bypass: AuthProvider handles auto-sign-in; if session lands, redirect + if (env.SKIP_ENV_VALIDATION && session?.user) { return ; } diff --git a/apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx b/apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx new file mode 100644 index 00000000000..d07f9a35f6a --- /dev/null +++ b/apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { authClient } from "@superset/auth/client"; +import { Button } from "@superset/ui/button"; +import { Input } from "@superset/ui/input"; +import { Label } from "@superset/ui/label"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +interface DevAuthFormProps { + mode: "sign-in" | "sign-up"; + callbackURL: string; +} + +export function DevAuthForm({ mode, callbackURL }: DevAuthFormProps) { + const router = useRouter(); + const [email, setEmail] = useState( + mode === "sign-up" ? "" : "admin@local.test", + ); + const [password, setPassword] = useState( + mode === "sign-up" ? "" : "supersetdev", + ); + const [name, setName] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + setError(null); + + try { + if (mode === "sign-up") { + const res = await authClient.signUp.email({ + email, + password, + name: name || email.split("@")[0] || "Dev User", + }); + if (res.error) throw new Error(res.error.message); + } else { + const res = await authClient.signIn.email({ email, password }); + if (res.error) throw new Error(res.error.message); + } + router.push(callbackURL); + } catch (err) { + const message = + err instanceof Error ? err.message : "Authentication failed"; + setError(message); + setSubmitting(false); + } + }; + + return ( +
+

+ Dev only — email + password auth +

+ {mode === "sign-up" && ( +
+ + setName(e.target.value)} + placeholder="Optional" + /> +
+ )} +
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + minLength={8} + /> +
+ {error &&

{error}

} + +
+ ); +} diff --git a/apps/web/src/app/(auth)/components/DevAuthForm/index.ts b/apps/web/src/app/(auth)/components/DevAuthForm/index.ts new file mode 100644 index 00000000000..eddbcfa938b --- /dev/null +++ b/apps/web/src/app/(auth)/components/DevAuthForm/index.ts @@ -0,0 +1 @@ +export { DevAuthForm } from "./DevAuthForm"; diff --git a/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx index b27142e33f0..63049a01b27 100644 --- a/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +++ b/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx @@ -8,6 +8,9 @@ import { useState } from "react"; import { FaGithub } from "react-icons/fa"; import { FcGoogle } from "react-icons/fc"; import { env } from "@/env"; +import { DevAuthForm } from "../../components/DevAuthForm"; + +const isDev = process.env.NODE_ENV !== "production"; export default function SignInPage() { const searchParams = useSearchParams(); @@ -66,6 +69,7 @@ export default function SignInPage() { {error && (

{error}

)} + {isDev && } + + ); +} diff --git a/apps/desktop/src/renderer/routes/sign-in/components/LocalDevAuthForm/index.ts b/apps/desktop/src/renderer/routes/sign-in/components/LocalDevAuthForm/index.ts new file mode 100644 index 00000000000..9514c0436a0 --- /dev/null +++ b/apps/desktop/src/renderer/routes/sign-in/components/LocalDevAuthForm/index.ts @@ -0,0 +1 @@ +export { LocalDevAuthForm } from "./LocalDevAuthForm"; diff --git a/apps/desktop/src/renderer/routes/sign-in/page.tsx b/apps/desktop/src/renderer/routes/sign-in/page.tsx index aa08c51ad5d..d7d7f5eb4e2 100644 --- a/apps/desktop/src/renderer/routes/sign-in/page.tsx +++ b/apps/desktop/src/renderer/routes/sign-in/page.tsx @@ -7,6 +7,7 @@ import { FcGoogle } from "react-icons/fc"; import { env } from "renderer/env.renderer"; import { track } from "renderer/lib/analytics"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { LocalDevAuthForm } from "./components/LocalDevAuthForm"; import { SupersetLogo } from "./components/SupersetLogo"; import { useSessionRecovery } from "./hooks/useSessionRecovery"; @@ -17,6 +18,7 @@ export const Route = createFileRoute("/sign-in/")({ function SignInPage() { const signInMutation = electronTrpc.auth.signIn.useMutation(); const { hasLocalToken, isPending, session } = useSessionRecovery(); + const isLocalProfile = env.SUPERSET_PROFILE === "local"; // Dev bypass: AuthProvider handles auto-sign-in; if session lands, redirect if (env.SKIP_ENV_VALIDATION && session?.user) { @@ -63,7 +65,11 @@ function SignInPage() {

-
+
+ {isLocalProfile && } + + {isLocalProfile &&
} +