From 90eb9023c8c7d0facc7a3f4f3ed72f2fdf4d7a09 Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 14:40:25 +0100 Subject: [PATCH 1/9] unify dialogs --- dashboard/app/page.tsx | 4 +- dashboard/components/AuthDialog.tsx | 392 +++++++++++++++++++++++++++ dashboard/components/LoginDialog.tsx | 126 --------- dashboard/package.json | 2 +- pnpm-lock.yaml | 14 +- tinybird/package.json | 2 +- 6 files changed, 403 insertions(+), 137 deletions(-) create mode 100644 dashboard/components/AuthDialog.tsx delete mode 100644 dashboard/components/LoginDialog.tsx diff --git a/dashboard/app/page.tsx b/dashboard/app/page.tsx index aa5b856..3764b1f 100644 --- a/dashboard/app/page.tsx +++ b/dashboard/app/page.tsx @@ -19,7 +19,7 @@ import { useInsightsData } from '@/lib/hooks/use-insights-data' import useCurrentVisitors from '@/lib/hooks/use-current-visitors' import React from 'react' import { Header } from '@/components/Header' -import LoginDialog from '@/components/LoginDialog' +import AuthDialog from '@/components/AuthDialog' import { useLogin } from '@/lib/hooks/use-login' export default function DashboardPage() { @@ -39,7 +39,7 @@ export default function DashboardPage() { return ( {/* Show login dialog overlay when not logged in */} - {showLoginDialog && } + {showLoginDialog && } <> {process.env.NODE_ENV === 'production' && ( diff --git a/dashboard/components/AuthDialog.tsx b/dashboard/components/AuthDialog.tsx new file mode 100644 index 0000000..35918cc --- /dev/null +++ b/dashboard/components/AuthDialog.tsx @@ -0,0 +1,392 @@ +'use client' + +import { FormEvent, useState } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/Dialog' +import { Input } from '@/components/ui/Input' +import { Button } from '@/components/ui/Button' +import { Select } from '@/components/ui/Select' +import { Link } from '@/components/ui/Link' +import { Text } from '@/components/ui/Text' +import { Loader } from '@/components/ui/Loader' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs' +import { SignJWT } from 'jose' + +type AuthMode = 'signin' | 'token' + +const regionMap: Record = { + 'gcp-europe-west2': 'https://api.europe-west2.gcp.tinybird.co', + eu_shared: 'https://api.tinybird.co', + us_east: 'https://api.us-east.tinybird.co', + 'gcp-northamerica-northeast2': + 'https://api.northamerica-northeast2.gcp.tinybird.co', + 'us-east-aws': 'https://api.us-east.aws.tinybird.co', + 'aws-us-west-2': 'https://api.us-west-2.aws.tinybird.co', + 'aws-eu-central-1': 'https://api.eu-central-1.aws.tinybird.co', + 'aws-eu-west-1': 'https://api.eu-west-1.aws.tinybird.co', + localhost: 'http://127.0.0.1:8001', + local: 'http://localhost:7181', +} + +const regionValues = Object.values(regionMap) + +const extractHostFromToken = (token: string): string | undefined => { + try { + const payload = token.split('.')[1] + if (!payload) return undefined + const base64 = + payload.replace(/-/g, '+').replace(/_/g, '/') + + '==='.slice((payload.length + 3) % 4) + const json = atob(base64) + const data = JSON.parse(json) + return data.host + } catch { + return undefined + } +} + +export const extractWorkspaceIdFromToken = ( + token: string +): string | undefined => { + try { + const payload = token.split('.')[1] + if (!payload) return undefined + const base64 = + payload.replace(/-/g, '+').replace(/_/g, '/') + + '==='.slice((payload.length + 3) % 4) + const json = atob(base64) + const data = JSON.parse(json) + return data.u + } catch { + return undefined + } +} + +export async function createJwt( + token: string, + tenant_id: string +): Promise { + const expiration_time = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60 + const workspace_id = extractWorkspaceIdFromToken(token) + const resources = [ + 'domains', + 'top_sources', + 'top_devices', + 'kpis', + 'top_locations', + 'top_browsers', + 'top_pages', + 'trend', + 'domain', + 'current_visitors', + 'web_vitals_current', + 'web_vitals_routes', + 'web_vitals_distribution', + 'web_vitals_events', + 'web_vitals_timeseries', + 'analytics_hits', + 'actions', + ] + + const datasources_resources = [ + 'analytics_events', + 'tenant_actions_mv', + 'tenant_domains_mv', + ] + + const datasources_scopes = datasources_resources.map(resource => ({ + type: 'DATASOURCES:READ', + resource, + filter: `tenant_id = '${tenant_id}'`, + })) + + const payload = { + workspace_id: workspace_id, + name: 'frontend_jwt', + exp: expiration_time, + scopes: [ + ...resources.map(resource => ({ + type: 'PIPES:READ', + resource, + fixed_params: { + tenant_id: tenant_id, + }, + })), + ...datasources_scopes, + ], + } + const key = new TextEncoder().encode(token) + return await new SignJWT(payload as any) + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime(expiration_time) + .sign(key) +} + +interface AuthDialogProps { + isLoading?: boolean + defaultMode?: AuthMode +} + +export default function AuthDialog({ + isLoading: isCheckingAuth, + defaultMode = 'signin', +}: AuthDialogProps) { + const [mode, setMode] = useState(defaultMode) + + // Sign in state + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [signInError, setSignInError] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + // Token state + const [hostUrl, setHostUrl] = useState(regionValues[0]) + const [hostName, setHostName] = useState('') + const [tenantId, setTenantId] = useState('') + const [tokenHasHost, setTokenHasHost] = useState(true) + + async function handleSignIn(e: React.FormEvent) { + e.preventDefault() + setSignInError('') + setIsSubmitting(true) + + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }) + + if (response.ok) { + window.location.reload() + } else { + const data = await response.json() + setSignInError(data.error || 'Invalid credentials') + setIsSubmitting(false) + } + } catch { + setSignInError('An error occurred. Please try again.') + setIsSubmitting(false) + } + } + + const handleTokenChange = (e: React.ChangeEvent) => { + const token = e.target.value + const hostKey = extractHostFromToken(token) + + if (hostKey) { + setTokenHasHost(true) + if (regionMap[hostKey]) { + setHostUrl(regionMap[hostKey]) + setHostName('') + } else { + setHostUrl('other') + setHostName(hostKey) + } + } else { + setTokenHasHost(false) + setHostUrl(regionValues[0]) + setHostName('') + } + } + + const handleTokenSubmit = async (event: FormEvent) => { + event.preventDefault() + const form = event.currentTarget + const formData = new FormData(form) + const token = formData.get('token') as string + const host = hostUrl === 'other' ? hostName : hostUrl + const tenant_id = formData.get('tenant_id') as string + + if (!token || (hostUrl === 'other' && !hostName)) return + + const jwt = await createJwt(token, tenant_id || '') + + const url = new URL(window.location.href) + url.searchParams.set('token', jwt) + url.searchParams.set('host', host) + if (tenant_id) url.searchParams.set('tenant_id', tenant_id) + window.location.href = url.toString() + } + + const hostOptions = [ + ...regionValues.map(value => ({ + value, + label: value, + })), + { value: 'other', label: 'Other' }, + ] + + if (isCheckingAuth) { + return ( +
+ +
+ ) + } + + return ( + + + + Access Dashboard + + Sign in with your credentials or use a Tinybird token directly. + + + + setMode(value as AuthMode)} + variant="pill" + > + + Sign in + Use Token + + + +
+
+ + setUsername(e.target.value)} + placeholder="Enter username" + required + autoComplete="off" + autoFocus + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Enter password" + required + autoComplete="new-password" + /> + {!!signInError && ( + + {signInError} + + )} +
+ +
+
+ + +
+
+ + + + Get your admin token + +
+ + {!tokenHasHost && ( + <> +
+ + setHostName(e.target.value)} + /> +
+ )} + + )} + +
+ + setTenantId(e.target.value)} + /> +
+ + +
+
+
+
+
+ ) +} diff --git a/dashboard/components/LoginDialog.tsx b/dashboard/components/LoginDialog.tsx deleted file mode 100644 index 122c672..0000000 --- a/dashboard/components/LoginDialog.tsx +++ /dev/null @@ -1,126 +0,0 @@ -'use client' - -import { useState } from 'react' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from '@/components/ui/Dialog' -import { Input } from '@/components/ui/Input' -import { Button } from '@/components/ui/Button' -import { Text } from '@/components/ui/Text' -import { Loader } from '@/components/ui/Loader' - -interface LoginDialogProps { - isLoading?: boolean -} - -export default function LoginDialog({ - isLoading: isCheckingAuth, -}: LoginDialogProps) { - const [username, setUsername] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setError('') - setIsSubmitting(true) - - try { - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }), - }) - - if (response.ok) { - window.location.reload() - } else { - const data = await response.json() - setError(data.error || 'Invalid credentials') - setIsSubmitting(false) - } - } catch { - setError('An error occurred. Please try again.') - setIsSubmitting(false) - } - } - - // Show loading state while checking authentication - if (isCheckingAuth) { - return ( -
- -
- ) - } - - return ( - - - - Sign in to Dashboard - - Enter your credentials to access the analytics dashboard. - - - -
-
- - setUsername(e.target.value)} - placeholder="Enter username" - required - autoComplete="off" - autoFocus - /> -
- -
- - setPassword(e.target.value)} - placeholder="Enter password" - required - autoComplete="new-password" - /> - {!!error && ( - - {error} - - )} -
- -
-
-
- ) -} diff --git a/dashboard/package.json b/dashboard/package.json index db714a0..780f9c7 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -39,7 +39,7 @@ "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.7", "@tinybirdco/analytics-client": "workspace:*", - "@tinybirdco/sdk": "^0.0.25", + "@tinybirdco/sdk": "^0.0.26", "ai": "^4.3.17", "class-variance-authority": "^0.7.1", "country-flag-icons": "^1.5.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fdcf6c..ea3e622 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,8 +71,8 @@ importers: specifier: workspace:* version: link:../tinybird '@tinybirdco/sdk': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.26 + version: 0.0.26 ai: specifier: ^4.3.17 version: 4.3.19(react@18.3.1)(zod@3.25.76) @@ -196,8 +196,8 @@ importers: tinybird: dependencies: '@tinybirdco/sdk': - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.0.26 + version: 0.0.26 devDependencies: '@types/node': specifier: ^22.13.10 @@ -2085,8 +2085,8 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tinybirdco/sdk@0.0.25': - resolution: {integrity: sha512-PYJcwEgv2/gOAPcHHgaimoTMy4hFuy3UyT6tSBNznEcd+HJms0W4DA/y68yB3m8A9O+zYKztejNtK/U/8lNg7Q==} + '@tinybirdco/sdk@0.0.26': + resolution: {integrity: sha512-xHurGkKtKS1Kp7wyZ/KnfBF0oDyqL+S3gDm82IOUT4mU1CCCnt2+n2aIqJ1trqbSCCMZGtSmIsJVWlxjXA2PFw==} engines: {node: '>=20.0.0'} hasBin: true @@ -7145,7 +7145,7 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tinybirdco/sdk@0.0.25': + '@tinybirdco/sdk@0.0.26': dependencies: '@clack/prompts': 1.0.0 chokidar: 4.0.3 diff --git a/tinybird/package.json b/tinybird/package.json index 3fea943..1b40cf0 100644 --- a/tinybird/package.json +++ b/tinybird/package.json @@ -13,7 +13,7 @@ "preview": "tinybird preview" }, "dependencies": { - "@tinybirdco/sdk": "^0.0.25" + "@tinybirdco/sdk": "^0.0.26" }, "devDependencies": { "@types/node": "^22.13.10", From f06ea6f4a7344640b3320a1bd0a143ca13618304 Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 14:51:34 +0100 Subject: [PATCH 2/9] polish styles --- dashboard/components/AuthDialog.tsx | 246 ++++++++++++++-------------- 1 file changed, 124 insertions(+), 122 deletions(-) diff --git a/dashboard/components/AuthDialog.tsx b/dashboard/components/AuthDialog.tsx index 35918cc..104229f 100644 --- a/dashboard/components/AuthDialog.tsx +++ b/dashboard/components/AuthDialog.tsx @@ -232,7 +232,7 @@ export default function AuthDialog({ return ( - + Access Dashboard @@ -240,149 +240,151 @@ export default function AuthDialog({ - setMode(value as AuthMode)} - variant="pill" - > + setMode(value as AuthMode)}> Sign in - Use Token + Tinybird Token
-
- - setUsername(e.target.value)} - placeholder="Enter username" - required - autoComplete="off" - autoFocus - /> +
+
+ + setUsername(e.target.value)} + placeholder="Enter username" + required + autoComplete="off" + autoFocus + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Enter password" + required + autoComplete="new-password" + /> + {!!signInError && ( + + {signInError} + + )} +
- -
- - setPassword(e.target.value)} - placeholder="Enter password" - required - autoComplete="new-password" - /> - {!!signInError && ( - - {signInError} - - )} +
+
- - -
-
- - - - Get your admin token - -
- - {!tokenHasHost && ( - <> -
- - + + Get your admin token + +
+ + {!tokenHasHost && ( + <>
- setHostName(e.target.value)} + setTenantId(e.target.value)} - /> -
- + {hostUrl === 'other' && ( +
+ + setHostName(e.target.value)} + /> +
+ )} + + )} + +
+ + setTenantId(e.target.value)} + /> +
+
+
+ +
From 3bfe4ce55a1601f4b96ffd173f75f07459a4c5f0 Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 15:21:57 +0100 Subject: [PATCH 3/9] support both modes --- dashboard/app/api/config/route.ts | 23 ++++-- dashboard/app/api/endpoints/[name]/route.ts | 15 +++- dashboard/app/api/sql/route.ts | 8 +- dashboard/components/AuthDialog.tsx | 31 ++++++-- dashboard/lib/api.ts | 21 +++++- dashboard/lib/hooks/use-login.ts | 81 ++++++++++++++++++--- dashboard/lib/hooks/use-workspace.ts | 62 +++++++++++++++- dashboard/lib/server.ts | 11 +++ tinybird/src/client.ts | 11 ++- 9 files changed, 234 insertions(+), 29 deletions(-) diff --git a/dashboard/app/api/config/route.ts b/dashboard/app/api/config/route.ts index 01eb491..cb848ee 100644 --- a/dashboard/app/api/config/route.ts +++ b/dashboard/app/api/config/route.ts @@ -1,5 +1,5 @@ -import { NextResponse } from 'next/server' -import { getTinybirdConfig, getWorkspace } from '@/lib/server' +import { NextRequest, NextResponse } from 'next/server' +import { getTinybirdConfig, getWorkspace, getWorkspaceWithCredentials } from '@/lib/server' interface TinybirdRegionInfo { provider: string @@ -64,8 +64,14 @@ async function getRegionInfoFromHost( return { provider: 'Unknown', region: 'unknown' } } -export async function GET() { - const { token, host } = getTinybirdConfig() +export async function GET(request: NextRequest) { + // Check for token from headers first (public mode), then fall back to env vars + const headerToken = request.headers.get('X-Tinybird-Token') + const headerHost = request.headers.get('X-Tinybird-Host') + const { token: envToken, host: envHost } = getTinybirdConfig() + + const token = headerToken || envToken + const host = headerHost || envHost const missing: string[] = [] if (!token) missing.push('TINYBIRD_TOKEN') @@ -75,7 +81,14 @@ export async function GET() { // Include workspace info if configured const regionInfo = host ? await getRegionInfoFromHost(host) : null - const tinybirdWorkspace = configured ? await getWorkspace() : null + + // Fetch workspace using header credentials if provided, otherwise use env vars + const tinybirdWorkspace = configured + ? headerToken && headerHost + ? await getWorkspaceWithCredentials(headerToken, headerHost) + : await getWorkspace() + : null + const workspace = configured && host && regionInfo ? { diff --git a/dashboard/app/api/endpoints/[name]/route.ts b/dashboard/app/api/endpoints/[name]/route.ts index f0727eb..060317b 100644 --- a/dashboard/app/api/endpoints/[name]/route.ts +++ b/dashboard/app/api/endpoints/[name]/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { getServerClient, getTinybirdConfig } from '@/lib/server' +import { getServerClient, getTinybirdConfig, createClientWithCredentials } from '@/lib/server' const VALID_ENDPOINTS = [ 'currentVisitors', @@ -44,7 +44,13 @@ export async function GET( ) } - const { token, host } = getTinybirdConfig() + // Check for token from headers first (public mode), then fall back to env vars + const headerToken = request.headers.get('X-Tinybird-Token') + const headerHost = request.headers.get('X-Tinybird-Host') + const { token: envToken, host: envHost } = getTinybirdConfig() + + const token = headerToken || envToken + const host = headerHost || envHost if (!token || !host) { const missing = [] @@ -60,7 +66,10 @@ export async function GET( ) } - const client = getServerClient() + // Use header credentials if provided, otherwise use server client + const client = headerToken && headerHost + ? createClientWithCredentials(headerToken, headerHost) + : getServerClient() const searchParams = request.nextUrl.searchParams const queryParams: Record = {} diff --git a/dashboard/app/api/sql/route.ts b/dashboard/app/api/sql/route.ts index 9a31c1a..a125568 100644 --- a/dashboard/app/api/sql/route.ts +++ b/dashboard/app/api/sql/route.ts @@ -14,7 +14,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'SQL query required' }, { status: 400 }) } - const { token, host } = getTinybirdConfig() + // Check for token from headers first (public mode), then fall back to env vars + const headerToken = request.headers.get('X-Tinybird-Token') + const headerHost = request.headers.get('X-Tinybird-Host') + const { token: envToken, host: envHost } = getTinybirdConfig() + + const token = headerToken || envToken + const host = headerHost || envHost if (!token || !host) { const missing = [] diff --git a/dashboard/components/AuthDialog.tsx b/dashboard/components/AuthDialog.tsx index 104229f..4c13c74 100644 --- a/dashboard/components/AuthDialog.tsx +++ b/dashboard/components/AuthDialog.tsx @@ -16,6 +16,7 @@ import { Text } from '@/components/ui/Text' import { Loader } from '@/components/ui/Loader' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs' import { SignJWT } from 'jose' +import { setStoredCredentials } from '@/lib/hooks/use-login' type AuthMode = 'signin' | 'token' @@ -205,13 +206,33 @@ export default function AuthDialog({ if (!token || (hostUrl === 'other' && !hostName)) return + // Fetch workspace info using the admin token before creating scoped JWT + let workspaceInfo: { name: string; id?: string } | undefined + try { + const workspaceUrl = new URL('/v1/workspace', host) + const response = await fetch(workspaceUrl.toString(), { + headers: { Authorization: `Bearer ${token}` }, + }) + if (response.ok) { + const data = await response.json() + workspaceInfo = { name: data.name, id: data.id } + } + } catch { + // Workspace fetch failed, continue without it + } + const jwt = await createJwt(token, tenant_id || '') - const url = new URL(window.location.href) - url.searchParams.set('token', jwt) - url.searchParams.set('host', host) - if (tenant_id) url.searchParams.set('tenant_id', tenant_id) - window.location.href = url.toString() + // Save credentials to localStorage (including workspace info) + setStoredCredentials({ + token: jwt, + host, + tenantId: tenant_id || undefined, + workspace: workspaceInfo, + }) + + // Reload page to use the new credentials + window.location.reload() } const hostOptions = [ diff --git a/dashboard/lib/api.ts b/dashboard/lib/api.ts index 655438e..e3be334 100644 --- a/dashboard/lib/api.ts +++ b/dashboard/lib/api.ts @@ -1,5 +1,17 @@ import fetch from 'cross-fetch' import { PipeParams, QueryPipe, QuerySQL, QueryError } from './types/api' +import { getStoredCredentials } from './hooks/use-login' + +function getAuthHeaders(): HeadersInit { + const credentials = getStoredCredentials() + if (credentials?.token && credentials?.host) { + return { + 'X-Tinybird-Token': credentials.token, + 'X-Tinybird-Host': credentials.host, + } + } + return {} +} export async function queryPipe( name: string, @@ -11,7 +23,9 @@ export async function queryPipe( searchParams.set(key, value as string) }) - const response = await fetch(`/api/endpoints/${name}?${searchParams}`) + const response = await fetch(`/api/endpoints/${name}?${searchParams}`, { + headers: getAuthHeaders(), + }) const data = await response.json() if (!response.ok) { @@ -24,7 +38,10 @@ export async function queryPipe( export async function querySQL(sql: string): Promise> { const response = await fetch('/api/sql', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders(), + }, body: JSON.stringify({ sql }), }) diff --git a/dashboard/lib/hooks/use-login.ts b/dashboard/lib/hooks/use-login.ts index fabc0f1..96a5d82 100644 --- a/dashboard/lib/hooks/use-login.ts +++ b/dashboard/lib/hooks/use-login.ts @@ -1,23 +1,86 @@ 'use client' import useSWR from 'swr' -import { useCallback } from 'react' +import { useCallback, useEffect, useState } from 'react' + +const STORAGE_KEY = 'tinybird_credentials' + +export interface WorkspaceInfo { + name: string + id?: string +} + +export interface StoredCredentials { + token: string + host: string + tenantId?: string + workspace?: WorkspaceInfo +} + +export function getStoredCredentials(): StoredCredentials | null { + if (typeof window === 'undefined') return null + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (!stored) return null + return JSON.parse(stored) + } catch { + return null + } +} + +export function setStoredCredentials(credentials: StoredCredentials): void { + localStorage.setItem(STORAGE_KEY, JSON.stringify(credentials)) +} + +export function clearStoredCredentials(): void { + localStorage.removeItem(STORAGE_KEY) +} const fetcher = (url: string) => fetch(url).then(res => res.json()) export function useLogin() { - const { data, error, isLoading, mutate } = useSWR('/api/auth', fetcher, { - revalidateOnFocus: false, - revalidateOnReconnect: false, - }) + const [hasTokenAuth, setHasTokenAuth] = useState(false) + const [isTokenChecked, setIsTokenChecked] = useState(false) + + // Check for localStorage token on mount + useEffect(() => { + const credentials = getStoredCredentials() + setHasTokenAuth(!!credentials?.token && !!credentials?.host) + setIsTokenChecked(true) + }, []) + + // Check server session auth + const { data, error, isLoading: isSessionLoading, mutate } = useSWR( + '/api/auth', + fetcher, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ) const logout = useCallback(async () => { - await fetch('/api/auth/logout', { method: 'POST' }) - mutate({ authenticated: false }, false) - }, [mutate]) + // Clear localStorage credentials + clearStoredCredentials() + setHasTokenAuth(false) + + // Also logout from server session if authenticated + if (data?.authenticated) { + await fetch('/api/auth/logout', { method: 'POST' }) + mutate({ authenticated: false }, false) + } + + // Force reload to show login dialog + window.location.reload() + }, [data?.authenticated, mutate]) + + const isSessionAuth = data?.authenticated ?? false + const isLoading = isSessionLoading || !isTokenChecked return { - isLoggedIn: data?.authenticated ?? false, + isLoggedIn: isSessionAuth || hasTokenAuth, + isSessionAuth, + isTokenAuth: hasTokenAuth, isLoading, error, logout, diff --git a/dashboard/lib/hooks/use-workspace.ts b/dashboard/lib/hooks/use-workspace.ts index 82b9d07..ab397ca 100644 --- a/dashboard/lib/hooks/use-workspace.ts +++ b/dashboard/lib/hooks/use-workspace.ts @@ -1,6 +1,7 @@ 'use client' import useSWR from 'swr' +import { getStoredCredentials } from './use-login' interface WorkspaceInfo { name: string @@ -14,7 +15,66 @@ interface ConfigResponse { workspace: WorkspaceInfo | null } -const fetcher = (url: string) => fetch(url).then(res => res.json()) +// Extract region info from host URL +function getRegionFromHost(host: string): { provider: string; region: string } { + if (host.includes('localhost') || host.includes('127.0.0.1')) { + return { provider: 'Local', region: 'localhost' } + } + + // Map common hosts to provider/region + if (host.includes('europe-west2')) { + return { provider: 'GCP', region: 'europe-west2' } + } + if (host.includes('api.tinybird.co') && !host.includes('us-east')) { + return { provider: 'GCP', region: 'eu_shared' } + } + if (host.includes('us-east.tinybird.co')) { + return { provider: 'GCP', region: 'us-east' } + } + if (host.includes('northamerica-northeast2')) { + return { provider: 'GCP', region: 'northamerica-northeast2' } + } + if (host.includes('us-east.aws')) { + return { provider: 'AWS', region: 'us-east' } + } + if (host.includes('us-west-2.aws')) { + return { provider: 'AWS', region: 'us-west-2' } + } + if (host.includes('eu-central-1.aws')) { + return { provider: 'AWS', region: 'eu-central-1' } + } + if (host.includes('eu-west-1.aws')) { + return { provider: 'AWS', region: 'eu-west-1' } + } + + return { provider: 'Unknown', region: 'unknown' } +} + +const fetcher = (url: string) => { + const credentials = getStoredCredentials() + + // If we have stored credentials with workspace info, return that directly + if (credentials?.token && credentials?.host && credentials?.workspace) { + const regionInfo = getRegionFromHost(credentials.host) + return Promise.resolve({ + configured: true, + missing: [], + workspace: { + name: credentials.workspace.name, + provider: regionInfo.provider, + region: regionInfo.region, + }, + } as ConfigResponse) + } + + const headers: HeadersInit = {} + if (credentials?.token && credentials?.host) { + headers['X-Tinybird-Token'] = credentials.token + headers['X-Tinybird-Host'] = credentials.host + } + + return fetch(url, { headers }).then(res => res.json()) +} export function useWorkspace() { const { data, error, isLoading } = useSWR('/api/config', fetcher, { diff --git a/dashboard/lib/server.ts b/dashboard/lib/server.ts index 4856b4b..bbe44b7 100644 --- a/dashboard/lib/server.ts +++ b/dashboard/lib/server.ts @@ -43,6 +43,10 @@ export function getServerClient() { return createAnalyticsClient() } +export function createClientWithCredentials(token: string, host: string) { + return createAnalyticsClient({ token, baseUrl: host }) +} + export async function getWorkspace(): Promise { const { token, host } = getTinybirdConfig() @@ -50,6 +54,13 @@ export async function getWorkspace(): Promise { return null } + return getWorkspaceWithCredentials(token, host) +} + +export async function getWorkspaceWithCredentials( + token: string, + host: string +): Promise { const url = new URL('/v1/workspace', host) const response = await fetch(url.toString(), { diff --git a/tinybird/src/client.ts b/tinybird/src/client.ts index 8f63f16..d758f63 100644 --- a/tinybird/src/client.ts +++ b/tinybird/src/client.ts @@ -106,16 +106,21 @@ export const pipes = { // This ensures tinybird.json is found regardless of where the app runs from const __configDir = dirname(fileURLToPath(import.meta.url)); +interface CreateAnalyticsClientOptions { + token?: string; + baseUrl?: string; +} + /** * Create a Tinybird client with custom configuration */ -export function createAnalyticsClient() { +export function createAnalyticsClient(options?: CreateAnalyticsClientOptions) { return createTinybirdClient({ datasources, pipes, configDir: __configDir, - baseUrl: process.env.TINYBIRD_HOST, - token: process.env.TINYBIRD_TOKEN, + baseUrl: options?.baseUrl ?? process.env.TINYBIRD_HOST, + token: options?.token ?? process.env.TINYBIRD_TOKEN, }); } From 901c2e0e38b8ccbb2b6155d3d73461c95ad3ac4d Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 16:16:31 +0100 Subject: [PATCH 4/9] feat: unified auth dialog with token and sign-in modes Create a consolidated AuthDialog component that supports both authenticated mode (username/password) and public mode (direct token via URL params). Workspace info is fetched using the admin token and passed in URL params to avoid permission issues with scoped JWTs. Updated authentication hooks to read credentials from URL params, and API routes to support both session auth and token-based auth via headers. Co-Authored-By: Claude Haiku 4.5 --- dashboard/components/AuthDialog.tsx | 22 ++++------ dashboard/lib/hooks/use-login.ts | 61 ++++++++++++---------------- dashboard/lib/hooks/use-workspace.ts | 50 +++++++++++++++-------- 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/dashboard/components/AuthDialog.tsx b/dashboard/components/AuthDialog.tsx index 4c13c74..f116acb 100644 --- a/dashboard/components/AuthDialog.tsx +++ b/dashboard/components/AuthDialog.tsx @@ -16,7 +16,6 @@ import { Text } from '@/components/ui/Text' import { Loader } from '@/components/ui/Loader' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs' import { SignJWT } from 'jose' -import { setStoredCredentials } from '@/lib/hooks/use-login' type AuthMode = 'signin' | 'token' @@ -207,7 +206,7 @@ export default function AuthDialog({ if (!token || (hostUrl === 'other' && !hostName)) return // Fetch workspace info using the admin token before creating scoped JWT - let workspaceInfo: { name: string; id?: string } | undefined + let workspaceName: string | undefined try { const workspaceUrl = new URL('/v1/workspace', host) const response = await fetch(workspaceUrl.toString(), { @@ -215,7 +214,7 @@ export default function AuthDialog({ }) if (response.ok) { const data = await response.json() - workspaceInfo = { name: data.name, id: data.id } + workspaceName = data.name } } catch { // Workspace fetch failed, continue without it @@ -223,16 +222,13 @@ export default function AuthDialog({ const jwt = await createJwt(token, tenant_id || '') - // Save credentials to localStorage (including workspace info) - setStoredCredentials({ - token: jwt, - host, - tenantId: tenant_id || undefined, - workspace: workspaceInfo, - }) - - // Reload page to use the new credentials - window.location.reload() + // Navigate to URL with token params (including workspace name) + const url = new URL(window.location.href) + url.searchParams.set('token', jwt) + url.searchParams.set('host', host) + if (tenant_id) url.searchParams.set('tenant_id', tenant_id) + if (workspaceName) url.searchParams.set('workspace', workspaceName) + window.location.href = url.toString() } const hostOptions = [ diff --git a/dashboard/lib/hooks/use-login.ts b/dashboard/lib/hooks/use-login.ts index 96a5d82..50ca074 100644 --- a/dashboard/lib/hooks/use-login.ts +++ b/dashboard/lib/hooks/use-login.ts @@ -1,53 +1,43 @@ 'use client' import useSWR from 'swr' -import { useCallback, useEffect, useState } from 'react' - -const STORAGE_KEY = 'tinybird_credentials' - -export interface WorkspaceInfo { - name: string - id?: string -} +import { useSearchParams } from 'next/navigation' +import { useCallback } from 'react' export interface StoredCredentials { token: string host: string tenantId?: string - workspace?: WorkspaceInfo } export function getStoredCredentials(): StoredCredentials | null { if (typeof window === 'undefined') return null try { - const stored = localStorage.getItem(STORAGE_KEY) - if (!stored) return null - return JSON.parse(stored) + const searchParams = new URLSearchParams(window.location.search) + const token = searchParams.get('token') + const host = searchParams.get('host') + if (token && host) { + return { + token, + host, + tenantId: searchParams.get('tenant_id') || undefined, + } + } + return null } catch { return null } } -export function setStoredCredentials(credentials: StoredCredentials): void { - localStorage.setItem(STORAGE_KEY, JSON.stringify(credentials)) -} - -export function clearStoredCredentials(): void { - localStorage.removeItem(STORAGE_KEY) -} - const fetcher = (url: string) => fetch(url).then(res => res.json()) export function useLogin() { - const [hasTokenAuth, setHasTokenAuth] = useState(false) - const [isTokenChecked, setIsTokenChecked] = useState(false) + const searchParams = useSearchParams() - // Check for localStorage token on mount - useEffect(() => { - const credentials = getStoredCredentials() - setHasTokenAuth(!!credentials?.token && !!credentials?.host) - setIsTokenChecked(true) - }, []) + // Check URL params for token auth + const token = searchParams?.get('token') + const host = searchParams?.get('host') + const hasTokenAuth = !!token && !!host // Check server session auth const { data, error, isLoading: isSessionLoading, mutate } = useSWR( @@ -60,9 +50,11 @@ export function useLogin() { ) const logout = useCallback(async () => { - // Clear localStorage credentials - clearStoredCredentials() - setHasTokenAuth(false) + // Clear URL params + const url = new URL(window.location.href) + url.searchParams.delete('token') + url.searchParams.delete('host') + url.searchParams.delete('tenant_id') // Also logout from server session if authenticated if (data?.authenticated) { @@ -70,18 +62,17 @@ export function useLogin() { mutate({ authenticated: false }, false) } - // Force reload to show login dialog - window.location.reload() + // Navigate to URL without token params + window.location.href = url.toString() }, [data?.authenticated, mutate]) const isSessionAuth = data?.authenticated ?? false - const isLoading = isSessionLoading || !isTokenChecked return { isLoggedIn: isSessionAuth || hasTokenAuth, isSessionAuth, isTokenAuth: hasTokenAuth, - isLoading, + isLoading: isSessionLoading, error, logout, } diff --git a/dashboard/lib/hooks/use-workspace.ts b/dashboard/lib/hooks/use-workspace.ts index ab397ca..457517a 100644 --- a/dashboard/lib/hooks/use-workspace.ts +++ b/dashboard/lib/hooks/use-workspace.ts @@ -1,6 +1,7 @@ 'use client' import useSWR from 'swr' +import { useSearchParams } from 'next/navigation' import { getStoredCredentials } from './use-login' interface WorkspaceInfo { @@ -53,20 +54,6 @@ function getRegionFromHost(host: string): { provider: string; region: string } { const fetcher = (url: string) => { const credentials = getStoredCredentials() - // If we have stored credentials with workspace info, return that directly - if (credentials?.token && credentials?.host && credentials?.workspace) { - const regionInfo = getRegionFromHost(credentials.host) - return Promise.resolve({ - configured: true, - missing: [], - workspace: { - name: credentials.workspace.name, - provider: regionInfo.provider, - region: regionInfo.region, - }, - } as ConfigResponse) - } - const headers: HeadersInit = {} if (credentials?.token && credentials?.host) { headers['X-Tinybird-Token'] = credentials.token @@ -77,10 +64,37 @@ const fetcher = (url: string) => { } export function useWorkspace() { - const { data, error, isLoading } = useSWR('/api/config', fetcher, { - revalidateOnFocus: false, - revalidateOnReconnect: false, - }) + const searchParams = useSearchParams() + + // Check URL params for workspace info (from token mode) + const workspaceFromUrl = searchParams?.get('workspace') + const hostFromUrl = searchParams?.get('host') + const hasUrlWorkspace = !!workspaceFromUrl && !!hostFromUrl + + // Always call useSWR but skip fetch if we have URL params + const { data, error, isLoading } = useSWR( + hasUrlWorkspace ? null : '/api/config', + fetcher, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ) + + // If we have workspace name in URL params, use that directly + if (hasUrlWorkspace) { + const regionInfo = getRegionFromHost(hostFromUrl) + return { + workspace: { + name: workspaceFromUrl, + provider: regionInfo.provider, + region: regionInfo.region, + }, + isConfigured: true, + isLoading: false, + error: null, + } + } return { workspace: data?.workspace ?? null, From 68365262fa902cc68e1e15a48b5f76e66b9366d6 Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 16:18:24 +0100 Subject: [PATCH 5/9] fix: pass workspace name via header to avoid JWT permission issues The scoped JWT doesn't have permission to read /v1/workspace. Now the workspace name from URL params is also passed as X-Tinybird-Workspace header to the API, which uses it directly instead of trying to fetch. Co-Authored-By: Claude Haiku 4.5 --- dashboard/app/api/config/route.ts | 14 +++++++++----- dashboard/lib/hooks/use-workspace.ts | 25 +++++++++++++++++-------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/dashboard/app/api/config/route.ts b/dashboard/app/api/config/route.ts index cb848ee..18408a2 100644 --- a/dashboard/app/api/config/route.ts +++ b/dashboard/app/api/config/route.ts @@ -68,6 +68,7 @@ export async function GET(request: NextRequest) { // Check for token from headers first (public mode), then fall back to env vars const headerToken = request.headers.get('X-Tinybird-Token') const headerHost = request.headers.get('X-Tinybird-Host') + const headerWorkspace = request.headers.get('X-Tinybird-Workspace') const { token: envToken, host: envHost } = getTinybirdConfig() const token = headerToken || envToken @@ -82,17 +83,20 @@ export async function GET(request: NextRequest) { // Include workspace info if configured const regionInfo = host ? await getRegionInfoFromHost(host) : null - // Fetch workspace using header credentials if provided, otherwise use env vars - const tinybirdWorkspace = configured - ? headerToken && headerHost + // Use workspace name from header if provided (avoids permission issues with scoped JWT) + // Otherwise fetch using credentials + let workspaceName = headerWorkspace + if (!workspaceName && configured) { + const tinybirdWorkspace = headerToken && headerHost ? await getWorkspaceWithCredentials(headerToken, headerHost) : await getWorkspace() - : null + workspaceName = tinybirdWorkspace?.name || null + } const workspace = configured && host && regionInfo ? { - name: tinybirdWorkspace?.name || 'Unknown', + name: workspaceName || 'Unknown', provider: regionInfo.provider, region: regionInfo.region, } diff --git a/dashboard/lib/hooks/use-workspace.ts b/dashboard/lib/hooks/use-workspace.ts index 457517a..4586c72 100644 --- a/dashboard/lib/hooks/use-workspace.ts +++ b/dashboard/lib/hooks/use-workspace.ts @@ -51,16 +51,22 @@ function getRegionFromHost(host: string): { provider: string; region: string } { return { provider: 'Unknown', region: 'unknown' } } -const fetcher = (url: string) => { - const credentials = getStoredCredentials() +function createFetcher(workspaceName: string | null) { + return (url: string) => { + const credentials = getStoredCredentials() - const headers: HeadersInit = {} - if (credentials?.token && credentials?.host) { - headers['X-Tinybird-Token'] = credentials.token - headers['X-Tinybird-Host'] = credentials.host - } + const headers: HeadersInit = {} + if (credentials?.token && credentials?.host) { + headers['X-Tinybird-Token'] = credentials.token + headers['X-Tinybird-Host'] = credentials.host + } + // Pass workspace name from URL params to avoid permission issues with scoped JWT + if (workspaceName) { + headers['X-Tinybird-Workspace'] = workspaceName + } - return fetch(url, { headers }).then(res => res.json()) + return fetch(url, { headers }).then(res => res.json()) + } } export function useWorkspace() { @@ -71,6 +77,9 @@ export function useWorkspace() { const hostFromUrl = searchParams?.get('host') const hasUrlWorkspace = !!workspaceFromUrl && !!hostFromUrl + // Create fetcher with workspace name from URL (to pass as header) + const fetcher = createFetcher(workspaceFromUrl) + // Always call useSWR but skip fetch if we have URL params const { data, error, isLoading } = useSWR( hasUrlWorkspace ? null : '/api/config', From 3b727058f77cdf92c22a6805c5fdee046bae440d Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 16:35:03 +0100 Subject: [PATCH 6/9] fix: support token auth in middleware and improve workspace detection - Add token/host header check in middleware to allow public mode requests - Update /api/auth to return authenticated:true when token headers present - Decode JWT to extract workspace name as fallback in /api/config - Pass token/host headers when checking auth status in use-login - Use devMode: false for token-based client to avoid branch mode Co-Authored-By: Claude Opus 4.5 --- dashboard/app/api/auth/route.ts | 11 +++++++++-- dashboard/app/api/config/route.ts | 23 ++++++++++++++++++++++- dashboard/lib/hooks/use-login.ts | 14 +++++++++++++- dashboard/lib/server.ts | 2 +- dashboard/middleware.ts | 7 +++++++ tinybird/src/client.ts | 12 ++++++++++-- 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/dashboard/app/api/auth/route.ts b/dashboard/app/api/auth/route.ts index 182cae7..0a9f739 100644 --- a/dashboard/app/api/auth/route.ts +++ b/dashboard/app/api/auth/route.ts @@ -1,4 +1,4 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { cookies } from 'next/headers' import { createHash } from 'crypto' @@ -31,12 +31,19 @@ function validateSession(sessionToken: string): boolean { } // GET - Check auth status -export async function GET() { +export async function GET(request: NextRequest) { // If auth is disabled, always return authenticated if (process.env.DISABLE_AUTH === 'true') { return NextResponse.json({ authenticated: true }) } + // Check for token auth from headers (passed from URL params by client) + const headerToken = request.headers.get('X-Tinybird-Token') + const headerHost = request.headers.get('X-Tinybird-Host') + if (headerToken && headerHost) { + return NextResponse.json({ authenticated: true }) + } + const cookieStore = await cookies() const sessionCookie = cookieStore.get(SESSION_COOKIE_NAME) diff --git a/dashboard/app/api/config/route.ts b/dashboard/app/api/config/route.ts index 18408a2..28aba45 100644 --- a/dashboard/app/api/config/route.ts +++ b/dashboard/app/api/config/route.ts @@ -13,7 +13,23 @@ interface TinybirdRegionResponse { api_host: string } -// Cache for regions to avoid repeated API calls +// Try to extract workspace name from JWT token payload +function extractWorkspaceNameFromToken(token: string): string | null { + try { + const payload = token.split('.')[1] + if (!payload) return null + const base64 = + payload.replace(/-/g, '+').replace(/_/g, '/') + + '==='.slice((payload.length + 3) % 4) + const json = Buffer.from(base64, 'base64').toString() + const data = JSON.parse(json) + // Skip 'frontend_jwt' as it's just a token label, not workspace name + const name = data.workspace_name || (data.name !== 'frontend_jwt' ? data.name : null) + return name || null + } catch { + return null + } +} async function fetchTinybirdRegions(): Promise { try { @@ -93,6 +109,11 @@ export async function GET(request: NextRequest) { workspaceName = tinybirdWorkspace?.name || null } + // If still no workspace name and we have a token, try to decode it + if (!workspaceName && headerToken) { + workspaceName = extractWorkspaceNameFromToken(headerToken) + } + const workspace = configured && host && regionInfo ? { diff --git a/dashboard/lib/hooks/use-login.ts b/dashboard/lib/hooks/use-login.ts index 50ca074..c347882 100644 --- a/dashboard/lib/hooks/use-login.ts +++ b/dashboard/lib/hooks/use-login.ts @@ -29,7 +29,16 @@ export function getStoredCredentials(): StoredCredentials | null { } } -const fetcher = (url: string) => fetch(url).then(res => res.json()) +function createFetcher(token: string | null, host: string | null) { + return (url: string) => { + const headers: HeadersInit = {} + if (token && host) { + headers['X-Tinybird-Token'] = token + headers['X-Tinybird-Host'] = host + } + return fetch(url, { headers }).then(res => res.json()) + } +} export function useLogin() { const searchParams = useSearchParams() @@ -39,6 +48,9 @@ export function useLogin() { const host = searchParams?.get('host') const hasTokenAuth = !!token && !!host + // Create fetcher with token/host to pass as headers + const fetcher = createFetcher(token, host) + // Check server session auth const { data, error, isLoading: isSessionLoading, mutate } = useSWR( '/api/auth', diff --git a/dashboard/lib/server.ts b/dashboard/lib/server.ts index bbe44b7..fb7442e 100644 --- a/dashboard/lib/server.ts +++ b/dashboard/lib/server.ts @@ -44,7 +44,7 @@ export function getServerClient() { } export function createClientWithCredentials(token: string, host: string) { - return createAnalyticsClient({ token, baseUrl: host }) + return createAnalyticsClient({ token, baseUrl: host, devMode: false }) } export async function getWorkspace(): Promise { diff --git a/dashboard/middleware.ts b/dashboard/middleware.ts index 59d7377..35ad48b 100644 --- a/dashboard/middleware.ts +++ b/dashboard/middleware.ts @@ -24,6 +24,13 @@ export function middleware(request: NextRequest) { return NextResponse.next() } + // Check for token auth from headers (public mode) + const headerToken = request.headers.get('X-Tinybird-Token') + const headerHost = request.headers.get('X-Tinybird-Host') + if (headerToken && headerHost) { + return NextResponse.next() + } + // Check for session cookie const sessionCookie = request.cookies.get(SESSION_COOKIE_NAME) diff --git a/tinybird/src/client.ts b/tinybird/src/client.ts index d758f63..d721f41 100644 --- a/tinybird/src/client.ts +++ b/tinybird/src/client.ts @@ -109,19 +109,27 @@ const __configDir = dirname(fileURLToPath(import.meta.url)); interface CreateAnalyticsClientOptions { token?: string; baseUrl?: string; + devMode?: boolean | "branch"; } /** * Create a Tinybird client with custom configuration */ export function createAnalyticsClient(options?: CreateAnalyticsClientOptions) { - return createTinybirdClient({ + const clientOptions: Parameters[0] = { datasources, pipes, configDir: __configDir, baseUrl: options?.baseUrl ?? process.env.TINYBIRD_HOST, token: options?.token ?? process.env.TINYBIRD_TOKEN, - }); + }; + + // Only set devMode if explicitly provided (otherwise use tinybird.json config) + if (options?.devMode !== undefined) { + clientOptions.devMode = options.devMode; + } + + return createTinybirdClient(clientOptions); } /** From 5eb2905ecb9976c4cd88a9361d2adea7716604ba Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 16:40:27 +0100 Subject: [PATCH 7/9] always point to the token workspace --- tinybird/src/client.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tinybird/src/client.ts b/tinybird/src/client.ts index d721f41..8ee5e48 100644 --- a/tinybird/src/client.ts +++ b/tinybird/src/client.ts @@ -109,7 +109,7 @@ const __configDir = dirname(fileURLToPath(import.meta.url)); interface CreateAnalyticsClientOptions { token?: string; baseUrl?: string; - devMode?: boolean | "branch"; + devMode?: boolean; } /** @@ -122,13 +122,9 @@ export function createAnalyticsClient(options?: CreateAnalyticsClientOptions) { configDir: __configDir, baseUrl: options?.baseUrl ?? process.env.TINYBIRD_HOST, token: options?.token ?? process.env.TINYBIRD_TOKEN, + devMode: options?.devMode ?? process.env.NODE_ENV === "development", }; - // Only set devMode if explicitly provided (otherwise use tinybird.json config) - if (options?.devMode !== undefined) { - clientOptions.devMode = options.devMode; - } - return createTinybirdClient(clientOptions); } From fd93aa792bcfbf1bfdaa49f6360d4a4b6648d236 Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 16:44:03 +0100 Subject: [PATCH 8/9] bump --- dashboard/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- tinybird/package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dashboard/package.json b/dashboard/package.json index 780f9c7..8cbdce6 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -39,7 +39,7 @@ "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.7", "@tinybirdco/analytics-client": "workspace:*", - "@tinybirdco/sdk": "^0.0.26", + "@tinybirdco/sdk": "^0.0.27", "ai": "^4.3.17", "class-variance-authority": "^0.7.1", "country-flag-icons": "^1.5.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea3e622..edc5861 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,8 +71,8 @@ importers: specifier: workspace:* version: link:../tinybird '@tinybirdco/sdk': - specifier: ^0.0.26 - version: 0.0.26 + specifier: ^0.0.27 + version: 0.0.27 ai: specifier: ^4.3.17 version: 4.3.19(react@18.3.1)(zod@3.25.76) @@ -196,8 +196,8 @@ importers: tinybird: dependencies: '@tinybirdco/sdk': - specifier: ^0.0.26 - version: 0.0.26 + specifier: ^0.0.27 + version: 0.0.27 devDependencies: '@types/node': specifier: ^22.13.10 @@ -2085,8 +2085,8 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tinybirdco/sdk@0.0.26': - resolution: {integrity: sha512-xHurGkKtKS1Kp7wyZ/KnfBF0oDyqL+S3gDm82IOUT4mU1CCCnt2+n2aIqJ1trqbSCCMZGtSmIsJVWlxjXA2PFw==} + '@tinybirdco/sdk@0.0.27': + resolution: {integrity: sha512-cDWJSSULHf1l8dWW7WTpMupcmOPFHdXDQIJ5kUhoL/y8QZ2NDUdheOZyC8y6u3VebpNXpPXPtiVAFA4JCnUFkg==} engines: {node: '>=20.0.0'} hasBin: true @@ -7145,7 +7145,7 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tinybirdco/sdk@0.0.26': + '@tinybirdco/sdk@0.0.27': dependencies: '@clack/prompts': 1.0.0 chokidar: 4.0.3 diff --git a/tinybird/package.json b/tinybird/package.json index 1b40cf0..208c1c5 100644 --- a/tinybird/package.json +++ b/tinybird/package.json @@ -13,7 +13,7 @@ "preview": "tinybird preview" }, "dependencies": { - "@tinybirdco/sdk": "^0.0.26" + "@tinybirdco/sdk": "^0.0.27" }, "devDependencies": { "@types/node": "^22.13.10", From 5247407fabe0af554fc21b46b478e0879af214d4 Mon Sep 17 00:00:00 2001 From: Rafa Moreno Date: Tue, 10 Feb 2026 17:08:44 +0100 Subject: [PATCH 9/9] just add fixed params if needed --- dashboard/components/AuthDialog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dashboard/components/AuthDialog.tsx b/dashboard/components/AuthDialog.tsx index f116acb..70c97d2 100644 --- a/dashboard/components/AuthDialog.tsx +++ b/dashboard/components/AuthDialog.tsx @@ -99,10 +99,12 @@ export async function createJwt( 'tenant_domains_mv', ] + const filter = tenant_id ? `tenant_id = '${tenant_id}'` : '' + const fixed_params = tenant_id ? { tenant_id } : {} const datasources_scopes = datasources_resources.map(resource => ({ type: 'DATASOURCES:READ', resource, - filter: `tenant_id = '${tenant_id}'`, + filter, })) const payload = { @@ -113,9 +115,7 @@ export async function createJwt( ...resources.map(resource => ({ type: 'PIPES:READ', resource, - fixed_params: { - tenant_id: tenant_id, - }, + fixed_params, })), ...datasources_scopes, ],