@mgcmnd/auth-client
Version:
Client-side authentication provider for mgcmnd.net services.
1 lines • 12.5 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/AuthContext.tsx","../src/AuthCallbackHandler.tsx"],"sourcesContent":["export { AuthProvider, useAuth } from './AuthContext';\nexport type { AuthUser, AuthConfig } from './AuthContext';\n\nexport { AuthCallbackHandler } from './AuthCallbackHandler';\nexport type { AuthCallbackHandlerProps } from './AuthCallbackHandler';","import React, { createContext, useContext, useState, useEffect } from 'react';\nimport { jwtDecode } from 'jwt-decode';\n\nexport interface AuthUser {\n id: string;\n email?: string;\n name?: string;\n picture?: string;\n [key: string]: any;\n}\n\nexport interface AuthState {\n isAuthenticated: boolean;\n user: AuthUser | null;\n isLoading: boolean;\n error: string | null;\n token: string | null;\n}\n\nexport interface AuthConfig {\n authServerUrl?: string;\n tokenStorageKey?: string;\n redirectPath?: string;\n onLoginSuccess?: (user: AuthUser, token: string) => void;\n onLogoutSuccess?: () => void;\n}\n\ninterface AuthContextType extends AuthState {\n loginWithGoogle: (redirectPath?: string) => void;\n loginWithGitHub: (redirectPath?: string) => void;\n loginWithEmail: (email: string, redirectPath?: string) => Promise<void>;\n logout: (redirectUrl?: string) => void;\n handleCallback: () => void;\n}\n\nconst DEFAULT_CONFIG: Required<AuthConfig> = {\n authServerUrl: 'https://auth.mgcmnd.net',\n tokenStorageKey: 'mgcmnd_auth_token',\n redirectPath: '/auth/callback',\n onLoginSuccess: () => { },\n onLogoutSuccess: () => { },\n};\n\nconst AuthContext = createContext<AuthContextType | undefined>(undefined);\n\nexport const AuthProvider: React.FC<{ children: React.ReactNode; config?: AuthConfig }> = ({\n children,\n config = {},\n}) => {\n const cfg = { ...DEFAULT_CONFIG, ...config };\n\n const [state, setState] = useState<AuthState>({\n isAuthenticated: false,\n user: null,\n isLoading: false,\n error: null,\n token: null,\n });\n\n const generateState = () => {\n const array = new Uint8Array(32);\n window.crypto.getRandomValues(array);\n return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');\n };\n\n const getRedirectUri = (path?: string) => {\n const basePath = path || cfg.redirectPath;\n return `${window.location.origin}${basePath}`;\n };\n\n const decodeAndSetToken = (token: string, skipCallback = false) => {\n try {\n // Prevent processing the same token multiple times\n if (state.token === token && state.isAuthenticated) {\n return;\n }\n\n const decoded = jwtDecode<any>(token);\n const user: AuthUser = {\n id: decoded.sub || decoded.id,\n email: decoded.email,\n name: decoded.name,\n picture: decoded.picture,\n ...decoded,\n };\n\n localStorage.setItem(cfg.tokenStorageKey, token);\n setState({\n isAuthenticated: true,\n user,\n isLoading: false,\n error: null,\n token,\n });\n\n // Only call onLoginSuccess when explicitly logging in (not on mount)\n if (!skipCallback) {\n cfg.onLoginSuccess(user, token);\n }\n } catch (error) {\n setState(prev => ({\n ...prev,\n isLoading: false,\n error: 'Invalid token format',\n }));\n }\n };\n\n // Load token on mount\n useEffect(() => {\n const token = localStorage.getItem(cfg.tokenStorageKey);\n if (token) {\n // Skip callback on initial load to prevent duplicate calls\n decodeAndSetToken(token, true);\n }\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Auth methods\n const loginWithProvider = (provider: 'google' | 'github', redirectPath?: string) => {\n const state = generateState();\n sessionStorage.setItem('oauth_state', state);\n\n const params = new URLSearchParams({\n redirect_uri: getRedirectUri(redirectPath),\n state,\n });\n\n window.location.href = `${cfg.authServerUrl}/${provider}/login?${params}`;\n };\n\n const loginWithGoogle = (redirectPath?: string) => loginWithProvider('google', redirectPath);\n const loginWithGitHub = (redirectPath?: string) => loginWithProvider('github', redirectPath);\n\n const loginWithEmail = async (email: string, redirectPath?: string) => {\n setState(prev => ({ ...prev, isLoading: true, error: null }));\n\n try {\n const state = generateState();\n sessionStorage.setItem('oauth_state', state);\n\n const response = await fetch(`${cfg.authServerUrl}/email/login`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email,\n redirect_uri: getRedirectUri(redirectPath),\n state,\n }),\n });\n\n if (!response.ok) {\n throw new Error('Failed to request email login');\n }\n\n setState(prev => ({ ...prev, isLoading: false }));\n } catch (error) {\n setState(prev => ({\n ...prev,\n isLoading: false,\n error: error instanceof Error ? error.message : 'Email login failed',\n }));\n throw error;\n }\n };\n\n const logout = (redirectUrl?: string) => {\n localStorage.removeItem(cfg.tokenStorageKey);\n setState({\n isAuthenticated: false,\n user: null,\n isLoading: false,\n error: null,\n token: null,\n });\n cfg.onLogoutSuccess();\n\n if (redirectUrl) {\n window.location.href = redirectUrl;\n }\n };\n\n const handleCallback = () => {\n const params = new URLSearchParams(window.location.search);\n const token = params.get('token');\n const stateParam = params.get('state');\n const error = params.get('error');\n const storedState = sessionStorage.getItem('oauth_state');\n\n // Clean up\n sessionStorage.removeItem('oauth_state');\n window.history.replaceState({}, document.title, window.location.pathname);\n\n if (error) {\n setState(prev => ({ ...prev, error }));\n return;\n }\n\n if (stateParam !== storedState) {\n setState(prev => ({ ...prev, error: 'Invalid state parameter' }));\n return;\n }\n\n if (token) {\n decodeAndSetToken(token, false); // Explicitly call callback for new login\n } else {\n setState(prev => ({ ...prev, error: 'No token received' }));\n }\n };\n\n const value: AuthContextType = {\n ...state,\n loginWithGoogle,\n loginWithGitHub,\n loginWithEmail,\n logout,\n handleCallback,\n };\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n};\n\nexport const useAuth = () => {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return context;\n};","import React, { useEffect } from 'react';\nimport { useAuth } from './AuthContext';\n\nexport interface AuthCallbackHandlerProps {\n onSuccess?: () => void;\n onError?: (error: string) => void;\n loading?: React.ReactNode;\n}\n\nexport const AuthCallbackHandler: React.FC<AuthCallbackHandlerProps> = ({\n onSuccess,\n onError,\n loading = <p>Processing authentication...</p>,\n}) => {\n const { handleCallback, isAuthenticated, error, isLoading } = useAuth();\n\n useEffect(() => {\n handleCallback();\n }, []);\n\n useEffect(() => {\n if (!isLoading) {\n if (isAuthenticated && onSuccess) {\n onSuccess();\n } else if (error && onError) {\n onError(error);\n }\n }\n }, [isLoading, isAuthenticated, error, onSuccess, onError]);\n\n if (isLoading) {\n return <>{loading}</>;\n }\n\n return null;\n};"],"mappings":"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","names":["state","import_react","import_jsx_runtime"]}