Skip to content

Commit 1dd3eda

Browse files
committed
feat(ui): add API key management to welcome step
- Integrate API key creation/management into onboarding - Detect existing keys and handle authentication - Add loading states and clipboard copy functionality - Improve UI messaging for self-hosting status
1 parent a88f9b2 commit 1dd3eda

File tree

1 file changed

+277
-57
lines changed

1 file changed

+277
-57
lines changed

apps/ui/src/components/onboarding/welcome-step.tsx

Lines changed: 277 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,300 @@
1-
import { Rocket } from "lucide-react";
1+
import { useQueryClient } from "@tanstack/react-query";
2+
import { Copy, Key, KeyIcon } from "lucide-react";
3+
import { useRouter } from "next/navigation";
24
import * as React from "react";
5+
import { useState, useEffect, useCallback } from "react";
36

7+
import { useDefaultProject } from "@/hooks/useDefaultProject";
48
import { useAuth } from "@/lib/auth-client";
5-
import {
6-
Card,
7-
CardContent,
8-
CardDescription,
9-
CardHeader,
10-
CardTitle,
11-
} from "@/lib/components/card";
9+
import { Button } from "@/lib/components/button";
1210
import { Step } from "@/lib/components/stepper";
11+
import { toast } from "@/lib/components/use-toast";
12+
import { useApi } from "@/lib/fetch-client";
1313

