@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,34 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import React, { createContext } from "react";
|
|
5
|
+
import type { GitHubAuthConfig } 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 GitHubAuthContextType {
|
|
13
|
+
user: AuthUser | null;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
signInWithGitHub: () => void;
|
|
16
|
+
signOut: () => void;
|
|
17
|
+
updateUser: (user: AuthUser | null) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const GitHubAuthContext = createContext<GitHubAuthContextType | null>(
|
|
21
|
+
null
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export interface GitHubAuthProviderProps {
|
|
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 GitHub Auth configuration (if not provided, uses default-client) */
|
|
33
|
+
config?: GitHubAuthConfig;
|
|
34
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { createGitHubAuthConfig } from "./config.ts";
|
|
5
|
+
import { GitHubAuthClient } from "./client.ts";
|
|
6
|
+
import type { GitHubOAuthProfile } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default GitHub OAuth client (convention-over-configuration)
|
|
10
|
+
* Uses sensible defaults and environment variables for easy setup
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let defaultClient: GitHubAuthClient | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gets or creates the default GitHub Auth client
|
|
17
|
+
*/
|
|
18
|
+
function getDefaultClient(): GitHubAuthClient {
|
|
19
|
+
if (!defaultClient) {
|
|
20
|
+
const config = createGitHubAuthConfig();
|
|
21
|
+
defaultClient = new GitHubAuthClient(config);
|
|
22
|
+
}
|
|
23
|
+
return defaultClient;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initiates GitHub OAuth login with default configuration
|
|
28
|
+
* Reads from VITE_GITHUB_CLIENT_ID and VITE_REDIRECT_URI environment variables
|
|
29
|
+
*/
|
|
30
|
+
export function initiateGitHubLogin(): void {
|
|
31
|
+
getDefaultClient().initiateLogin();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handles OAuth callback with default configuration
|
|
36
|
+
* Calls POST /api/auth/github/callback by default
|
|
37
|
+
* @param code Authorization code from GitHub OAuth callback
|
|
38
|
+
* @param state State parameter from GitHub OAuth callback (optional)
|
|
39
|
+
* @returns User profile from backend
|
|
40
|
+
*/
|
|
41
|
+
export async function handleGitHubCallback(
|
|
42
|
+
code: string,
|
|
43
|
+
state?: string
|
|
44
|
+
): Promise<GitHubOAuthProfile> {
|
|
45
|
+
return await getDefaultClient().handleCallback(code, state);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default GitHub Auth class for common use cases
|
|
50
|
+
*/
|
|
51
|
+
export class DefaultGitHubAuth {
|
|
52
|
+
private client: GitHubAuthClient;
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
this.client = getDefaultClient();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initiates GitHub OAuth login
|
|
60
|
+
*/
|
|
61
|
+
login(): void {
|
|
62
|
+
this.client.initiateLogin();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handles OAuth callback
|
|
67
|
+
*/
|
|
68
|
+
async handleCallback(
|
|
69
|
+
code: string,
|
|
70
|
+
state?: string
|
|
71
|
+
): Promise<GitHubOAuthProfile> {
|
|
72
|
+
return await this.client.handleCallback(code, state);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type { GitHubOAuthProfile, GitHubUserProfile } from "./types.ts";
|
|
6
|
+
export { toUserProfile } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
// Configuration
|
|
9
|
+
export type { GitHubAuthCallbackResponse, GitHubAuthConfig } from "./config.ts";
|
|
10
|
+
export { createGitHubAuthConfig, DEFAULT_SCOPE } from "./config.ts";
|
|
11
|
+
|
|
12
|
+
// Configurable client (config-over-convention)
|
|
13
|
+
export { GitHubAuthClient, initiateGitHubLogin } from "./client.ts";
|
|
14
|
+
|
|
15
|
+
// Default client (convention-over-configuration)
|
|
16
|
+
export {
|
|
17
|
+
DefaultGitHubAuth,
|
|
18
|
+
handleGitHubCallback,
|
|
19
|
+
initiateGitHubLogin as defaultInitiateGitHubLogin,
|
|
20
|
+
} from "./default-client.ts";
|
|
21
|
+
|
|
22
|
+
// Auth Provider
|
|
23
|
+
export {
|
|
24
|
+
DefaultGitHubAuthProvider,
|
|
25
|
+
GitHubAuthProvider,
|
|
26
|
+
} from "./AuthProvider.tsx";
|
|
27
|
+
// Hook
|
|
28
|
+
export { useGitHubAuth } from "./useGitHubAuth.ts";
|
|
29
|
+
export type {
|
|
30
|
+
AuthUser,
|
|
31
|
+
GitHubAuthContextType,
|
|
32
|
+
GitHubAuthProviderProps,
|
|
33
|
+
} from "./context.ts";
|
|
34
|
+
|
|
35
|
+
// Auth Callback
|
|
36
|
+
export {
|
|
37
|
+
DefaultGitHubAuthCallback,
|
|
38
|
+
GitHubAuthCallback,
|
|
39
|
+
} from "./AuthCallback.tsx";
|
|
40
|
+
export type {
|
|
41
|
+
DefaultGitHubAuthCallbackProps,
|
|
42
|
+
GitHubAuthCallbackProps,
|
|
43
|
+
} from "./AuthCallback.tsx";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitHub OAuth user profile returned from GitHub's user API endpoint
|
|
6
|
+
*/
|
|
7
|
+
export interface GitHubOAuthProfile {
|
|
8
|
+
id: number; // GitHub user ID
|
|
9
|
+
login: string; // GitHub username
|
|
10
|
+
name?: string; // User's display name
|
|
11
|
+
email?: string; // User's email
|
|
12
|
+
avatar_url: string; // User's avatar URL
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User profile structure that can be used by applications
|
|
17
|
+
* This is the format returned from the backend after OAuth callback
|
|
18
|
+
*/
|
|
19
|
+
export interface GitHubUserProfile {
|
|
20
|
+
id: string;
|
|
21
|
+
displayName: string;
|
|
22
|
+
email?: string;
|
|
23
|
+
profilePicture?: string;
|
|
24
|
+
githubOAuthId: number;
|
|
25
|
+
githubUsername: string;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Converts GitHub OAuth profile to application user profile format
|
|
32
|
+
*/
|
|
33
|
+
export function toUserProfile(profile: GitHubOAuthProfile): GitHubUserProfile {
|
|
34
|
+
return {
|
|
35
|
+
id: profile.id.toString(),
|
|
36
|
+
displayName: profile.name ?? profile.login,
|
|
37
|
+
email: profile.email,
|
|
38
|
+
profilePicture: profile.avatar_url,
|
|
39
|
+
githubOAuthId: profile.id,
|
|
40
|
+
githubUsername: profile.login,
|
|
41
|
+
createdAt: new Date(),
|
|
42
|
+
updatedAt: new Date(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { useContext } from "react";
|
|
5
|
+
import { GitHubAuthContext, type GitHubAuthContextType } from "./context.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access GitHub Auth context
|
|
9
|
+
*/
|
|
10
|
+
export function useGitHubAuth(): GitHubAuthContextType {
|
|
11
|
+
const context = useContext(GitHubAuthContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useGitHubAuth must be used within a GitHubAuthProvider");
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/** @jsxImportSource react */
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
6
|
+
import type { GoogleOAuthProfile } from "./types.ts";
|
|
7
|
+
import { GoogleAuthClient } from "./client.ts";
|
|
8
|
+
import { handleGoogleCallback as defaultHandleCallback } from "./default-client.ts";
|
|
9
|
+
import type { GoogleAuthConfig } from "./config.ts";
|
|
10
|
+
|
|
11
|
+
export interface GoogleAuthCallbackProps {
|
|
12
|
+
/** Authorization code from URL query params */
|
|
13
|
+
code: string;
|
|
14
|
+
/** Backend API base URL (default: auto-detect) */
|
|
15
|
+
apiBaseUrl?: string;
|
|
16
|
+
/** Callback when user profile is successfully retrieved */
|
|
17
|
+
onSuccess: (profile: GoogleOAuthProfile) => void | Promise<void>;
|
|
18
|
+
/** Callback when an error occurs */
|
|
19
|
+
onError: (error: Error) => void;
|
|
20
|
+
/** Custom Google Auth configuration (if not provided, uses default-client) */
|
|
21
|
+
config?: GoogleAuthConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Configurable Google Auth Callback component
|
|
26
|
+
* Handles OAuth callback and calls appropriate callbacks
|
|
27
|
+
*/
|
|
28
|
+
export function GoogleAuthCallback({
|
|
29
|
+
code,
|
|
30
|
+
apiBaseUrl,
|
|
31
|
+
onSuccess,
|
|
32
|
+
onError,
|
|
33
|
+
config,
|
|
34
|
+
}: GoogleAuthCallbackProps) {
|
|
35
|
+
const processedRef = useRef(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (processedRef.current) return;
|
|
39
|
+
processedRef.current = true;
|
|
40
|
+
|
|
41
|
+
const handleCallback = async () => {
|
|
42
|
+
try {
|
|
43
|
+
let profile: GoogleOAuthProfile;
|
|
44
|
+
if (config) {
|
|
45
|
+
const client = new GoogleAuthClient(config);
|
|
46
|
+
profile = await client.handleCallback(code);
|
|
47
|
+
} else {
|
|
48
|
+
profile = await defaultHandleCallback(code);
|
|
49
|
+
}
|
|
50
|
+
await onSuccess(profile);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
void handleCallback();
|
|
57
|
+
}, [code, apiBaseUrl, onSuccess, onError, config]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex items-center justify-center min-h-screen bg-background">
|
|
61
|
+
<div className="text-center">
|
|
62
|
+
<h1 className="text-2xl font-bold mb-4">Completing sign in...</h1>
|
|
63
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DefaultGoogleAuthCallbackProps {
|
|
70
|
+
/** Authorization code from URL query params */
|
|
71
|
+
code: string;
|
|
72
|
+
/** Callback when existing user is found */
|
|
73
|
+
onUserFound: (profile: GoogleOAuthProfile) => void | Promise<void>;
|
|
74
|
+
/** Callback when new user (not found in system) */
|
|
75
|
+
onUserNotFound: (profile: GoogleOAuthProfile) => void | Promise<void>;
|
|
76
|
+
/** Callback when an error occurs */
|
|
77
|
+
onError: (error: Error) => void;
|
|
78
|
+
/** Backend API endpoint for user lookup (default: /api/users) */
|
|
79
|
+
userLookupEndpoint?: string;
|
|
80
|
+
/** Redirect path after successful login (default: /home) */
|
|
81
|
+
redirectPath?: string;
|
|
82
|
+
/** Redirect path for new users (default: /create-user) */
|
|
83
|
+
newUserRedirectPath?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default Google Auth Callback component with convention-over-configuration
|
|
88
|
+
* Handles common flow: callback -> lookup user -> redirect
|
|
89
|
+
*/
|
|
90
|
+
export function DefaultGoogleAuthCallback({
|
|
91
|
+
code,
|
|
92
|
+
onUserFound,
|
|
93
|
+
onUserNotFound,
|
|
94
|
+
onError,
|
|
95
|
+
userLookupEndpoint,
|
|
96
|
+
redirectPath,
|
|
97
|
+
newUserRedirectPath,
|
|
98
|
+
}: DefaultGoogleAuthCallbackProps) {
|
|
99
|
+
const processedRef = useRef(false);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (processedRef.current) return;
|
|
103
|
+
processedRef.current = true;
|
|
104
|
+
|
|
105
|
+
const handleCallback = async () => {
|
|
106
|
+
try {
|
|
107
|
+
// Get user profile from backend
|
|
108
|
+
const profile = await defaultHandleCallback(code);
|
|
109
|
+
|
|
110
|
+
// Look up existing user
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(userLookupEndpoint ?? "");
|
|
113
|
+
if (response.ok) {
|
|
114
|
+
const userResponse = (await response.json()) as {
|
|
115
|
+
value?: { googleOAuthId?: string }[];
|
|
116
|
+
};
|
|
117
|
+
const users = userResponse.value ?? [];
|
|
118
|
+
const existingUser = users.find(
|
|
119
|
+
(u: { googleOAuthId?: string }) => u.googleOAuthId === profile.sub
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (existingUser) {
|
|
123
|
+
await onUserFound(profile);
|
|
124
|
+
// Store redirect path if using localStorage
|
|
125
|
+
const storedRedirect =
|
|
126
|
+
localStorage.getItem("redirectAfterLogin") ?? redirectPath;
|
|
127
|
+
if (storedRedirect) {
|
|
128
|
+
// Redirect logic would go here
|
|
129
|
+
}
|
|
130
|
+
localStorage.removeItem("redirectAfterLogin");
|
|
131
|
+
// Note: Actual navigation should be handled by the app's router
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (lookupError) {
|
|
136
|
+
console.warn(
|
|
137
|
+
"User lookup failed, treating as new user:",
|
|
138
|
+
lookupError
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// New user flow
|
|
143
|
+
localStorage.setItem("oauth_profile", JSON.stringify(profile));
|
|
144
|
+
await onUserNotFound(profile);
|
|
145
|
+
// Note: Actual navigation should be handled by the app's router
|
|
146
|
+
} catch (error) {
|
|
147
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
void handleCallback();
|
|
152
|
+
}, [
|
|
153
|
+
code,
|
|
154
|
+
onUserFound,
|
|
155
|
+
onUserNotFound,
|
|
156
|
+
onError,
|
|
157
|
+
userLookupEndpoint,
|
|
158
|
+
redirectPath,
|
|
159
|
+
newUserRedirectPath,
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex items-center justify-center min-h-screen bg-background">
|
|
164
|
+
<div className="text-center">
|
|
165
|
+
<h1 className="text-2xl font-bold mb-4">Completing sign in...</h1>
|
|
166
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright 2026 Chesapeake Computing
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/** @jsxImportSource react */
|
|
5
|
+
import React, { useCallback, useEffect, useState } from "react";
|
|
6
|
+
import { GoogleAuthClient } from "./client.ts";
|
|
7
|
+
import { initiateGoogleLogin as defaultInitiateLogin } from "./default-client.ts";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type AuthUser,
|
|
11
|
+
GoogleAuthContext,
|
|
12
|
+
type GoogleAuthProviderProps,
|
|
13
|
+
} from "./context.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generic Google Auth Provider component
|
|
17
|
+
* Manages user state and provides auth context to children
|
|
18
|
+
*/
|
|
19
|
+
export function GoogleAuthProvider({
|
|
20
|
+
children,
|
|
21
|
+
storageKey,
|
|
22
|
+
onUserUpdate,
|
|
23
|
+
config,
|
|
24
|
+
}: GoogleAuthProviderProps) {
|
|
25
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
26
|
+
const [loading, setLoading] = useState(true);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// Initial load from localStorage
|
|
30
|
+
const storedUser = localStorage.getItem(storageKey ?? "user");
|
|
31
|
+
if (storedUser) {
|
|
32
|
+
try {
|
|
33
|
+
const parsedUser = JSON.parse(storedUser) as AuthUser;
|
|
34
|
+
// Validate that parsed user is actually a valid user object
|
|
35
|
+
// Reject null, empty objects, or invalid values
|
|
36
|
+
if (
|
|
37
|
+
typeof parsedUser === "object" &&
|
|
38
|
+
Object.keys(parsedUser).length > 0
|
|
39
|
+
) {
|
|
40
|
+
setUser(parsedUser);
|
|
41
|
+
} else {
|
|
42
|
+
// Invalid user data, clear it
|
|
43
|
+
localStorage.removeItem(storageKey ?? "user");
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("Error parsing stored user:", error);
|
|
47
|
+
localStorage.removeItem(storageKey ?? "user");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}, [storageKey]);
|
|
52
|
+
|
|
53
|
+
const updateUser = useCallback(
|
|
54
|
+
(newUser: AuthUser | null) => {
|
|
55
|
+
setUser(newUser);
|
|
56
|
+
if (newUser) {
|
|
57
|
+
localStorage.setItem(storageKey ?? "user", JSON.stringify(newUser));
|
|
58
|
+
} else {
|
|
59
|
+
localStorage.removeItem(storageKey ?? "user");
|
|
60
|
+
}
|
|
61
|
+
onUserUpdate?.(newUser);
|
|
62
|
+
},
|
|
63
|
+
[storageKey, onUserUpdate]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const signInWithGoogle = useCallback(() => {
|
|
67
|
+
if (config) {
|
|
68
|
+
const client = new GoogleAuthClient(config);
|
|
69
|
+
client.initiateLogin();
|
|
70
|
+
} else {
|
|
71
|
+
defaultInitiateLogin();
|
|
72
|
+
}
|
|
73
|
+
}, [config]);
|
|
74
|
+
|
|
75
|
+
const signOut = useCallback(() => {
|
|
76
|
+
setLoading(true);
|
|
77
|
+
localStorage.removeItem(storageKey ?? "user");
|
|
78
|
+
setUser(null);
|
|
79
|
+
onUserUpdate?.(null);
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}, [storageKey, onUserUpdate]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<GoogleAuthContext.Provider
|
|
85
|
+
value={{ user, loading, signInWithGoogle, signOut, updateUser }}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</GoogleAuthContext.Provider>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default Google Auth Provider with convention-over-configuration
|
|
94
|
+
* Uses sensible defaults: /api/auth/google/callback, storage key "user"
|
|
95
|
+
*/
|
|
96
|
+
export function DefaultGoogleAuthProvider({
|
|
97
|
+
children,
|
|
98
|
+
onUserUpdate,
|
|
99
|
+
}: {
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
onUserUpdate?: (user: AuthUser | null) => void;
|
|
102
|
+
}) {
|
|
103
|
+
return (
|
|
104
|
+
<GoogleAuthProvider storageKey="user" onUserUpdate={onUserUpdate}>
|
|
105
|
+
{children}
|
|
106
|
+
</GoogleAuthProvider>
|
|
107
|
+
);
|
|
108
|
+
}
|