UNPKG

@gftdcojp/gftd-orm

Version:

Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture

448 lines (432 loc) 12.5 kB
/** * Next.js Auth0 React Hooks & Protection Middleware * * nextjs-auth0互換のHooksとミドルウェア: * - useUser() - ユーザー情報取得フック * - UserProvider - Reactコンテキストプロバイダー * - withApiAuthRequired() - API保護ミドルウェア * - withPageAuthRequired() - ページ保護ミドルウェア */ 'use client'; import React, { createContext, useContext, useEffect, useState } from 'react'; /** * User Context */ const UserContext = createContext(undefined); /** * 🎣 UserProvider - Reactコンテキストプロバイダー */ export function UserProvider({ children, user: initialUser, profileUrl = '/auth/profile', loginUrl = '/auth/login' }) { const [user, setUser] = useState(initialUser || null); const [error, setError] = useState(); const [isLoading, setIsLoading] = useState(!initialUser); /** * セッション確認 */ const checkSession = async () => { try { setIsLoading(true); setError(undefined); const response = await fetch(profileUrl, { credentials: 'same-origin', }); if (response.ok) { const data = await response.json(); setUser(data.user); } else if (response.status === 401) { setUser(null); } else { throw new Error(`Failed to fetch user: ${response.status}`); } } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error')); setUser(null); } finally { setIsLoading(false); } }; /** * 初期化時にセッション確認 */ useEffect(() => { if (!initialUser) { checkSession(); } }, [initialUser]); const contextValue = { user, error, isLoading, checkSession, }; return (<UserContext.Provider value={contextValue}> {children} </UserContext.Provider>); } /** * 🎣 useUser Hook - ユーザー情報取得 */ export function useUser() { const context = useContext(UserContext); if (context === undefined) { throw new Error('useUser must be used within a UserProvider'); } return context; } /** * 🛡️ Server-side Protection Middleware */ /** * API Route保護用のwrapper */ export function withApiAuthRequired(handler) { return async (...args) => { try { // Server-side sessionチェック const session = await getServerSession(); if (!session) { return new Response(JSON.stringify({ error: 'unauthorized', message: 'Authentication required' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // 元のハンドラーを実行 return await handler(...args); } catch (error) { console.error('withApiAuthRequired error:', error); return new Response(JSON.stringify({ error: 'internal_server_error', message: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } }; } /** * Page Component保護用のwrapper */ export function withPageAuthRequired(Component, options = {}) { const { loginUrl = '/auth/login' } = options; const ProtectedComponent = (props) => { const { user, isLoading, error } = useUser(); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); // Hydration対策: マウント前は統一された表示 if (!isMounted) { return (<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}> <div>Loading...</div> </div>); } // ローディング中 if (isLoading) { return (<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}> <div>Loading...</div> </div>); } // エラー発生 if (error) { return (<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}> <div>Error: {error.message}</div> </div>); } // 未認証の場合はリダイレクト if (!user) { const returnTo = options.returnTo || window.location.pathname; const redirectUrl = `${loginUrl}?returnTo=${encodeURIComponent(returnTo)}`; window.location.href = redirectUrl; return (<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}> <div>Redirecting to login...</div> </div>); } // 認証済みの場合は元のコンポーネントを表示 return <Component {...props}/>; }; // Display nameを設定 ProtectedComponent.displayName = `withPageAuthRequired(${Component.displayName || Component.name || 'Component'})`; return ProtectedComponent; } /** * 🔧 Server-side Utilities */ /** * サーバーサイドでセッションを取得(Server Componentで使用) */ async function getServerSession() { try { // Dynamic importを使用してServer-side限定の関数を呼び出し const { getSession } = await import('./nextjs-auth0'); return await getSession(); } catch (error) { console.error('Failed to get server session:', error); return null; } } /** * 🎯 Higher-Order Components (HOCs) */ /** * 認証が必要なレイアウトコンポーネント */ export function AuthenticatedLayout({ children, fallback }) { const { user, isLoading } = useUser(); if (isLoading) { return fallback || (<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> <div>Loading...</div> </div>); } if (!user) { return (<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> <h2>Authentication Required</h2> <div style={{ marginTop: '1rem' }}> <a href="/auth/login" style={{ padding: '0.5rem 1rem', backgroundColor: '#0070f3', color: 'white', textDecoration: 'none', borderRadius: '4px', marginRight: '0.5rem' }}> Log In </a> <a href="/auth/login?screen_hint=signup" style={{ padding: '0.5rem 1rem', backgroundColor: '#28a745', color: 'white', textDecoration: 'none', borderRadius: '4px' }}> Sign Up </a> </div> </div>); } return <>{children}</>; } /** * 🎛️ Utility Hooks */ /** * Access Token取得フック */ export function useAccessToken() { const [accessToken, setAccessToken] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const fetchAccessToken = async () => { try { setIsLoading(true); setError(null); const response = await fetch('/auth/access-token', { credentials: 'same-origin', }); if (response.ok) { const data = await response.json(); setAccessToken(data.accessToken); } else { throw new Error(`Failed to fetch access token: ${response.status}`); } } catch (err) { setError(err instanceof Error ? err : new Error('Unknown error')); } finally { setIsLoading(false); } }; useEffect(() => { fetchAccessToken(); }, []); return { accessToken, isLoading, error, refetch: fetchAccessToken, }; } /** * ログアウト機能フック */ export function useLogout() { const logout = (returnTo) => { const logoutUrl = returnTo ? `/auth/logout?returnTo=${encodeURIComponent(returnTo)}` : '/auth/logout'; if (typeof window !== 'undefined') { window.location.href = logoutUrl; } }; return logout; } /** * 📖 使用例 */ export const nextjsAuth0HooksExamples = { /** * App Component での UserProvider 設定例 */ appComponent: ` // app/layout.tsx import { UserProvider } from './src/nextjs-auth0-hooks'; import { getSession } from './src/nextjs-auth0'; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const session = await getSession(); return ( <html> <body> <UserProvider user={session?.user}> {children} </UserProvider> </body> </html> ); } `, /** * Client Component での useUser 使用例 */ clientComponent: ` 'use client'; import { useUser, useLogout } from './src/nextjs-auth0-hooks'; export default function UserProfile() { const { user, isLoading, error } = useUser(); const logout = useLogout(); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; if (!user) return <div>Not authenticated</div>; return ( <div> <h1>Welcome, {user.name}!</h1> <p>Email: {user.email}</p> <button onClick={() => logout()}>Logout</button> </div> ); } `, /** * Protected Page の実装例 */ protectedPage: ` 'use client'; import { withPageAuthRequired } from './src/nextjs-auth0-hooks'; function Dashboard() { return ( <div> <h1>Dashboard</h1> <p>This page is protected and requires authentication.</p> </div> ); } export default withPageAuthRequired(Dashboard); `, /** * Protected API Route の実装例 */ protectedApi: ` // app/api/protected/route.ts import { withApiAuthRequired } from './src/nextjs-auth0-hooks'; async function handler() { return Response.json({ message: 'This is a protected API route', timestamp: new Date().toISOString() }); } export const GET = withApiAuthRequired(handler); export const POST = withApiAuthRequired(handler); `, /** * Layout with Authentication の実装例 */ authenticatedLayout: ` 'use client'; import { AuthenticatedLayout } from './src/nextjs-auth0-hooks'; export default function AppLayout({ children }: { children: React.ReactNode }) { return ( <AuthenticatedLayout fallback={<div>Loading your dashboard...</div>} > <nav> <h1>My App</h1> </nav> <main>{children}</main> </AuthenticatedLayout> ); } `, /** * Access Token使用例 */ accessTokenUsage: ` 'use client'; import { useAccessToken } from './src/nextjs-auth0-hooks'; export default function ApiCaller() { const { accessToken, isLoading, error } = useAccessToken(); const callProtectedApi = async () => { if (!accessToken) return; const response = await fetch('/api/protected', { headers: { 'Authorization': \`Bearer \${accessToken}\` } }); const data = await response.json(); console.log(data); }; if (isLoading) return <div>Loading access token...</div>; if (error) return <div>Error: {error.message}</div>; return ( <button onClick={callProtectedApi}> Call Protected API </button> ); } `, }; //# sourceMappingURL=nextjs-auth0-hooks.jsx.map