UNPKG

cosmic-authentication

Version:

Authentication library for cosmic.new. Designed to be used and deployed on cosmic.new

173 lines (172 loc) 9.05 kB
"use strict"; 'use client'; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthProvider = AuthProvider; exports.useAuth = useAuth; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const AuthContext = (0, react_1.createContext)(undefined); // Helper function to detect if running inside an iframe const isInsideIframe = () => { try { return window.self !== window.top; } catch (e) { // If we can't access window.top due to cross-origin restrictions, we're likely in an iframe return true; } }; // Simple notification component for iframe detection const IframeNotification = ({ onClose }) => ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-white rounded-lg p-6 m-4 max-w-md shadow-xl", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center mb-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex-shrink-0", children: (0, jsx_runtime_1.jsx)("svg", { className: "h-6 w-6 text-amber-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L4.35 16.5c-.77.833.192 2.5 1.732 2.5z" }) }) }), (0, jsx_runtime_1.jsx)("h3", { className: "ml-3 text-lg font-medium text-gray-900", children: "Authentication Required" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "text-gray-700 mb-6", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-3", children: "To test Cosmic authentication, please open this preview in a new tab." }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-600", children: "Cosmic auth cannot be displayed within an iframe due to security policies." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end space-x-3", children: [(0, jsx_runtime_1.jsx)("button", { onClick: onClose, className: "px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 border border-gray-300 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500", children: "Close" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => { window.open(window.location.href, '_blank'); onClose(); }, className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500", children: "Open in New Tab" })] })] }) })); function AuthProvider({ children }) { const [authState, setAuthState] = (0, react_1.useState)({ isAuthenticated: false, user: null }); const [loading, setLoading] = (0, react_1.useState)(true); const [showIframeNotification, setShowIframeNotification] = (0, react_1.useState)(false); const checkAuthStatus = (0, react_1.useCallback)(async () => { setLoading(true); try { const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/status`, { cache: 'no-store', credentials: 'same-origin', }); if (!response.ok) { setAuthState({ isAuthenticated: false, user: null }); return { isAuthenticated: false, user: null }; } const { authenticated, user } = await response.json(); const newState = { isAuthenticated: authenticated, user }; setAuthState(newState); // Clear the return URL cookie after successful authentication check // This prevents stale redirect cookies from affecting future auth flows if (authenticated) { try { await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/clear-return-url`, { method: 'POST', credentials: 'same-origin', }); } catch (error) { console.error('[checkAuthStatus] Failed to clear return URL cookie:', error); } } return newState; } catch (error) { console.error('[checkAuthStatus] error', error); const newState = { isAuthenticated: false, user: null }; setAuthState(newState); return newState; } finally { setLoading(false); } }, []); (0, react_1.useEffect)(() => { checkAuthStatus(); const handleVisibility = () => { if (document.visibilityState === 'visible') { checkAuthStatus(); } }; document.addEventListener('visibilitychange', handleVisibility); return () => document.removeEventListener('visibilitychange', handleVisibility); }, [checkAuthStatus]); const signIn = async () => { try { // Check if we're inside an iframe if (isInsideIframe()) { setShowIframeNotification(true); return; } // Get client ID from config const clientId = process.env.NEXT_PUBLIC_CLIENT_ID; if (!clientId) { console.error("Client ID is not configured."); return; } // Clear any stale return URL cookies before starting new auth flow try { await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/clear-return-url`, { method: 'POST', credentials: 'same-origin', }); } catch (error) { console.error('[signIn] Failed to clear return URL cookie:', error); } // Use the callback page as the redirect URL const redirectUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/callback`; // Build the auth service URL with client ID and redirect URL const authUrl = `https://auth.cosmic.new/signin?client_id=${encodeURIComponent(clientId)}&redirect_url=${encodeURIComponent(redirectUrl)}`; // Clear any existing auth state before redirecting setAuthState({ isAuthenticated: false, user: null }); // Redirect to the auth URL window.location.href = authUrl; } catch (error) { console.error('Error during sign-in:', error); } }; const signOut = async () => { try { setLoading(true); // Step 1: Delete refresh token const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/signout`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }, credentials: 'same-origin' // Ensure cookies are sent }); if (response.ok) { const data = await response.json(); // Step 2: If there's a next step URL, call it to delete the access token if (data.nextStep) { const step2Response = await fetch(data.nextStep, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }, credentials: 'same-origin' }); if (!step2Response.ok) { console.error('Step 2 sign out failed:', await step2Response.text()); } } setAuthState({ isAuthenticated: false, user: null }); window.location.href = "/"; // Redirect to the homepage } else { console.error('Sign out failed:', await response.text()); } } catch (error) { console.error('Sign out error:', error); } finally { setLoading(false); } }; return ((0, jsx_runtime_1.jsxs)(AuthContext.Provider, { value: { isAuthenticated: authState.isAuthenticated, user: authState.user, signIn, signOut, checkAuthStatus, loading, }, children: [children, showIframeNotification && ((0, jsx_runtime_1.jsx)(IframeNotification, { onClose: () => setShowIframeNotification(false) }))] })); } function useAuth() { const context = (0, react_1.useContext)(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }