@chesapeake/stack 0.1.0

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +190 -0
  3. package/README.md +440 -0
  4. package/components/ui/avatar.tsx +87 -0
  5. package/components/ui/badge.tsx +49 -0
  6. package/components/ui/badgeVariants.ts +26 -0
  7. package/components/ui/button-group.tsx +67 -0
  8. package/components/ui/button.tsx +70 -0
  9. package/components/ui/buttonGroupVariants.ts +23 -0
  10. package/components/ui/buttonVariants.ts +38 -0
  11. package/components/ui/calendar.tsx +220 -0
  12. package/components/ui/card.tsx +162 -0
  13. package/components/ui/dialog.tsx +290 -0
  14. package/components/ui/drawer.tsx +227 -0
  15. package/components/ui/dropdown-menu.tsx +260 -0
  16. package/components/ui/index.ts +86 -0
  17. package/components/ui/input.tsx +59 -0
  18. package/components/ui/label.tsx +47 -0
  19. package/components/ui/location-input.md +194 -0
  20. package/components/ui/location-input.tsx +384 -0
  21. package/components/ui/popover.tsx +110 -0
  22. package/components/ui/select.tsx +306 -0
  23. package/components/ui/separator.tsx +50 -0
  24. package/components/ui/sonner-api.ts +8 -0
  25. package/components/ui/sonner.tsx +69 -0
  26. package/components/ui/textarea.tsx +53 -0
  27. package/components/ui/tooltip.tsx +125 -0
  28. package/components/ux/ActivityAnimation.tsx +135 -0
  29. package/components/ux/EmptyState.tsx +22 -0
  30. package/components/ux/FeedbackDrawer.tsx +141 -0
  31. package/components/ux/FireAnimation.tsx +189 -0
  32. package/components/ux/FriendList.tsx +111 -0
  33. package/components/ux/FriendRequestList.tsx +104 -0
  34. package/components/ux/GifSearch.tsx +264 -0
  35. package/components/ux/ImageUpload.tsx +829 -0
  36. package/components/ux/SelectedUsersList.tsx +91 -0
  37. package/components/ux/StandardLayout/PageLayout.tsx +107 -0
  38. package/components/ux/StandardLayout/ProfilePage.tsx +452 -0
  39. package/components/ux/StandardLayout/StandardLayout.tsx +345 -0
  40. package/components/ux/StandardLayout/index.ts +22 -0
  41. package/components/ux/TutorialCarousel.tsx +128 -0
  42. package/components/ux/UserSearch.tsx +324 -0
  43. package/components/ux/UserSelector.tsx +116 -0
  44. package/components/ux/auth/AuthSelector.tsx +154 -0
  45. package/components/ux/auth/MultiAuthProvider.tsx +196 -0
  46. package/components/ux/auth/README.md +317 -0
  47. package/components/ux/auth/context.ts +46 -0
  48. package/components/ux/auth/github/AuthCallback.tsx +177 -0
  49. package/components/ux/auth/github/AuthProvider.tsx +111 -0
  50. package/components/ux/auth/github/README.md +208 -0
  51. package/components/ux/auth/github/client.ts +127 -0
  52. package/components/ux/auth/github/config.ts +68 -0
  53. package/components/ux/auth/github/context.ts +34 -0
  54. package/components/ux/auth/github/default-client.ts +74 -0
  55. package/components/ux/auth/github/index.ts +43 -0
  56. package/components/ux/auth/github/types.ts +44 -0
  57. package/components/ux/auth/github/useGitHubAuth.ts +16 -0
  58. package/components/ux/auth/google/AuthCallback.tsx +170 -0
  59. package/components/ux/auth/google/AuthProvider.tsx +108 -0
  60. package/components/ux/auth/google/README.md +450 -0
  61. package/components/ux/auth/google/client.ts +109 -0
  62. package/components/ux/auth/google/config.ts +65 -0
  63. package/components/ux/auth/google/context.ts +34 -0
  64. package/components/ux/auth/google/default-client.ts +69 -0
  65. package/components/ux/auth/google/index.ts +43 -0
  66. package/components/ux/auth/google/types.ts +42 -0
  67. package/components/ux/auth/google/useGoogleAuth.ts +16 -0
  68. package/components/ux/auth/index.ts +72 -0
  69. package/components/ux/auth/useAuth.ts +16 -0
  70. package/components/ux/index.ts +46 -0
  71. package/components/ux/theme-provider-context.ts +20 -0
  72. package/components/ux/theme-provider.tsx +114 -0
  73. package/components/ux/useTheme.ts +42 -0
  74. package/index.ts +29 -0
  75. package/lib/utils.ts +57 -0
  76. package/package.json +170 -0
  77. package/styles/base.css +38 -0
  78. package/styles/components.css +54 -0
  79. package/styles/index.css +7 -0
  80. package/styles/tokens.css +189 -0
  81. package/tailwind.preset.ts +169 -0
  82. package/types/user.ts +69 -0