1414
export function WelcomeStep() {
1515
const { useSession } = useAuth();
1616
const session = useSession();
1717
const user = session.data?.user;
18-
const organization = { name: "Your Organization" };
18+
const { data: defaultProject } = useDefaultProject();
19+
const api = useApi();
20+
const queryClient = useQueryClient();
21+
const router = useRouter();
22+
const [apiKey, setApiKey] = useState<string | null>(null);
23+
const [isLocalhost, setIsLocalhost] = useState(false);
24+
25+
const createApiKeyMutation = api.useMutation("post", "/keys/api");
26+
27+
// Fetch existing API keys to check if one already exists
28+
const { data: existingKeys, refetch: refetchKeys } = api.useQuery(
29+
"get",
30+
"/keys/api",
31+
{
32+
params: {
33+
query: { projectId: defaultProject?.id || "" },
34+
},
35+
},
36+
{
37+
enabled: !!defaultProject?.id && !!user, // Only fetch if user is authenticated
38+
staleTime: 0, // Always fetch fresh data
39+
refetchOnWindowFocus: true,
40+
refetchOnMount: true,
41+
},
42+
);
43+
44+
// Detect localhost/self-hosting
45+
useEffect(() => {
46+
if (typeof window !== "undefined") {
47+
const hostname = window.location.hostname;
48+
setIsLocalhost(
49+
hostname === "localhost" ||
50+
hostname === "127.0.0.1" ||
51+
hostname.includes("192.168.") ||
52+
hostname.includes("10.0."),
53+
);
54+
}
55+
}, []);
56+
57+
// Refetch API keys when component mounts to get fresh data
58+
useEffect(() => {
59+
if (defaultProject?.id && user) {
60+
refetchKeys();
61+
}
62+
}, [defaultProject?.id, user, refetchKeys]);
63+
64+
// Also invalidate cache on mount to ensure fresh data
65+
useEffect(() => {
66+
if (defaultProject?.id) {
67+
const queryKey = api.queryOptions("get", "/keys/api", {
68+
params: {
69+
query: { projectId: defaultProject.id },
70+
},
71+
}).queryKey;
72+
queryClient.invalidateQueries({ queryKey });
73+
}
74+
}, [defaultProject?.id, api, queryClient]);
75+
76+
const createApiKey = useCallback(async () => {
77+
// Security: Multiple authentication checks
78+
if (!user) {
79+
console.error("Unauthorized: No user session");
80+
toast({
81+
title: "Authentication Error",
82+
description: "Please sign in to create API keys.",
83+
variant: "destructive",
84+
});
85+
return;
86+
}
87+
88+
if (!defaultProject?.id) {
89+
console.error("Unauthorized: No project access");
90+
toast({
91+
title: "Access Error",
92+
description: "No project found. Please contact support.",
93+
variant: "destructive",
94+
});
95+
return;
96+
}
97+
98+
try {
99+
const response = await createApiKeyMutation.mutateAsync({
100+
body: {
101+
description: "GATEWAY-001",
102+
projectId: defaultProject.id,
103+
usageLimit: null,
104+
},
105+
});
106+
setApiKey(response.apiKey.token);
107+
108+
// Invalidate API keys cache to ensure other components see the new key
109+
const queryKey = api.queryOptions("get", "/keys/api", {
110+
params: {
111+
query: { projectId: defaultProject.id },
112+
},
113+
}).queryKey;
114+
queryClient.invalidateQueries({ queryKey });
115+
} catch (error) {
116+
console.error("Failed to create API key:", error);
117+
toast({
118+
title: "Error",
119+
description:
120+
"Failed to create API key. We'll help you create one in the next step.",
121+
variant: "destructive",
122+
});
123+
}
124+
}, [user, defaultProject?.id, createApiKeyMutation]);
125+
126+
// Auto-create API key on component mount only if no existing keys
127+
useEffect(() => {
128+
// Security: Only proceed if user is authenticated
129+
if (!user) {
130+
return;
131+
}
132+
133+
// Don't create keys if we're still loading existing keys data
134+
if (existingKeys === undefined) {
135+
return;
136+
}
137+
138+
const hasExistingKeys =
139+
existingKeys?.apiKeys && existingKeys.apiKeys.length > 0;
140+
const shouldCreateKey =
141+
defaultProject?.id &&
142+
!apiKey &&
143+
!createApiKeyMutation.isPending &&
144+
!hasExistingKeys;
145+
146+
if (shouldCreateKey) {
147+
createApiKey();
148+
}
149+
}, [
150+
user,
151+
defaultProject?.id,
152+
apiKey,
153+
createApiKeyMutation.isPending,
154+
createApiKey,
155+
existingKeys,
156+
]);
157+
158+
const copyToClipboard = () => {
159+
if (apiKey) {
160+
navigator.clipboard.writeText(apiKey);
161+
toast({
162+
title: "Copied to clipboard",
163+
description: "API key copied to clipboard",
164+
});
165+
}
166+
};
167+
168+
// Security: Ensure user is authenticated before proceeding
169+
if (!user) {
170+
return (
171+
<Step>
172+
<div className="flex flex-col gap-6 text-center">
173+
<h1 className="text-2xl font-bold">Authentication Required</h1>
174+
<p className="text-muted-foreground">
175+
Please sign in to continue with the onboarding process.
176+
</p>
177+
</div>
178+
</Step>
179+
);
180+
}
19181

20182
return (
21183
<Step>
22-
<div className="flex flex-col gap-6">
23-
<div className="flex flex-col gap-2 text-center">
24-
<h1 className="text-2xl font-bold">Welcome to LLM Gateway!</h1>
25-
<p className="text-muted-foreground">
26-
Let's get you set up with everything you need to start using the
27-
platform.
184+
<div className="space-y-8">
185+
{/* Hero Section */}
186+
<div className="text-center space-y-3">
187+
<h1 className="text-3xl font-semibold tracking-tight">
188+
Welcome to LLM Gateway
189+
</h1>
190+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
191+
{isLocalhost
192+
? `You're all set up for self-hosting! ${!existingKeys?.apiKeys?.length ? "Let's get you connected to the platform with your first API key." : ""}`
193+
: `${existingKeys?.apiKeys?.length ? "You can skip this step and go to the dashboard to manage your API keys." : "Let's get you connected to the platform with your first API key."}`}
28194
</p>
29195
</div>
30196

31-
<Card>
32-
<CardHeader>
33-
<CardTitle className="flex items-center gap-2">
34-
<Rocket className="h-5 w-5" />
35-
Your Project is Ready
36-
</CardTitle>
37-
<CardDescription>
38-
We've automatically created a project for you to get started.
39-
</CardDescription>
40-
</CardHeader>
41-
<CardContent className="flex flex-col gap-4">
42-
<div className="grid grid-cols-2 gap-4">
43-
<div>
44-
<p className="text-sm font-medium">User</p>
45-
<p className="text-muted-foreground text-sm">{user?.name}</p>
46-
</div>
47-
<div>
48-
<p className="text-sm font-medium">Email</p>
49-
<p className="text-muted-foreground text-sm">{user?.email}</p>
197+
{/* API Key Status Card/Alert */}
198+
{(apiKey ||
199+
createApiKeyMutation.isPending ||
200+
(existingKeys?.apiKeys && existingKeys.apiKeys.length > 0)) && (
201+
<div
202+
className={`rounded-xl border-2 p-4 ${
203+
apiKey ||
204+
(existingKeys?.apiKeys && existingKeys.apiKeys.length > 0)
205+
? "border-green-200 bg-gradient-to-br from-green-50 to-emerald-50 dark:border-green-800 dark:from-green-950 dark:to-emerald-950"
206+
: "border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50 dark:border-blue-800 dark:from-blue-950 dark:to-indigo-950"
207+
}`}
208+
>
209+
<div className="flex flex-col items-center md:items-start md:flex-row gap-4">
210+
<div
211+
className={`flex-shrink-0 w-12 h-12 rounded-full flex items-center justify-center ${
212+
apiKey ||
213+
(existingKeys?.apiKeys && existingKeys.apiKeys.length > 0)
214+
? "bg-green-100 dark:bg-green-900"
215+
: "bg-blue-100 dark:bg-blue-900"
216+
}`}
217+
>
218+
{apiKey ||
219+
(existingKeys?.apiKeys && existingKeys.apiKeys.length > 0) ? (
220+
<KeyIcon className="h-6 w-6 text-green-600 dark:text-green-400" />
221+
) : (
222+
<Key className="h-6 w-6 text-blue-600 dark:text-blue-400" />
223+
)}
50224
</div>
51-
<div>
52-
<p className="text-sm font-medium">Organization</p>
53-
<p className="text-muted-foreground text-sm">
54-
{organization?.name}
55-
</p>
56-
</div>
57-
<div>
58-
<p className="text-sm font-medium">Project</p>
59-
<p className="text-muted-foreground text-sm">Default Project</p>
225+
<div className="flex-1 min-w-0">
226+
<h2 className="text-xl font-medium mb-2 text-green-900 dark:text-green-100 text-center md:text-left">
227+
{apiKey
228+
? "Your API key is ready!"
229+
: existingKeys?.apiKeys && existingKeys.apiKeys.length > 0
230+
? "You can already access models through LLM Gateway"
231+
: "Creating your API key..."}
232+
</h2>
233+
234+
{apiKey ? (
235+
<div className="space-y-3">
236+
<div className="bg-white dark:bg-slate-800 rounded-lg p-4 border border-slate-200 dark:border-slate-600">
237+
<div className="flex items-center justify-between gap-3">
238+
<code className="text-sm font-mono break-all text-slate-900 dark:text-slate-100">
239+
{apiKey || "Generating API key..."}
240+
</code>
241+
<Button
242+
variant="outline"
243+
size="sm"
244+
onClick={copyToClipboard}
245+
className="flex-shrink-0"
246+
>
247+
<Copy className="h-4 w-4 mr-2" />
248+
Copy
249+
</Button>
250+
</div>
251+
<p className="text-xs text-slate-500 dark:text-slate-500 pt-1">
252+
Copy and store this key securely. You won't be able to
253+
see it again. Check the docs on how to use it.
254+
</p>
255+
</div>
256+
</div>
257+
) : existingKeys?.apiKeys && existingKeys.apiKeys.length > 0 ? (
258+
<div className="space-y-2">
259+
<p className="text-slate-600 dark:text-slate-400 text-center md:text-left">
260+
You have {existingKeys.apiKeys.length} API key
261+
{existingKeys.apiKeys.length > 1 ? "s" : ""} configured
262+
and ready to use. You can manage{" "}
263+
{existingKeys.apiKeys.length > 1 ? "them" : "it"} in the
264+
dashboard.
265+
</p>
266+
{defaultProject && (
267+
<p className="text-xs text-slate-500 dark:text-slate-500">
268+
Keys are counted from your default project:{" "}
269+
<code className="bg-slate-100 dark:bg-slate-800 p-1 rounded-sm border border-slate-200 dark:border-slate-600 text-current">
270+
{defaultProject.name}
271+
</code>
272+
</p>
273+
)}
274+
</div>
275+
) : (
276+
<p className="text-slate-600 dark:text-slate-400">
277+
Setting up your first API key to get started...
278+
</p>
279+
)}
60280
</div>
61281
</div>
282+
</div>
283+
)}
62284

63-
<div className="rounded-md bg-muted p-4">
64-
<p className="text-sm">
65-
In this onboarding process, we'll help you:
66-
</p>
67-
<ul className="mt-2 list-inside list-disc text-sm">
68-
<li>Create your first API key to access the LLM Gateway</li>
69-
<li>
70-
Choose between buying credits or bringing your own API keys
71-
</li>
72-
<li>Set up your preferred payment method or provider keys</li>
73-
<li>Get you ready to start making requests</li>
74-
</ul>
285+
{/* Skip Setup Option for Existing Users */}
286+
{existingKeys?.apiKeys &&
287+
existingKeys.apiKeys.length > 1 &&
288+
!apiKey && (
289+
<div className="mt-6 text-center">
290+
<Button
291+
variant="outline"
292+
onClick={() => router.push("/dashboard")}
293+
>
294+
Skip setup, go to dashboard
295+
</Button>
75296
</div>
76-
</CardContent>
77-
</Card>
297+
)}
78298
</div>
79299
</Step>
80300
);

0 commit comments

Comments
 (0)