@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +190 -0
- package/README.md +440 -0
- package/components/ui/avatar.tsx +87 -0
- package/components/ui/badge.tsx +49 -0
- package/components/ui/badgeVariants.ts +26 -0
- package/components/ui/button-group.tsx +67 -0
- package/components/ui/button.tsx +70 -0
- package/components/ui/buttonGroupVariants.ts +23 -0
- package/components/ui/buttonVariants.ts +38 -0
- package/components/ui/calendar.tsx +220 -0
- package/components/ui/card.tsx +162 -0
- package/components/ui/dialog.tsx +290 -0
- package/components/ui/drawer.tsx +227 -0
- package/components/ui/dropdown-menu.tsx +260 -0
- package/components/ui/index.ts +86 -0
- package/components/ui/input.tsx +59 -0
- package/components/ui/label.tsx +47 -0
- package/components/ui/location-input.md +194 -0
- package/components/ui/location-input.tsx +384 -0
- package/components/ui/popover.tsx +110 -0
- package/components/ui/select.tsx +306 -0
- package/components/ui/separator.tsx +50 -0
- package/components/ui/sonner-api.ts +8 -0
- package/components/ui/sonner.tsx +69 -0
- package/components/ui/textarea.tsx +53 -0
- package/components/ui/tooltip.tsx +125 -0
- package/components/ux/ActivityAnimation.tsx +135 -0
- package/components/ux/EmptyState.tsx +22 -0
- package/components/ux/FeedbackDrawer.tsx +141 -0
- package/components/ux/FireAnimation.tsx +189 -0
- package/components/ux/FriendList.tsx +111 -0
- package/components/ux/FriendRequestList.tsx +104 -0
- package/components/ux/GifSearch.tsx +264 -0
- package/components/ux/ImageUpload.tsx +829 -0
- package/components/ux/SelectedUsersList.tsx +91 -0
- package/components/ux/StandardLayout/PageLayout.tsx +107 -0
- package/components/ux/StandardLayout/ProfilePage.tsx +452 -0
- package/components/ux/StandardLayout/StandardLayout.tsx +345 -0
- package/components/ux/StandardLayout/index.ts +22 -0
- package/components/ux/TutorialCarousel.tsx +128 -0
- package/components/ux/UserSearch.tsx +324 -0
- package/components/ux/UserSelector.tsx +116 -0
- package/components/ux/auth/AuthSelector.tsx +154 -0
- package/components/ux/auth/MultiAuthProvider.tsx +196 -0
- package/components/ux/auth/README.md +317 -0
- package/components/ux/auth/context.ts +46 -0
- package/components/ux/auth/github/AuthCallback.tsx +177 -0
- package/components/ux/auth/github/AuthProvider.tsx +111 -0
- package/components/ux/auth/github/README.md +208 -0
- package/components/ux/auth/github/client.ts +127 -0
- package/components/ux/auth/github/config.ts +68 -0
- package/components/ux/auth/github/context.ts +34 -0
- package/components/ux/auth/github/default-client.ts +74 -0
- package/components/ux/auth/github/index.ts +43 -0
- package/components/ux/auth/github/types.ts +44 -0
- package/components/ux/auth/github/useGitHubAuth.ts +16 -0
- package/components/ux/auth/google/AuthCallback.tsx +170 -0
- package/components/ux/auth/google/AuthProvider.tsx +108 -0
- package/components/ux/auth/google/README.md +450 -0
- package/components/ux/auth/google/client.ts +109 -0
- package/components/ux/auth/google/config.ts +65 -0
- package/components/ux/auth/google/context.ts +34 -0
- package/components/ux/auth/google/default-client.ts +69 -0
- package/components/ux/auth/google/index.ts +43 -0
- package/components/ux/auth/google/types.ts +42 -0
- package/components/ux/auth/google/useGoogleAuth.ts +16 -0
- package/components/ux/auth/index.ts +72 -0
- package/components/ux/auth/useAuth.ts +16 -0
- package/components/ux/index.ts +46 -0
- package/components/ux/theme-provider-context.ts +20 -0
- package/components/ux/theme-provider.tsx +114 -0
- package/components/ux/useTheme.ts +42 -0
- package/index.ts +29 -0
- package/lib/utils.ts +57 -0
- package/package.json +170 -0
- package/styles/base.css +38 -0
- package/styles/components.css +54 -0
- package/styles/index.css +7 -0
- package/styles/tokens.css +189 -0
- package/tailwind.preset.ts +169 -0
- package/types/user.ts +69 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/** @jsxImportSource react */
|
|
5
|
+
import { useCallback, useEffect, useState } from "react";
|
|
6
|
+
import type { GoogleAuthConfig } from "./google/config.ts";
|
|
7
|
+
import type { GitHubAuthConfig } from "./github/config.ts";
|
|
8
|
+
import { GoogleAuthProvider } from "./google/AuthProvider.tsx";
|
|
9
|
+
import { GitHubAuthProvider } from "./github/AuthProvider.tsx";
|
|
10
|
+
import { GoogleAuthClient } from "./google/client.ts";
|
|
11
|
+
import { GitHubAuthClient } from "./github/client.ts";
|
|
12
|
+
import { initiateGoogleLogin as defaultInitiateGoogleLogin } from "./google/default-client.ts";
|
|
13
|
+
import { initiateGitHubLogin as defaultInitiateGitHubLogin } from "./github/default-client.ts";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type AuthUser,
|
|
17
|
+
MultiAuthContext,
|
|
18
|
+
type MultiAuthProviderProps,
|
|
19
|
+
} from "./context.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Multi-Provider Auth Provider component
|
|
23
|
+
* Wraps individual providers and provides unified auth interface
|
|
24
|
+
*/
|
|
25
|
+
export function MultiAuthProvider({
|
|
26
|
+
children,
|
|
27
|
+
providers,
|
|
28
|
+
storageKey,
|
|
29
|
+
apiBaseUrl,
|
|
30
|
+
onUserUpdate,
|
|
31
|
+
}: MultiAuthProviderProps) {
|
|
32
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [activeProvider, setActiveProvider] = useState<
|
|
35
|
+
"google" | "github" | null
|
|
36
|
+
>(null);
|
|
37
|
+
|
|
38
|
+
const googleConfig = providers.find(
|
|
39
|
+
(p) => p.provider === "google" && p.enabled
|
|
40
|
+
);
|
|
41
|
+
const githubConfig = providers.find(
|
|
42
|
+
(p) => p.provider === "github" && p.enabled
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
// Initial load from localStorage
|
|
47
|
+
const key = storageKey ?? "user";
|
|
48
|
+
const storedUser = localStorage.getItem(key);
|
|
49
|
+
if (storedUser) {
|
|
50
|
+
try {
|
|
51
|
+
const parsedUser = JSON.parse(storedUser) as AuthUser;
|
|
52
|
+
// Validate that parsed user is actually a valid user object
|
|
53
|
+
// Reject null, empty objects, or invalid values
|
|
54
|
+
if (
|
|
55
|
+
typeof parsedUser === "object" &&
|
|
56
|
+
Object.keys(parsedUser).length > 0
|
|
57
|
+
) {
|
|
58
|
+
setUser(parsedUser);
|
|
59
|
+
setActiveProvider(
|
|
60
|
+
(parsedUser as { provider?: "google" | "github" }).provider ?? null
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
// Invalid user data, clear it
|
|
64
|
+
localStorage.removeItem(key);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Error parsing stored user:", error);
|
|
68
|
+
localStorage.removeItem(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}, [storageKey]);
|
|
73
|
+
|
|
74
|
+
const updateUser = useCallback(
|
|
75
|
+
(newUser: AuthUser | null, provider?: "google" | "github") => {
|
|
76
|
+
setUser(newUser);
|
|
77
|
+
const key = storageKey ?? "user";
|
|
78
|
+
if (newUser) {
|
|
79
|
+
const userWithProvider = {
|
|
80
|
+
...newUser,
|
|
81
|
+
provider:
|
|
82
|
+
provider ??
|
|
83
|
+
(newUser as { provider?: "google" | "github" }).provider,
|
|
84
|
+
};
|
|
85
|
+
localStorage.setItem(key, JSON.stringify(userWithProvider));
|
|
86
|
+
setActiveProvider(
|
|
87
|
+
provider ??
|
|
88
|
+
(newUser as { provider?: "google" | "github" }).provider ??
|
|
89
|
+
null
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
localStorage.removeItem(key);
|
|
93
|
+
setActiveProvider(null);
|
|
94
|
+
}
|
|
95
|
+
onUserUpdate?.(newUser);
|
|
96
|
+
},
|
|
97
|
+
[storageKey, onUserUpdate]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const signOut = useCallback(() => {
|
|
101
|
+
setLoading(true);
|
|
102
|
+
const key = storageKey ?? "user";
|
|
103
|
+
localStorage.removeItem(key);
|
|
104
|
+
setUser(null);
|
|
105
|
+
setActiveProvider(null);
|
|
106
|
+
onUserUpdate?.(null);
|
|
107
|
+
setLoading(false);
|
|
108
|
+
}, [storageKey, onUserUpdate]);
|
|
109
|
+
|
|
110
|
+
const signInWithGoogle = useCallback(() => {
|
|
111
|
+
if (googleConfig) {
|
|
112
|
+
if (googleConfig.config) {
|
|
113
|
+
const client = new GoogleAuthClient(
|
|
114
|
+
googleConfig.config as GoogleAuthConfig
|
|
115
|
+
);
|
|
116
|
+
client.initiateLogin();
|
|
117
|
+
} else {
|
|
118
|
+
defaultInitiateGoogleLogin();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, [googleConfig]);
|
|
122
|
+
|
|
123
|
+
const signInWithGitHub = useCallback(() => {
|
|
124
|
+
if (githubConfig) {
|
|
125
|
+
if (githubConfig.config) {
|
|
126
|
+
const client = new GitHubAuthClient(
|
|
127
|
+
githubConfig.config as GitHubAuthConfig
|
|
128
|
+
);
|
|
129
|
+
client.initiateLogin();
|
|
130
|
+
} else {
|
|
131
|
+
defaultInitiateGitHubLogin();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}, [githubConfig]);
|
|
135
|
+
|
|
136
|
+
// Render providers based on configuration
|
|
137
|
+
let content = children;
|
|
138
|
+
|
|
139
|
+
// Wrap with GitHub provider if enabled
|
|
140
|
+
if (githubConfig) {
|
|
141
|
+
content = (
|
|
142
|
+
<GitHubAuthProvider
|
|
143
|
+
apiBaseUrl={apiBaseUrl}
|
|
144
|
+
storageKey={storageKey}
|
|
145
|
+
config={githubConfig.config as GitHubAuthConfig}
|
|
146
|
+
onUserUpdate={(user) => {
|
|
147
|
+
if (user) {
|
|
148
|
+
updateUser(user as AuthUser, "github");
|
|
149
|
+
} else if (activeProvider === "github") {
|
|
150
|
+
updateUser(null);
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{content}
|
|
155
|
+
</GitHubAuthProvider>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Wrap with Google provider if enabled
|
|
160
|
+
if (googleConfig) {
|
|
161
|
+
content = (
|
|
162
|
+
<GoogleAuthProvider
|
|
163
|
+
apiBaseUrl={apiBaseUrl}
|
|
164
|
+
storageKey={storageKey}
|
|
165
|
+
config={googleConfig.config as GoogleAuthConfig}
|
|
166
|
+
onUserUpdate={(user) => {
|
|
167
|
+
if (user) {
|
|
168
|
+
updateUser(user as AuthUser, "google");
|
|
169
|
+
} else if (activeProvider === "google") {
|
|
170
|
+
updateUser(null);
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{content}
|
|
175
|
+
</GoogleAuthProvider>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<MultiAuthContext.Provider
|
|
181
|
+
value={{
|
|
182
|
+
user,
|
|
183
|
+
loading,
|
|
184
|
+
provider: activeProvider,
|
|
185
|
+
signInWithGoogle,
|
|
186
|
+
signInWithGitHub,
|
|
187
|
+
signOut,
|
|
188
|
+
updateUser: (user) => {
|
|
189
|
+
updateUser(user, activeProvider ?? undefined);
|
|
190
|
+
},
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{content}
|
|
194
|
+
</MultiAuthContext.Provider>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Multi-Provider Authentication
|
|
2
|
+
|
|
3
|
+
This package provides a unified authentication system that supports multiple
|
|
4
|
+
OAuth providers (Google, GitHub) with a single interface. Apps can configure
|
|
5
|
+
which providers are available, and users can choose which provider to use.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔐 Support for multiple OAuth providers (Google, GitHub)
|
|
10
|
+
- 🎛️ Configure which providers are enabled per app
|
|
11
|
+
- 🎨 Unified `AuthSelector` component for provider selection
|
|
12
|
+
- 🔄 Single user identity at a time (one provider active)
|
|
13
|
+
- ⚙️ Both configurable and default client options
|
|
14
|
+
- 📦 TypeScript types for type safety
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
This package is part of the `@chesapeake/shared-components` package. Import
|
|
19
|
+
from:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { ... } from "@shared/components/ux/auth";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Setup MultiAuthProvider
|
|
28
|
+
|
|
29
|
+
Wrap your app with `MultiAuthProvider` and configure which providers are
|
|
30
|
+
enabled:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import {
|
|
34
|
+
type AuthProviderConfig,
|
|
35
|
+
MultiAuthProvider,
|
|
36
|
+
} from "@shared/components/ux/auth";
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
// Configure which providers are available
|
|
40
|
+
const authProviders: AuthProviderConfig[] = [
|
|
41
|
+
{ provider: "github", enabled: true },
|
|
42
|
+
{ provider: "google", enabled: true }, // Enable both
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<MultiAuthProvider providers={authProviders}>
|
|
47
|
+
<YourApp />
|
|
48
|
+
</MultiAuthProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Use AuthSelector Component
|
|
54
|
+
|
|
55
|
+
The `AuthSelector` component provides buttons for enabled providers:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { AuthSelector } from "@shared/components/ux/auth";
|
|
59
|
+
|
|
60
|
+
function LoginPage() {
|
|
61
|
+
return (
|
|
62
|
+
<AuthSelector
|
|
63
|
+
enableGithub={true}
|
|
64
|
+
enableGoogle={true}
|
|
65
|
+
onAuthSuccess={(user, provider) => {
|
|
66
|
+
console.log(`User authenticated with ${provider}:`, user);
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. Use Auth Hook
|
|
74
|
+
|
|
75
|
+
Access auth state and methods with the unified `useAuth` hook:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { useAuth } from "@shared/components/ux/auth";
|
|
79
|
+
|
|
80
|
+
function UserProfile() {
|
|
81
|
+
const { user, loading, provider, signOut } = useAuth();
|
|
82
|
+
|
|
83
|
+
if (loading) return <div>Loading...</div>;
|
|
84
|
+
if (!user) return <div>Not authenticated</div>;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
<p>Signed in with {provider}</p>
|
|
89
|
+
<button onClick={signOut}>Sign Out</button>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration Examples
|
|
96
|
+
|
|
97
|
+
### Single Provider (GitHub only)
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
const authProviders: AuthProviderConfig[] = [
|
|
101
|
+
{ provider: "github", enabled: true },
|
|
102
|
+
{ provider: "google", enabled: false },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
<MultiAuthProvider providers={authProviders}>
|
|
106
|
+
<AuthSelector enableGithub={true} enableGoogle={false} />
|
|
107
|
+
</MultiAuthProvider>;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Single Provider (Google only)
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
const authProviders: AuthProviderConfig[] = [
|
|
114
|
+
{ provider: "google", enabled: true },
|
|
115
|
+
{ provider: "github", enabled: false },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
<MultiAuthProvider providers={authProviders}>
|
|
119
|
+
<AuthSelector enableGoogle={true} enableGithub={false} />
|
|
120
|
+
</MultiAuthProvider>;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Custom Configuration
|
|
124
|
+
|
|
125
|
+
You can provide custom configuration for each provider:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const authProviders: AuthProviderConfig[] = [
|
|
129
|
+
{
|
|
130
|
+
provider: "github",
|
|
131
|
+
enabled: true,
|
|
132
|
+
config: {
|
|
133
|
+
clientId: "your-github-client-id",
|
|
134
|
+
redirectUri: "http://localhost:5173/auth/callback",
|
|
135
|
+
backendInitEndpoint: "/api/auth/github", // Optional: if backend handles OAuth
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
provider: "google",
|
|
140
|
+
enabled: true,
|
|
141
|
+
config: {
|
|
142
|
+
clientId: "your-google-client-id",
|
|
143
|
+
redirectUri: "http://localhost:5173/auth/callback",
|
|
144
|
+
scope: "openid email profile",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Using Default Clients
|
|
151
|
+
|
|
152
|
+
If you don't provide config, the providers will use default clients that read
|
|
153
|
+
from environment variables:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// Uses VITE_GITHUB_CLIENT_ID or VITE_GITHUB_BACKEND_INIT_ENDPOINT
|
|
157
|
+
const authProviders: AuthProviderConfig[] = [
|
|
158
|
+
{ provider: "github", enabled: true }, // Uses default client
|
|
159
|
+
{ provider: "google", enabled: true }, // Uses VITE_GOOGLE_CLIENT_ID
|
|
160
|
+
];
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## AuthSelector Component
|
|
164
|
+
|
|
165
|
+
The `AuthSelector` component provides a ready-to-use UI for authentication:
|
|
166
|
+
|
|
167
|
+
### Props
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
interface AuthSelectorProps {
|
|
171
|
+
/** Enable Google OAuth provider */
|
|
172
|
+
enableGoogle?: boolean;
|
|
173
|
+
/** Enable GitHub OAuth provider */
|
|
174
|
+
enableGithub?: boolean;
|
|
175
|
+
/** Custom styling/className */
|
|
176
|
+
className?: string;
|
|
177
|
+
/** Callback when user successfully authenticates */
|
|
178
|
+
onAuthSuccess?: (user: AuthUser, provider: "google" | "github") => void;
|
|
179
|
+
/** Custom button components (optional) */
|
|
180
|
+
googleButton?: React.ReactNode;
|
|
181
|
+
githubButton?: React.ReactNode;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Behavior
|
|
186
|
+
|
|
187
|
+
- Renders buttons for enabled providers
|
|
188
|
+
- Buttons are disabled when user is already authenticated
|
|
189
|
+
- Shows current authenticated provider state
|
|
190
|
+
- Provides sign out button when authenticated
|
|
191
|
+
- Only one provider can be active at a time
|
|
192
|
+
|
|
193
|
+
## useAuth Hook
|
|
194
|
+
|
|
195
|
+
The unified `useAuth` hook provides access to auth state and methods:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
interface MultiAuthContextType {
|
|
199
|
+
user: AuthUser | null;
|
|
200
|
+
loading: boolean;
|
|
201
|
+
provider: "google" | "github" | null;
|
|
202
|
+
signInWithGoogle: () => void;
|
|
203
|
+
signInWithGitHub: () => void;
|
|
204
|
+
signOut: () => void;
|
|
205
|
+
updateUser: (user: AuthUser | null) => void;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Backend Integration
|
|
210
|
+
|
|
211
|
+
### Backend-Handled OAuth Flow
|
|
212
|
+
|
|
213
|
+
If your backend handles the entire OAuth flow (common for server-side session
|
|
214
|
+
management), configure providers with `backendInitEndpoint`:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
const authProviders: AuthProviderConfig[] = [
|
|
218
|
+
{
|
|
219
|
+
provider: "github",
|
|
220
|
+
enabled: true,
|
|
221
|
+
config: {
|
|
222
|
+
backendInitEndpoint: "/api/auth/github", // Backend handles OAuth initiation
|
|
223
|
+
redirectUri: window.location.origin + "/api/auth/github/callback",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The frontend will redirect to the backend endpoint, which then redirects to the
|
|
230
|
+
OAuth provider. After successful authentication, the backend should redirect
|
|
231
|
+
back to your app, and the frontend will sync with the backend session.
|
|
232
|
+
|
|
233
|
+
### Frontend-Handled OAuth Flow
|
|
234
|
+
|
|
235
|
+
If your frontend initiates OAuth and backend only handles the callback:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
const authProviders: AuthProviderConfig[] = [
|
|
239
|
+
{
|
|
240
|
+
provider: "github",
|
|
241
|
+
enabled: true,
|
|
242
|
+
config: {
|
|
243
|
+
clientId: "your-github-client-id",
|
|
244
|
+
redirectUri: "http://localhost:5173/auth/callback",
|
|
245
|
+
apiBaseUrl: "", // Backend API base URL
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Individual Provider Documentation
|
|
252
|
+
|
|
253
|
+
- [Google Auth](./google/README.md) - Google OAuth setup and usage
|
|
254
|
+
- [GitHub Auth](./github/README.md) - GitHub OAuth setup and usage
|
|
255
|
+
|
|
256
|
+
## Type Safety
|
|
257
|
+
|
|
258
|
+
All components and hooks are fully typed with TypeScript:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import type {
|
|
262
|
+
AuthProviderConfig,
|
|
263
|
+
AuthSelectorProps,
|
|
264
|
+
AuthUser,
|
|
265
|
+
MultiAuthContextType,
|
|
266
|
+
MultiAuthProviderProps,
|
|
267
|
+
} from "@shared/components/ux/auth";
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Examples
|
|
271
|
+
|
|
272
|
+
### Complete Example: Todo App with GitHub Auth
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import {
|
|
276
|
+
AuthSelector,
|
|
277
|
+
MultiAuthProvider,
|
|
278
|
+
useAuth,
|
|
279
|
+
} from "@shared/components/ux/auth";
|
|
280
|
+
|
|
281
|
+
function App() {
|
|
282
|
+
const authProviders: AuthProviderConfig[] = [
|
|
283
|
+
{
|
|
284
|
+
provider: "github",
|
|
285
|
+
enabled: true,
|
|
286
|
+
config: {
|
|
287
|
+
backendInitEndpoint: "/api/auth/github",
|
|
288
|
+
redirectUri: window.location.origin + "/api/auth/github/callback",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{ provider: "google", enabled: false },
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<MultiAuthProvider providers={authProviders}>
|
|
296
|
+
<TodoApp />
|
|
297
|
+
</MultiAuthProvider>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function ProfilePage() {
|
|
302
|
+
const { user, signOut } = useAuth();
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<div>
|
|
306
|
+
{user
|
|
307
|
+
? (
|
|
308
|
+
<div>
|
|
309
|
+
<p>Signed in as {user.login || user.email}</p>
|
|
310
|
+
<button onClick={signOut}>Sign Out</button>
|
|
311
|
+
</div>
|
|
312
|
+
)
|
|
313
|
+
: <AuthSelector enableGithub={true} enableGoogle={false} />}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import React, { createContext } from "react";
|
|
5
|
+
import type { GoogleAuthConfig } from "./google/config.ts";
|
|
6
|
+
import type { GitHubAuthConfig } from "./github/config.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generic user type that applications can extend
|
|
10
|
+
*/
|
|
11
|
+
export interface AuthUser {
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
provider?: "google" | "github";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MultiAuthContextType {
|
|
17
|
+
user: AuthUser | null;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
provider: "google" | "github" | null;
|
|
20
|
+
signInWithGoogle: () => void;
|
|
21
|
+
signInWithGitHub: () => void;
|
|
22
|
+
signOut: () => void;
|
|
23
|
+
updateUser: (user: AuthUser | null) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MultiAuthContext = createContext<MultiAuthContextType | null>(
|
|
27
|
+
null
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export interface AuthProviderConfig {
|
|
31
|
+
provider: "google" | "github";
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
config?: GoogleAuthConfig | GitHubAuthConfig;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MultiAuthProviderProps {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
/** Configuration for which providers are enabled */
|
|
39
|
+
providers: AuthProviderConfig[];
|
|
40
|
+
/** Storage key for user data in localStorage (default: "user") */
|
|
41
|
+
storageKey?: string;
|
|
42
|
+
/** Backend API base URL (default: auto-detect from window.location) */
|
|
43
|
+
apiBaseUrl?: string;
|
|
44
|
+
/** Callback when user is updated */
|
|
45
|
+
onUserUpdate?: (user: AuthUser | null) => void;
|
|
46
|
+
}
|