botschat 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/migrations/0009_google_auth.sql +10 -0
- package/migrations/0010_pairing_token_security.sql +10 -0
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +13 -7
- package/packages/api/src/env.ts +3 -0
- package/packages/api/src/index.ts +13 -5
- package/packages/api/src/routes/auth.ts +100 -0
- package/packages/api/src/routes/pairing.ts +12 -5
- package/packages/api/src/routes/setup.ts +199 -0
- package/packages/api/src/utils/firebase.ts +179 -0
- package/packages/api/src/utils/resolve-url.ts +79 -0
- package/packages/plugin/dist/src/channel.d.ts +1 -1
- package/packages/plugin/dist/src/channel.js +5 -3
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/package.json +5 -1
- package/packages/web/dist/assets/{index-C-wI8eHy.css → index-DuGeoFJT.css} +1 -1
- package/packages/web/dist/assets/index-DyzTR_Y4.js +847 -0
- package/packages/web/dist/botschat-logo.png +0 -0
- package/packages/web/dist/index.html +3 -3
- package/packages/web/index.html +1 -1
- package/packages/web/package.json +1 -0
- package/packages/web/src/App.tsx +39 -0
- package/packages/web/src/api.ts +12 -0
- package/packages/web/src/components/LoginPage.tsx +131 -13
- package/packages/web/src/components/OnboardingPage.tsx +383 -0
- package/packages/web/src/firebase.ts +93 -0
- package/packages/web/vite.config.ts +1 -1
- package/wrangler.toml +2 -0
- package/packages/web/dist/assets/index-CbPEKHLG.js +0 -93
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { pairingApi, setupApi, type PairingToken } from "../api";
|
|
3
|
+
import { useAppState } from "../store";
|
|
4
|
+
import { dlog } from "../debug-log";
|
|
5
|
+
|
|
6
|
+
/** Clipboard copy button with feedback */
|
|
7
|
+
function CopyButton({ text }: { text: string }) {
|
|
8
|
+
const [copied, setCopied] = useState(false);
|
|
9
|
+
|
|
10
|
+
const handleCopy = async () => {
|
|
11
|
+
try {
|
|
12
|
+
await navigator.clipboard.writeText(text);
|
|
13
|
+
setCopied(true);
|
|
14
|
+
setTimeout(() => setCopied(false), 2000);
|
|
15
|
+
} catch {
|
|
16
|
+
// Fallback for insecure context
|
|
17
|
+
const ta = document.createElement("textarea");
|
|
18
|
+
ta.value = text;
|
|
19
|
+
document.body.appendChild(ta);
|
|
20
|
+
ta.select();
|
|
21
|
+
document.execCommand("copy");
|
|
22
|
+
document.body.removeChild(ta);
|
|
23
|
+
setCopied(true);
|
|
24
|
+
setTimeout(() => setCopied(false), 2000);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
onClick={handleCopy}
|
|
31
|
+
className="shrink-0 px-2.5 py-1 text-tiny font-medium rounded-sm transition-colors"
|
|
32
|
+
style={{
|
|
33
|
+
background: copied ? "var(--accent-green)" : "var(--bg-hover)",
|
|
34
|
+
color: copied ? "#fff" : "var(--text-secondary)",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{copied ? "Copied!" : "Copy"}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Code block with copy button */
|
|
43
|
+
function CodeBlock({ code, multiline }: { code: string; multiline?: boolean }) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className="flex items-start gap-2 rounded-md px-3 py-2.5"
|
|
47
|
+
style={{ background: "var(--code-bg)", border: "1px solid var(--border)" }}
|
|
48
|
+
>
|
|
49
|
+
<pre
|
|
50
|
+
className="flex-1 text-caption font-mono overflow-x-auto whitespace-pre-wrap break-all"
|
|
51
|
+
style={{ color: "var(--text-primary)" }}
|
|
52
|
+
>
|
|
53
|
+
{code}
|
|
54
|
+
</pre>
|
|
55
|
+
<CopyButton text={code} />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Pulsing dot for "waiting" state */
|
|
61
|
+
function PulsingDot({ color }: { color: string }) {
|
|
62
|
+
return (
|
|
63
|
+
<span className="relative flex h-3 w-3">
|
|
64
|
+
<span
|
|
65
|
+
className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
|
|
66
|
+
style={{ backgroundColor: color }}
|
|
67
|
+
/>
|
|
68
|
+
<span
|
|
69
|
+
className="relative inline-flex rounded-full h-3 w-3"
|
|
70
|
+
style={{ backgroundColor: color }}
|
|
71
|
+
/>
|
|
72
|
+
</span>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function OnboardingPage({ onSkip }: { onSkip: () => void }) {
|
|
77
|
+
const state = useAppState();
|
|
78
|
+
const [pairingToken, setPairingToken] = useState<string | null>(null);
|
|
79
|
+
const [loadingToken, setLoadingToken] = useState(true);
|
|
80
|
+
|
|
81
|
+
// Cloud URL — resolved by backend (smart priority), editable by user
|
|
82
|
+
const [cloudUrl, setCloudUrl] = useState<string>(
|
|
83
|
+
typeof window !== "undefined" ? window.location.origin : "https://console.botschat.app",
|
|
84
|
+
);
|
|
85
|
+
const [cloudUrlLoopback, setCloudUrlLoopback] = useState(false);
|
|
86
|
+
const [cloudUrlHint, setCloudUrlHint] = useState<string | undefined>();
|
|
87
|
+
const [editingUrl, setEditingUrl] = useState(false);
|
|
88
|
+
|
|
89
|
+
// Fetch recommended cloudUrl from backend
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
let cancelled = false;
|
|
92
|
+
|
|
93
|
+
async function fetchCloudUrl() {
|
|
94
|
+
try {
|
|
95
|
+
const data = await setupApi.cloudUrl();
|
|
96
|
+
if (cancelled) return;
|
|
97
|
+
setCloudUrl(data.cloudUrl);
|
|
98
|
+
setCloudUrlLoopback(data.isLoopback);
|
|
99
|
+
setCloudUrlHint(data.hint);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
dlog.warn("Onboarding", `Failed to fetch cloudUrl, using origin: ${err}`);
|
|
102
|
+
// Fallback: detect loopback from window.location
|
|
103
|
+
const host = window.location.hostname;
|
|
104
|
+
const loopback = host === "localhost" || host.startsWith("127.");
|
|
105
|
+
setCloudUrlLoopback(loopback);
|
|
106
|
+
if (loopback) {
|
|
107
|
+
setCloudUrlHint(
|
|
108
|
+
"This URL (localhost) only works on this machine. " +
|
|
109
|
+
"If your OpenClaw is on a different host, replace with its LAN IP.",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fetchCloudUrl();
|
|
116
|
+
return () => { cancelled = true; };
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
// Load or create pairing token
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
let cancelled = false;
|
|
122
|
+
|
|
123
|
+
async function ensurePairingToken() {
|
|
124
|
+
setLoadingToken(true);
|
|
125
|
+
try {
|
|
126
|
+
// Check existing tokens
|
|
127
|
+
const { tokens } = await pairingApi.list();
|
|
128
|
+
if (cancelled) return;
|
|
129
|
+
|
|
130
|
+
if (tokens.length > 0) {
|
|
131
|
+
dlog.info("Onboarding", `Found ${tokens.length} existing pairing tokens`);
|
|
132
|
+
const { token } = await pairingApi.create("Onboarding setup");
|
|
133
|
+
if (!cancelled) setPairingToken(token);
|
|
134
|
+
} else {
|
|
135
|
+
dlog.info("Onboarding", "No pairing tokens found, creating one");
|
|
136
|
+
const { token } = await pairingApi.create("Default");
|
|
137
|
+
if (!cancelled) setPairingToken(token);
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
dlog.error("Onboarding", `Failed to get pairing token: ${err}`);
|
|
141
|
+
} finally {
|
|
142
|
+
if (!cancelled) setLoadingToken(false);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ensurePairingToken();
|
|
147
|
+
return () => { cancelled = true; };
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
const setupCommand = pairingToken
|
|
151
|
+
? `openclaw plugins install @botschat/openclaw-plugin && \\
|
|
152
|
+
openclaw config set channels.botschat.cloudUrl ${cloudUrl} && \\
|
|
153
|
+
openclaw config set channels.botschat.pairingToken ${pairingToken} && \\
|
|
154
|
+
openclaw config set channels.botschat.enabled true && \\
|
|
155
|
+
openclaw gateway restart`
|
|
156
|
+
: "Loading...";
|
|
157
|
+
|
|
158
|
+
const isConnected = state.openclawConnected;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
className="min-h-screen flex items-center justify-center p-4"
|
|
163
|
+
style={{ background: "var(--bg-secondary)" }}
|
|
164
|
+
>
|
|
165
|
+
<div className="w-full max-w-xl">
|
|
166
|
+
{/* Header */}
|
|
167
|
+
<div className="text-center mb-8">
|
|
168
|
+
<div
|
|
169
|
+
className="inline-flex items-center justify-center w-16 h-16 rounded-xl text-white text-2xl font-bold mb-4"
|
|
170
|
+
style={{ background: "#1264A3" }}
|
|
171
|
+
>
|
|
172
|
+
BC
|
|
173
|
+
</div>
|
|
174
|
+
<h1 className="text-3xl font-bold" style={{ color: "var(--text-primary)" }}>
|
|
175
|
+
Welcome to BotsChat!
|
|
176
|
+
</h1>
|
|
177
|
+
<p className="mt-2" style={{ color: "var(--text-secondary)" }}>
|
|
178
|
+
Connect your OpenClaw agent to start chatting.
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Main card */}
|
|
183
|
+
<div
|
|
184
|
+
className="rounded-md p-8"
|
|
185
|
+
style={{
|
|
186
|
+
background: "var(--bg-surface)",
|
|
187
|
+
boxShadow: "var(--shadow-lg)",
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{isConnected ? (
|
|
191
|
+
/* Success state */
|
|
192
|
+
<div className="text-center py-6">
|
|
193
|
+
<div
|
|
194
|
+
className="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4"
|
|
195
|
+
style={{ background: "rgba(43, 172, 118, 0.15)" }}
|
|
196
|
+
>
|
|
197
|
+
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="var(--accent-green)" strokeWidth={2.5}>
|
|
198
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
199
|
+
</svg>
|
|
200
|
+
</div>
|
|
201
|
+
<h2 className="text-h1 font-bold mb-2" style={{ color: "var(--text-primary)" }}>
|
|
202
|
+
OpenClaw Connected!
|
|
203
|
+
</h2>
|
|
204
|
+
<p className="text-body mb-6" style={{ color: "var(--text-secondary)" }}>
|
|
205
|
+
Your agent is ready. Start chatting now.
|
|
206
|
+
</p>
|
|
207
|
+
<button
|
|
208
|
+
onClick={onSkip}
|
|
209
|
+
className="px-6 py-2.5 font-bold text-body text-white rounded-sm transition-colors hover:brightness-110"
|
|
210
|
+
style={{ background: "var(--bg-active)" }}
|
|
211
|
+
>
|
|
212
|
+
Start Chatting
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
/* Setup steps */
|
|
217
|
+
<>
|
|
218
|
+
{/* Connection status */}
|
|
219
|
+
<div
|
|
220
|
+
className="flex items-center gap-3 rounded-md px-4 py-3 mb-6"
|
|
221
|
+
style={{
|
|
222
|
+
background: "rgba(232, 162, 48, 0.1)",
|
|
223
|
+
border: "1px solid rgba(232, 162, 48, 0.3)",
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<PulsingDot color="var(--accent-yellow)" />
|
|
227
|
+
<span className="text-caption font-medium" style={{ color: "var(--accent-yellow)" }}>
|
|
228
|
+
Waiting for OpenClaw connection...
|
|
229
|
+
</span>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Step 1 */}
|
|
233
|
+
<div className="mb-6">
|
|
234
|
+
<div className="flex items-center gap-2 mb-2">
|
|
235
|
+
<span
|
|
236
|
+
className="inline-flex items-center justify-center w-6 h-6 rounded-full text-tiny font-bold text-white"
|
|
237
|
+
style={{ background: "var(--bg-active)" }}
|
|
238
|
+
>
|
|
239
|
+
1
|
|
240
|
+
</span>
|
|
241
|
+
<h3 className="text-body font-bold" style={{ color: "var(--text-primary)" }}>
|
|
242
|
+
Run this command on your OpenClaw machine
|
|
243
|
+
</h3>
|
|
244
|
+
</div>
|
|
245
|
+
<p className="text-caption mb-3 ml-8" style={{ color: "var(--text-secondary)" }}>
|
|
246
|
+
This installs the BotsChat plugin, configures the connection, and restarts the gateway.
|
|
247
|
+
</p>
|
|
248
|
+
{/* Loopback URL warning */}
|
|
249
|
+
{cloudUrlLoopback && (
|
|
250
|
+
<div
|
|
251
|
+
className="flex items-start gap-2 rounded-md px-3 py-2.5 mb-3 ml-8 text-caption"
|
|
252
|
+
style={{
|
|
253
|
+
background: "rgba(232, 162, 48, 0.1)",
|
|
254
|
+
border: "1px solid rgba(232, 162, 48, 0.25)",
|
|
255
|
+
color: "var(--accent-yellow)",
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<svg className="w-4 h-4 mt-0.5 shrink-0" viewBox="0 0 20 20" fill="currentColor">
|
|
259
|
+
<path fillRule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.168 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 6a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 6zm0 9a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
|
|
260
|
+
</svg>
|
|
261
|
+
<span>
|
|
262
|
+
{cloudUrlHint || "localhost URL may not be reachable from other machines."}
|
|
263
|
+
{" "}
|
|
264
|
+
<button
|
|
265
|
+
onClick={() => setEditingUrl(true)}
|
|
266
|
+
className="underline font-medium hover:brightness-110"
|
|
267
|
+
style={{ color: "var(--text-link)" }}
|
|
268
|
+
>
|
|
269
|
+
Change URL
|
|
270
|
+
</button>
|
|
271
|
+
</span>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
{/* Editable URL inline */}
|
|
275
|
+
{editingUrl && (
|
|
276
|
+
<div className="flex items-center gap-2 mb-3 ml-8">
|
|
277
|
+
<label className="text-caption font-bold shrink-0" style={{ color: "var(--text-secondary)" }}>
|
|
278
|
+
Cloud URL:
|
|
279
|
+
</label>
|
|
280
|
+
<input
|
|
281
|
+
type="text"
|
|
282
|
+
value={cloudUrl}
|
|
283
|
+
onChange={(e) => {
|
|
284
|
+
setCloudUrl(e.target.value.replace(/\/+$/, ""));
|
|
285
|
+
setCloudUrlLoopback(false);
|
|
286
|
+
}}
|
|
287
|
+
className="flex-1 px-2.5 py-1.5 rounded-sm text-caption font-mono"
|
|
288
|
+
style={{
|
|
289
|
+
background: "var(--code-bg)",
|
|
290
|
+
border: "1px solid var(--border)",
|
|
291
|
+
color: "var(--text-primary)",
|
|
292
|
+
outline: "none",
|
|
293
|
+
}}
|
|
294
|
+
placeholder="http://192.168.x.x:8787"
|
|
295
|
+
autoFocus
|
|
296
|
+
/>
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => setEditingUrl(false)}
|
|
299
|
+
className="px-2.5 py-1 text-tiny font-medium rounded-sm"
|
|
300
|
+
style={{ background: "var(--bg-active)", color: "#fff" }}
|
|
301
|
+
>
|
|
302
|
+
Done
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
<div className="ml-8">
|
|
307
|
+
{loadingToken ? (
|
|
308
|
+
<div
|
|
309
|
+
className="rounded-md px-3 py-2.5 animate-pulse"
|
|
310
|
+
style={{ background: "var(--code-bg)", height: "80px" }}
|
|
311
|
+
/>
|
|
312
|
+
) : (
|
|
313
|
+
<CodeBlock code={setupCommand} multiline />
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{/* Step 2 */}
|
|
319
|
+
<div className="mb-6">
|
|
320
|
+
<div className="flex items-center gap-2 mb-2">
|
|
321
|
+
<span
|
|
322
|
+
className="inline-flex items-center justify-center w-6 h-6 rounded-full text-tiny font-bold text-white"
|
|
323
|
+
style={{ background: "var(--bg-active)" }}
|
|
324
|
+
>
|
|
325
|
+
2
|
|
326
|
+
</span>
|
|
327
|
+
<h3 className="text-body font-bold" style={{ color: "var(--text-primary)" }}>
|
|
328
|
+
Verify connection
|
|
329
|
+
</h3>
|
|
330
|
+
</div>
|
|
331
|
+
<p className="text-caption ml-8" style={{ color: "var(--text-secondary)" }}>
|
|
332
|
+
Check the gateway logs — you should see "Authenticated with BotsChat cloud":
|
|
333
|
+
</p>
|
|
334
|
+
<div className="ml-8 mt-2">
|
|
335
|
+
<CodeBlock code="openclaw gateway logs" />
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* PAT info (collapsible) */}
|
|
340
|
+
<details className="mb-4">
|
|
341
|
+
<summary
|
|
342
|
+
className="text-caption font-medium cursor-pointer select-none"
|
|
343
|
+
style={{ color: "var(--text-link)" }}
|
|
344
|
+
>
|
|
345
|
+
Or configure manually
|
|
346
|
+
</summary>
|
|
347
|
+
<div className="mt-3 space-y-3 ml-1">
|
|
348
|
+
<div>
|
|
349
|
+
<label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
|
|
350
|
+
Your Pairing Token
|
|
351
|
+
</label>
|
|
352
|
+
{pairingToken ? (
|
|
353
|
+
<CodeBlock code={pairingToken} />
|
|
354
|
+
) : (
|
|
355
|
+
<span className="text-caption" style={{ color: "var(--text-muted)" }}>Loading...</span>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
<div>
|
|
359
|
+
<label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
|
|
360
|
+
Cloud URL
|
|
361
|
+
</label>
|
|
362
|
+
<CodeBlock code={cloudUrl} />
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</details>
|
|
366
|
+
|
|
367
|
+
{/* Skip */}
|
|
368
|
+
<div className="text-center pt-2">
|
|
369
|
+
<button
|
|
370
|
+
onClick={onSkip}
|
|
371
|
+
className="text-caption hover:underline"
|
|
372
|
+
style={{ color: "var(--text-muted)" }}
|
|
373
|
+
>
|
|
374
|
+
Skip for now
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
</>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase client initialization for OAuth Sign-In (Google, GitHub).
|
|
3
|
+
*
|
|
4
|
+
* The Firebase config is loaded from Vite environment variables.
|
|
5
|
+
* Create a `.env` file in packages/web/ with your Firebase config:
|
|
6
|
+
*
|
|
7
|
+
* VITE_FIREBASE_API_KEY=AIzaSy...
|
|
8
|
+
* VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
|
9
|
+
* VITE_FIREBASE_PROJECT_ID=your-project-id
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { initializeApp, type FirebaseApp } from "firebase/app";
|
|
13
|
+
import {
|
|
14
|
+
getAuth,
|
|
15
|
+
GoogleAuthProvider,
|
|
16
|
+
GithubAuthProvider,
|
|
17
|
+
signInWithPopup,
|
|
18
|
+
type Auth,
|
|
19
|
+
} from "firebase/auth";
|
|
20
|
+
|
|
21
|
+
const firebaseConfig = {
|
|
22
|
+
apiKey: import.meta.env.VITE_FIREBASE_API_KEY as string,
|
|
23
|
+
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN as string,
|
|
24
|
+
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID as string,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let app: FirebaseApp | null = null;
|
|
28
|
+
let auth: Auth | null = null;
|
|
29
|
+
|
|
30
|
+
/** Check if Firebase is configured (all required env vars present). */
|
|
31
|
+
export function isFirebaseConfigured(): boolean {
|
|
32
|
+
return !!(firebaseConfig.apiKey && firebaseConfig.authDomain && firebaseConfig.projectId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getFirebaseAuth(): Auth {
|
|
36
|
+
if (!auth) {
|
|
37
|
+
if (!isFirebaseConfigured()) {
|
|
38
|
+
throw new Error("Firebase is not configured. Set VITE_FIREBASE_* env vars.");
|
|
39
|
+
}
|
|
40
|
+
app = initializeApp(firebaseConfig);
|
|
41
|
+
auth = getAuth(app);
|
|
42
|
+
}
|
|
43
|
+
return auth;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type FirebaseSignInResult = {
|
|
47
|
+
idToken: string;
|
|
48
|
+
email: string;
|
|
49
|
+
displayName: string | null;
|
|
50
|
+
photoURL: string | null;
|
|
51
|
+
provider: "google" | "github";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sign in with Google via popup and return the Firebase ID token.
|
|
56
|
+
*/
|
|
57
|
+
export async function signInWithGoogle(): Promise<FirebaseSignInResult> {
|
|
58
|
+
const firebaseAuth = getFirebaseAuth();
|
|
59
|
+
const provider = new GoogleAuthProvider();
|
|
60
|
+
provider.addScope("email");
|
|
61
|
+
provider.addScope("profile");
|
|
62
|
+
|
|
63
|
+
const result = await signInWithPopup(firebaseAuth, provider);
|
|
64
|
+
const idToken = await result.user.getIdToken();
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
idToken,
|
|
68
|
+
email: result.user.email ?? "",
|
|
69
|
+
displayName: result.user.displayName,
|
|
70
|
+
photoURL: result.user.photoURL,
|
|
71
|
+
provider: "google",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sign in with GitHub via popup and return the Firebase ID token.
|
|
77
|
+
*/
|
|
78
|
+
export async function signInWithGitHub(): Promise<FirebaseSignInResult> {
|
|
79
|
+
const firebaseAuth = getFirebaseAuth();
|
|
80
|
+
const provider = new GithubAuthProvider();
|
|
81
|
+
provider.addScope("user:email");
|
|
82
|
+
|
|
83
|
+
const result = await signInWithPopup(firebaseAuth, provider);
|
|
84
|
+
const idToken = await result.user.getIdToken();
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
idToken,
|
|
88
|
+
email: result.user.email ?? "",
|
|
89
|
+
displayName: result.user.displayName,
|
|
90
|
+
photoURL: result.user.photoURL,
|
|
91
|
+
provider: "github",
|
|
92
|
+
};
|
|
93
|
+
}
|
package/wrangler.toml
CHANGED
|
@@ -34,6 +34,8 @@ new_sqlite_classes = ["ConnectionDO"]
|
|
|
34
34
|
# --- Environment Variables ---
|
|
35
35
|
[vars]
|
|
36
36
|
ENVIRONMENT = "development"
|
|
37
|
+
# Firebase project ID for Google/GitHub Sign-In token verification.
|
|
38
|
+
FIREBASE_PROJECT_ID = "botschat-130ff"
|
|
37
39
|
|
|
38
40
|
# --- Dev settings ---
|
|
39
41
|
[dev]
|