Skip to content

Commit 56ad5d6

Browse files
committed
token validation
1 parent 4a4a8fd commit 56ad5d6

File tree

4 files changed

+109
-25
lines changed

4 files changed

+109
-25
lines changed

apps/web/src/app/[id]/page.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,53 @@
22

33
import { useQueryState } from 'nuqs';
44
import React, { useEffect, useState } from 'react';
5-
import { checkToolState } from '@/lib/tinybird';
5+
import { checkToolState, InvalidTokenError } from '@/lib/tinybird';
66
import { TOOL_IMPORTS, type ToolId, TOOLS, type ToolState } from '@/lib/constants';
7+
import { useRouter } from 'next/navigation';
8+
import TokenPrompt from '@/components/token-prompt';
79

810
export default function AppPage({ params }: { params: Promise<{ id: string }> }) {
911
const [token, setToken] = useQueryState('token');
1012
const [toolState, setToolState] = useState<ToolState>('available');
13+
const [error, setError] = useState<string>();
14+
const [isValidToken, setIsValidToken] = useState(false);
1115
const { id } = React.use(params) as { id: ToolId };
16+
const router = useRouter();
1217

1318
useEffect(() => {
1419
async function checkInstallation() {
15-
if (!token) return;
16-
const state = await checkToolState(token, TOOLS[id].ds);
17-
setToolState(state);
20+
if (!token) {
21+
setIsValidToken(false);
22+
return;
23+
}
24+
setError(undefined);
25+
try {
26+
const state = await checkToolState(token, TOOLS[id].ds);
27+
setToolState(state);
28+
setIsValidToken(true);
29+
} catch (error) {
30+
if (error instanceof InvalidTokenError) {
31+
setError('Invalid token');
32+
setToken(null);
33+
setIsValidToken(false);
34+
router.push('/');
35+
} else {
36+
console.error('Failed to check tool state:', error);
37+
setError('Failed to check tool state');
38+
setIsValidToken(false);
39+
}
40+
}
1841
}
1942
checkInstallation();
20-
}, [token, id]);
43+
}, [token, id, setToken, router]);
44+
45+
if (!token || !isValidToken) {
46+
return (
47+
<div className="container py-6">
48+
<TokenPrompt error={error} />
49+
</div>
50+
);
51+
}
2152

