cosmic-authentication
Version:
Authentication library for cosmic.new. Designed to be used and deployed on cosmic.new
173 lines (172 loc) • 9.05 kB
JavaScript
;
'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;
}