@@ -0,0 +1,450 @@
1
+ # Google OAuth Authentication
2
+
3
+ This package provides frontend components and utilities for Google OAuth
4
+ authentication. It supports both **config-over-convention** (full control) and
5
+ **convention-over-configuration** (sensible defaults) approaches.
6
+
7
+ ## Features
8
+
9
+ - 🔐 Secure OAuth flow (client secret never exposed to frontend)
10
+ - ⚙️ Two API styles: configurable and default
11
+ - 🎨 React components for auth provider and callback handling
12
+ - 📦 TypeScript types for type safety
13
+ - 🔄 Automatic user state management with localStorage
14
+
15
+ ## Installation
16
+
17
+ This package is part of `@chesapeake/stack`. Import from:
18
+
19
+ ```typescript
20
+ import { ... } from "@shared/components/ux/auth/google";
21
+ ```
22
+
23
+ ## Quick Start (Convention-over-Configuration)
24
+
25
+ For most use cases, the default client provides sensible defaults:
26
+
27
+ ### 1. Environment Variables
28
+
29
+ Add to your `.env` file:
30
+
31
+ ```env
32
+ VITE_GOOGLE_CLIENT_ID=your_google_client_id_here
33
+ VITE_REDIRECT_URI=http://localhost:5173/auth/callback
34
+ ```
35
+
36
+ ### 2. Setup Auth Provider
37
+
38
+ ```tsx
39
+ import {
40
+ DefaultGoogleAuthProvider,
41
+ useGoogleAuth,
42
+ } from "@shared/components/ux/auth/google";
43
+ import { defaultInitiateGoogleLogin } from "@shared/components/ux/auth/google";
44
+
45
+ function App() {
46
+ return (
47
+ <DefaultGoogleAuthProvider>
48
+ <YourApp />
49
+ </DefaultGoogleAuthProvider>
50
+ );
51
+ }
52
+
53
+ function LoginButton() {
54
+ const { signInWithGoogle } = useGoogleAuth();
55
+
56
+ return <button onClick={signInWithGoogle}>Sign in with Google</button>;
57
+ }
58
+ ```
59
+
60
+ ### 3. Handle OAuth Callback
61
+
62
+ ```tsx
63
+ import { DefaultGoogleAuthCallback } from "@shared/components/ux/auth/google";
64
+ import { useNavigate, useSearchParams } from "react-router-dom";
65
+ import { useGoogleAuth } from "@shared/components/ux/auth/google";
66
+
67
+ function AuthCallbackPage() {
68
+ const navigate = useNavigate();
69
+ const [searchParams] = useSearchParams();
70
+ const { updateUser } = useGoogleAuth();
71
+ const code = searchParams.get("code");
72
+
73
+ if (!code) {
74
+ return <div>No authorization code received</div>;
75
+ }
76
+
77
+ return (
78
+ <DefaultGoogleAuthCallback
79
+ code={code}
80
+ onUserFound={async (profile) => {
81
+ // User exists in your system
82
+ // Update user state
83
+ updateUser(profile);
84
+ navigate("/home");
85
+ }}
86
+ onUserNotFound={async (profile) => {
87
+ // New user - redirect to create account page
88
+ navigate("/create-user");
89
+ }}
90
+ onError={(error) => {
91
+ console.error("Auth error:", error);
92
+ navigate("/login");
93
+ }}
94
+ />
95
+ );
96
+ }
97
+ ```
98
+
99
+ ### 4. Backend Implementation
100
+
101
+ You need to implement a backend endpoint `POST /api/auth/google/callback` that:
102
+
103
+ 1. Receives `{ code: string }` in the request body
104
+ 2. Exchanges the code for tokens using Google's OAuth API (server-side)
105
+ 3. Fetches user info from Google
106
+ 4. Returns `{ user: GoogleOAuthProfile }`
107
+
108
+ See "Backend Implementation" section below for a complete Deno example.
109
+
110
+ ## Advanced Usage (Config-over-Convention)
111
+
112
+ For full control over the OAuth flow:
113
+
114
+ ```tsx
115
+ import {
116
+ GoogleAuthClient,
117
+ GoogleAuthProvider,
118
+ } from "@shared/components/ux/auth/google";
119
+ import type { GoogleAuthConfig } from "@shared/components/ux/auth/google";
120
+
121
+ const config: GoogleAuthConfig = {
122
+ clientId: "your-client-id",
123
+ redirectUri: "https://yourapp.com/auth/callback",
124
+ apiBaseUrl: "https://api.yourapp.com",
125
+ scope: "openid email profile",
126
+ storageKey: "myAppUser",
127
+ };
128
+
129
+ function App() {
130
+ return (
131
+ <GoogleAuthProvider config={config}>
132
+ <YourApp />
133
+ </GoogleAuthProvider>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ## API Reference
139
+
140
+ ### Types
141
+
142
+ #### `GoogleOAuthProfile`
143
+
144
+ User profile returned from Google's userinfo endpoint:
145
+
146
+ ```typescript
147
+ interface GoogleOAuthProfile {
148
+ sub: string; // Google user ID
149
+ name: string;
150
+ email: string;
151
+ picture?: string;
152
+ email_verified?: boolean;
153
+ }
154
+ ```
155
+
156
+ #### `GoogleAuthConfig`
157
+
158
+ Configuration for OAuth client:
159
+
160
+ ```typescript
161
+ interface GoogleAuthConfig {
162
+ clientId: string;
163
+ redirectUri: string;
164
+ apiBaseUrl?: string;
165
+ scope?: string;
166
+ storageKey?: string;
167
+ }
168
+ ```
169
+
170
+ ### Default Client Functions
171
+
172
+ #### `initiateGoogleLogin()`
173
+
174
+ Initiates OAuth flow with default configuration. Reads from
175
+ `VITE_GOOGLE_CLIENT_ID` and `VITE_REDIRECT_URI`.
176
+
177
+ #### `handleGoogleCallback(code: string): Promise<GoogleOAuthProfile>`
178
+
179
+ Handles OAuth callback by calling backend endpoint. Calls
180
+ `POST /api/auth/google/callback` by default.
181
+
182
+ ### Components
183
+
184
+ #### `DefaultGoogleAuthProvider`
185
+
186
+ Auth provider with sensible defaults:
187
+
188
+ - Storage key: `"user"`
189
+ - API endpoint: `/api/auth/google/callback` (auto-detected)
190
+
191
+ #### `GoogleAuthProvider`
192
+
193
+ Configurable auth provider with full control over all parameters.
194
+
195
+ #### `useGoogleAuth()`
196
+
197
+ Hook to access auth context:
198
+
199
+ ```typescript
200
+ const { user, loading, signInWithGoogle, signOut, updateUser } =
201
+ useGoogleAuth();
202
+ ```
203
+
204
+ #### `DefaultGoogleAuthCallback`
205
+
206
+ Callback component with sensible defaults for common flow.
207
+
208
+ #### `GoogleAuthCallback`
209
+
210
+ Configurable callback component for custom flows.
211
+
212
+ ## Backend Implementation
213
+
214
+ ### API Contract
215
+
216
+ **Endpoint:** `POST /api/auth/google/callback`
217
+
218
+ **Request:**
219
+
220
+ ```json
221
+ {
222
+ "code": "authorization_code_from_google"
223
+ }
224
+ ```
225
+
226
+ **Response:**
227
+
228
+ ```json
229
+ {
230
+ "user": {
231
+ "sub": "google_user_id",
232
+ "name": "User Name",
233
+ "email": "user@example.com",
234
+ "picture": "https://...",
235
+ "email_verified": true
236
+ }
237
+ }
238
+ ```
239
+
240
+ ### Minimal Deno Implementation
241
+
242
+ Here's a complete example for Deno:
243
+
244
+ ```typescript
245
+ /// <reference lib="deno.unstable" />
246
+
247
+ interface GoogleTokenResponse {
248
+ access_token: string;
249
+ token_type: string;
250
+ expires_in: number;
251
+ refresh_token?: string;
252
+ id_token?: string;
253
+ }
254
+
255
+ interface GoogleUserInfo {
256
+ sub: string;
257
+ name: string;
258
+ email: string;
259
+ picture?: string;
260
+ email_verified?: boolean;
261
+ }
262
+
263
+ /**
264
+ * Handles Google OAuth callback
265
+ * POST /api/auth/google/callback
266
+ */
267
+ export async function googleAuthHandler(req: Request): Promise<Response> {
268
+ if (req.method !== "POST") {
269
+ return new Response("Method not allowed", { status: 405 });
270
+ }
271
+
272
+ try {
273
+ // Parse request body
274
+ const { code } = await req.json();
275
+ if (!code) {
276
+ return new Response(
277
+ JSON.stringify({ error: "Authorization code is required" }),
278
+ { status: 400, headers: { "Content-Type": "application/json" } },
279
+ );
280
+ }
281
+
282
+ // Get environment variables
283
+ const clientId = Deno.env.get("GOOGLE_CLIENT_ID");
284
+ const clientSecret = Deno.env.get("GOOGLE_CLIENT_SECRET");
285
+ const redirectUri = Deno.env.get("GOOGLE_REDIRECT_URI") ||
286
+ "http://localhost:5173/auth/callback";
287
+
288
+ if (!clientId || !clientSecret) {
289
+ return new Response(
290
+ JSON.stringify({ error: "OAuth configuration missing" }),
291
+ { status: 500, headers: { "Content-Type": "application/json" } },
292
+ );
293
+ }
294
+
295
+ // Exchange authorization code for tokens
296
+ const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
297
+ method: "POST",
298
+ headers: {
299
+ "Content-Type": "application/x-www-form-urlencoded",
300
+ },
301
+ body: new URLSearchParams({
302
+ code,
303
+ client_id: clientId,
304
+ client_secret: clientSecret,
305
+ redirect_uri: redirectUri,
306
+ grant_type: "authorization_code",
307
+ }),
308
+ });
309
+
310
+ if (!tokenResponse.ok) {
311
+ const error = await tokenResponse.json();
312
+ return new Response(
313
+ JSON.stringify({
314
+ error: error.error_description ||
315
+ error.error ||
316
+ "Failed to exchange code for token",
317
+ }),
318
+ { status: 400, headers: { "Content-Type": "application/json" } },
319
+ );
320
+ }
321
+
322
+ const tokens: GoogleTokenResponse = await tokenResponse.json();
323
+
324
+ if (!tokens.access_token) {
325
+ return new Response(
326
+ JSON.stringify({ error: "No access token received" }),
327
+ { status: 400, headers: { "Content-Type": "application/json" } },
328
+ );
329
+ }
330
+
331
+ // Fetch user info from Google
332
+ const userResponse = await fetch(
333
+ "https://www.googleapis.com/oauth2/v3/userinfo",
334
+ {
335
+ headers: {
336
+ Authorization: `Bearer ${tokens.access_token}`,
337
+ },
338
+ },
339
+ );
340
+
341
+ if (!userResponse.ok) {
342
+ const error = await userResponse.json();
343
+ return new Response(
344
+ JSON.stringify({
345
+ error: error.error_description ||
346
+ error.error ||
347
+ "Failed to fetch user info",
348
+ }),
349
+ { status: 400, headers: { "Content-Type": "application/json" } },
350
+ );
351
+ }
352
+
353
+ const userInfo: GoogleUserInfo = await userResponse.json();
354
+
355
+ // Return user profile
356
+ return new Response(JSON.stringify({ user: userInfo }), {
357
+ status: 200,
358
+ headers: { "Content-Type": "application/json" },
359
+ });
360
+ } catch (error) {
361
+ console.error("OAuth error:", error);
362
+ return new Response(
363
+ JSON.stringify({
364
+ error: error instanceof Error ? error.message : "Internal server error",
365
+ }),
366
+ { status: 500, headers: { "Content-Type": "application/json" } },
367
+ );
368
+ }
369
+ }
370
+ ```
371
+
372
+ ### Adding to Your Server
373
+
374
+ In your Deno server routing:
375
+
376
+ ```typescript
377
+ // In your main server file (e.g., serve_deno_deploy.ts)
378
+ import { googleAuthHandler } from "./auth.ts";
379
+
380
+ async function routeRequest(
381
+ req: Request,
382
+ kv: Deno.Kv,
383
+ config: ServerConfig,
384
+ ): Promise<Response> {
385
+ const url = new URL(req.url);
386
+
387
+ // Handle Google OAuth callback
388
+ if (url.pathname === "/api/auth/google/callback") {
389
+ return googleAuthHandler(req);
390
+ }
391
+
392
+ // ... other routes
393
+ }
394
+ ```
395
+
396
+ ### Environment Variables
397
+
398
+ Add to your server environment:
399
+
400
+ ```env
401
+ GOOGLE_CLIENT_ID=your_google_client_id
402
+ GOOGLE_CLIENT_SECRET=your_google_client_secret
403
+ GOOGLE_REDIRECT_URI=http://localhost:5173/auth/callback
404
+ ```
405
+
406
+ ## Security Notes
407
+
408
+ 1. **Never expose client secret in frontend code** - Always exchange tokens
409
+ server-side
410
+ 2. **Use HTTPS in production** - OAuth requires secure connections
411
+ 3. **Validate redirect URIs** - Ensure redirect URIs match your Google OAuth app
412
+ configuration
413
+ 4. **Store tokens securely** - If storing refresh tokens, use secure server-side
414
+ storage
415
+
416
+ ## Google OAuth Setup
417
+
418
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
419
+ 2. Create a new project or select existing
420
+ 3. Enable Google+ API
421
+ 4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client ID"
422
+ 5. Configure:
423
+ - Application type: Web application
424
+ - Authorized redirect URIs: `http://localhost:5173/auth/callback` (dev) and
425
+ your production URL
426
+ 6. Copy Client ID and Client Secret
427
+
428
+ ## Troubleshooting
429
+
430
+ ### "Failed to exchange code for token"
431
+
432
+ - Check that `GOOGLE_CLIENT_SECRET` is set correctly on the server
433
+ - Verify redirect URI matches exactly (including trailing slashes)
434
+ - Ensure the authorization code hasn't expired (codes expire quickly)
435
+
436
+ ### "No authorization code received"
437
+
438
+ - Check that the callback URL is configured correctly in Google Cloud Console
439
+ - Verify the redirect URI in your frontend matches the one in Google Cloud
440
+ Console
441
+
442
+ ### CORS errors
443
+
444
+ - Ensure your backend allows requests from your frontend origin
445
+ - Check that the API endpoint is correctly configured
446
+
447
+ ## Examples
448
+
449
+ A demo that uses this package is available in this repo: run `npm run dev` from
450
+ the stack package root and see the component showcase.
@@ -0,0 +1,109 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { GoogleAuthCallbackResponse, GoogleAuthConfig } from "./config.ts";
5
+ import type { GoogleOAuthProfile } from "./types.ts";
6
+
7
+ /**
8
+ * Configurable Google OAuth client (config-over-convention)
9
+ * Provides full control over all OAuth parameters
10
+ */
11
+ export class GoogleAuthClient {
12
+ private config: GoogleAuthConfig;
13
+
14
+ constructor(config: GoogleAuthConfig) {
15
+ // Validate config
16
+ if (!config.backendInitEndpoint && !config.clientId) {
17
+ throw new Error(
18
+ "Either clientId or backendInitEndpoint must be provided"
19
+ );
20
+ }
21
+ this.config = config;
22
+ }
23
+
24
+ /**
25
+ * Initiates Google OAuth login flow by redirecting to Google or backend endpoint
26
+ */
27
+ initiateLogin(): void {
28
+ // If backend handles OAuth initiation, redirect to backend endpoint
29
+ if (this.config.backendInitEndpoint) {
30
+ globalThis.location.href = this.config.backendInitEndpoint;
31
+ return;
32
+ }
33
+
34
+ // Otherwise, redirect directly to Google
35
+ if (!this.config.clientId) {
36
+ throw new Error(
37
+ "clientId is required when backendInitEndpoint is not provided"
38
+ );
39
+ }
40
+
41
+ const params = new URLSearchParams({
42
+ client_id: this.config.clientId,
43
+ redirect_uri: this.config.redirectUri,
44
+ response_type: "code",
45
+ scope: this.config.scope ?? "openid email profile",
46
+ access_type: "offline",
47
+ prompt: "consent",
48
+ });
49
+
50
+ globalThis.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
51
+ }
52
+
53
+ /**
54
+ * Handles OAuth callback by exchanging code for user profile via backend
55
+ * @param code Authorization code from Google OAuth callback
56
+ * @returns User profile from backend
57
+ */
58
+ async handleCallback(code: string): Promise<GoogleOAuthProfile> {
59
+ const apiBaseUrl = this.config.apiBaseUrl ?? "";
60
+ const endpoint = `${apiBaseUrl}/api/auth/google/callback`;
61
+
62
+ const response = await fetch(endpoint, {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ },
67
+ body: JSON.stringify({ code }),
68
+ });
69
+
70
+ if (!response.ok) {
71
+ const error = (await response.json().catch(() => ({
72
+ error: "Failed to exchange code for token",
73
+ }))) as {
74
+ error_description?: string;
75
+ error?: string;
76
+ };
77
+ throw new Error(
78
+ error.error_description ??
79
+ error.error ??
80
+ "Failed to exchange code for token"
81
+ );
82
+ }
83
+
84
+ const data = (await response.json()) as GoogleAuthCallbackResponse;
85
+ return data.user;
86
+ }
87
+
88
+ /**
89
+ * Updates the configuration
90
+ */
91
+ updateConfig(config: Partial<GoogleAuthConfig>): void {
92
+ this.config = { ...this.config, ...config };
93
+ }
94
+
95
+ /**
96
+ * Gets the current configuration
97
+ */
98
+ getConfig(): GoogleAuthConfig {
99
+ return { ...this.config };
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Standalone function to initiate Google OAuth login with configuration
105
+ */
106
+ export function initiateGoogleLogin(config: GoogleAuthConfig): void {
107
+ const client = new GoogleAuthClient(config);
108
+ client.initiateLogin();
109
+ }
@@ -0,0 +1,65 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { GoogleOAuthProfile } from "./types.ts";
5
+
6
+ /**
7
+ * Configuration for Google OAuth client
8
+ */
9
+ export interface GoogleAuthConfig {
10
+ /** Google OAuth client ID (required if backendInitEndpoint is not provided) */
11
+ clientId?: string;
12
+ /** Redirect URI after OAuth authorization */
13
+ redirectUri: string;
14
+ /** Backend endpoint that initiates OAuth flow (e.g., "/api/auth/google") */
15
+ backendInitEndpoint?: string;
16
+ /** Backend API base URL (e.g., "https://api.example.com" or "/api") */
17
+ apiBaseUrl?: string;
18
+ /** OAuth scopes (default: "openid email profile") */
19
+ scope?: string;
20
+ /** Storage key for user data in localStorage (default: "user") */
21
+ storageKey?: string;
22
+ }
23
+
24
+ /**
25
+ * Default OAuth scopes
26
+ */
27
+ export const DEFAULT_SCOPE = "openid email profile";
28
+
29
+ /**
30
+ * Creates a GoogleAuthConfig from environment variables
31
+ * Note: This function expects Vite environment variables (import.meta.env)
32
+ */
33
+ export function createGoogleAuthConfig(): GoogleAuthConfig {
34
+ // Type assertion for Vite's import.meta.env
35
+ const env = (import.meta as { env?: Record<string, string | undefined> })
36
+ .env as {
37
+ VITE_GOOGLE_CLIENT_ID?: string;
38
+ VITE_REDIRECT_URI?: string;
39
+ VITE_API_BASE_URL?: string;
40
+ };
41
+
42
+ const clientId = env.VITE_GOOGLE_CLIENT_ID;
43
+ if (!clientId) {
44
+ throw new Error("VITE_GOOGLE_CLIENT_ID environment variable is required");
45
+ }
46
+
47
+ const redirectUri =
48
+ env.VITE_REDIRECT_URI ??
49
+ `${globalThis.location.origin}/api/auth/google/callback`;
50
+
51
+ return {
52
+ clientId,
53
+ redirectUri,
54
+ apiBaseUrl: env.VITE_API_BASE_URL,
55
+ scope: DEFAULT_SCOPE,
56
+ storageKey: "user",
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Backend API response from OAuth callback endpoint
62
+ */
63
+ export interface GoogleAuthCallbackResponse {
64
+ user: GoogleOAuthProfile;
65
+ }
@@ -0,0 +1,34 @@
1
+ // Copyright 2026 Chesapeake Computing
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import React, { createContext } from "react";
5
+ import type { GoogleAuthConfig } from "./config.ts";
6
+
7
+ /**
8
+ * Generic user type that applications can extend
9
+ */
10
+ export type AuthUser = Record<string, unknown>;
11
+
12
+ export interface GoogleAuthContextType {
13
+ user: AuthUser | null;
14
+ loading: boolean;
15
+ signInWithGoogle: () => void;
16
+ signOut: () => void;
17
+ updateUser: (user: AuthUser | null) => void;
18
+ }
19
+
20
+ export const GoogleAuthContext = createContext<GoogleAuthContextType | null>(
21
+ null
22
+ );
23
+
24
+ export interface GoogleAuthProviderProps {
25
+ children: React.ReactNode;
26
+ /** Backend API base URL (default: auto-detect from window.location) */
27
+ apiBaseUrl?: string;
28
+ /** Storage key for user data in localStorage (default: "user") */
29
+ storageKey?: string;
30
+ /** Callback when user is updated */
31
+ onUserUpdate?: (user: AuthUser | null) => void;
32
+ /** Custom Google Auth configuration (if not provided, uses default-client) */
33
+ config?: GoogleAuthConfig;
34
+ }