2253
if (!(id in TOOLS)) {
2354
return <div>Tool not found</div>;
@@ -37,6 +68,9 @@ export default function AppPage({ params }: { params: Promise<{ id: string }> })
3768
<h1 className="text-2xl font-bold">{TOOLS[id].name}</h1>
3869
<span className="text-sm text-muted-foreground">({toolState})</span>
3970
</div>
71+
{error && (
72+
<p className="text-sm text-red-500">{error}</p>
73+
)}
4074
<Component />
4175
</div>
4276
</div>

apps/web/src/app/page.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@ import { useQueryState } from 'nuqs';
44
import { useState, useEffect } from 'react';
55
import { Card } from '@/components/ui/card';
66
import Link from 'next/link';
7-
import { checkToolState } from '@/lib/tinybird';
7+
import { checkToolState, InvalidTokenError } from '@/lib/tinybird';
88
import { TOOLS, type AppGridItem, type ToolState } from '@/lib/constants';
99
import TokenPrompt from '@/components/token-prompt';
1010

1111
export default function Home() {
12-
const [token] = useQueryState('token');
12+
const [token, setToken] = useQueryState('token');
1313
const [toolStates, setToolStates] = useState<Record<string, ToolState>>({});
1414
const [isLoading, setIsLoading] = useState(false);
15+
const [error, setError] = useState<string>();
16+
const [isValidToken, setIsValidToken] = useState(false);
1517

1618
useEffect(() => {
1719
async function fetchToolStates() {
18-
if (!token) return;
20+
if (!token) {
21+
setIsValidToken(false);
22+
return;
23+
}
1924
setIsLoading(true);
25+
setError(undefined);
2026
try {
2127
const states = await Promise.all(
2228
Object.values(TOOLS).map(async (app) => {
@@ -25,25 +31,40 @@ export default function Home() {
2531
})
2632
);
2733
setToolStates(Object.fromEntries(states));
34+
setIsValidToken(true);
2835
} catch (error) {
29-
console.error('Failed to fetch tool states:', error);
36+
if (error instanceof InvalidTokenError) {
37+
setError('Invalid token');
38+
setToken(null);
39+
setIsValidToken(false);
40+
} else {
41+
console.error('Failed to fetch tool states:', error);
42+
setError('Failed to fetch tool states');
43+
setIsValidToken(false);
44+
}
3045
} finally {
3146
setIsLoading(false);
3247
}
3348
}
3449

3550
fetchToolStates();
36-
}, [token]);
51+
}, [token, setToken]);
52+
53+
if (!token || !isValidToken) {
54+
return (
55+
<div className="container py-6">
56+
<TokenPrompt error={error} />
57+
</div>
58+
);
59+
}
3760

3861
return (
3962
<div className="container py-6">
40-
<TokenPrompt />
41-
{token && isLoading && (
63+
{isLoading ? (
4264
<div className="flex items-center justify-center">
4365
<p className="text-lg font-semibold">Loading...</p>
4466
</div>
45-
)}
46-
{token && !isLoading && (
67+
) : (
4768
<div className="space-y-8">
4869
{/* Configured Apps */}
4970
{Object.values(TOOLS).some(app => toolStates[app.id] === 'configured') && (

apps/web/src/components/token-prompt.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { Button } from './ui/button'
77
import { Input } from './ui/input'
88
import Link from 'next/link'
99

10-
export default function TokenPrompt() {
10+
export default function TokenPrompt({ error }: { error?: string }) {
1111
const [token, setToken] = useQueryState('token')
1212
const [inputToken, setInputToken] = useState('')
1313

1414
const handleSave = () => {
15+
if (!inputToken.trim()) return;
1516
setToken(inputToken)
1617
}
1718

@@ -26,13 +27,19 @@ export default function TokenPrompt() {
2627
<p className="text-sm text-muted-foreground">
2728
Enter your Tinybird admin token to continue
2829
</p>
29-
<div className="flex w-full max-w-sm items-center space-x-2">
30-
<Input
31-
placeholder="tb_admin_xxxx"
32-
value={inputToken}
33-
onChange={(e) => setInputToken(e.target.value)}
34-
/>
35-
<Button onClick={handleSave}>Save</Button>
30+
<div className="flex w-full max-w-sm flex-col gap-2">
31+
<div className="flex items-center space-x-2">
32+
<Input
33+
placeholder="Enter your token"
34+
value={inputToken}
35+
onChange={(e) => setInputToken(e.target.value)}
36+
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
37+
/>
38+
<Button onClick={handleSave}>Save</Button>
39+
</div>
40+
{error && (
41+
<p className="text-sm text-red-500">{error}</p>
42+
)}
3643
</div>
3744
</div>
3845

apps/web/src/lib/tinybird.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { type ToolState } from './constants';
22

3+
export class InvalidTokenError extends Error {
4+
constructor() {
5+
super('Invalid token');
6+
this.name = 'InvalidTokenError';
7+
}
8+
}
9+
310
export interface TinybirdDataSource {
411
name: string;
512
description?: string;
@@ -13,10 +20,16 @@ export async function listDataSources(token: string): Promise<TinybirdDataSource
1320
const response = await fetch(`/api/datasources?token=${token}`);
1421

1522
if (!response.ok) {
23+
if (response.status === 401 || response.status === 403) {
24+
throw new InvalidTokenError();
25+
}
1626
throw new Error('Failed to fetch data sources');
1727
}
1828

1929
const data = await response.json();
30+
if (!data?.datasources) {
31+
throw new InvalidTokenError();
32+
}
2033
return data.datasources;
2134
}
2235

@@ -35,6 +48,9 @@ export async function query(token: string, sql: string): Promise<QueryResult> {
3548
const response = await fetch(`/api/query?token=${token}&query=${encodeURIComponent(sql)}`);
3649

3750
if (!response.ok) {
51+
if (response.status === 401 || response.status === 403) {
52+
throw new InvalidTokenError();
53+
}
3854
throw new Error('Failed to execute query');
3955
}
4056

@@ -52,13 +68,16 @@ export async function checkToolState(token: string, datasource: string): Promise
5268
return 'available';
5369
}
5470

55-
// Check if there's any data
56-
const result = await query(token, `SELECT count() as count FROM ${datasource} FORMAT JSON`);
71+
// Then check if it has data
72+
const result = await query(token, `SELECT count(*) as count FROM ${datasource} FORMAT JSON`);
5773
const hasData = result.data[0]?.count > 0;
5874

5975
return hasData ? 'configured' : 'installed';
6076
} catch (error) {
61-
console.error('Failed to check tool state:', error);
77+
if (error instanceof InvalidTokenError) {
78+
throw error;
79+
}
80+
console.error('Error checking tool state:', error);
6281
return 'available';
6382
}
6483
}
@@ -79,6 +98,9 @@ export async function pipe<T = QueryResult>(
7998
const response = await fetch(`/api/pipes?${searchParams}`);
8099

81100
if (!response.ok) {
101+
if (response.status === 401 || response.status === 403) {
102+
throw new InvalidTokenError();
103+
}
82104
throw new Error('Failed to execute pipe');
83105
}
84106

0 commit comments

Comments
 (0)