Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Auth Implementation

Authentication Implementation Guide

This guide provides practical implementation examples for working with the MyStoryFlow authentication system across all applications in the monorepo.

🚀 Quick Start

1. Basic Authentication Setup

// app/layout.tsx import { AuthProvider } from '@mystoryflow/auth' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <AuthProvider supabaseUrl={process.env.NEXT_PUBLIC_SUPABASE_URL!} supabaseAnonKey={process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!} > {children} </AuthProvider> </body> </html> ) } ``` --> ### 2. Using Authentication in Components ```tsx // components/UserProfile.tsx import { useAuth } from '@mystoryflow/auth' export function UserProfile() { const { user, profile, loading, signOut } = useAuth() if (loading) { return <div>Loading...</div> } if (!user) { return <div>Please sign in</div> } return ( <div className="p-4"> <h1>Welcome, {profile?.full_name || user.email}</h1> <p>Role: {profile?.role}</p> <button onClick={signOut} className="btn btn-secondary"> Sign Out </button> </div> ) } ``` --> ### 3. Protected Routes ```tsx // components/ProtectedRoute.tsx import { ProtectedRoute } from '@mystoryflow/auth' export function AdminPage() { return ( <ProtectedRoute requiredRole="admin"> <div> <h1>Admin Dashboard</h1> {/* Admin content */} </div> </ProtectedRoute> ) } ``` --> ## 🔐 Authentication Patterns ### Login Form Implementation ```tsx // components/LoginForm.tsx import { useState } from 'react' import { useAuth } from '@mystoryflow/auth' import { useRouter } from 'next/navigation' export function LoginForm() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState('') const { signIn } = useAuth() const router = useRouter() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setError('') try { const { error } = await signIn(email, password) if (error) { setError(error.message) } else { router.push('/dashboard') } } catch (err) { setError('An unexpected error occurred') } finally { setLoading(false) } } return ( <form onSubmit={handleSubmit} className="space-y-4"> <div> <label htmlFor="email">Email</label> <input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="w-full p-2 border rounded" /> </div> <div> <label htmlFor="password">Password</label> <input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="w-full p-2 border rounded" /> </div> {error && <div className="text-red-600 text-sm">{error}</div>} <button type="submit" disabled={loading} className="w-full p-2 bg-blue-600 text-white rounded disabled:opacity-50" > {loading ? 'Signing in...' : 'Sign In'} </button> </form> ) } ``` --> ## 🔄 Cross-App Navigation ### Admin Portal Link ```tsx // components/AdminPortalLink.tsx import { useAuth } from '@mystoryflow/auth' import { createSessionBridge } from '@mystoryflow/auth' export function AdminPortalLink() { const { user, profile } = useAuth() const openAdminPortal = async () => { if (!user || !['admin', 'super_admin'].includes(profile?.role)) { alert('Admin access required') return } try { const sessionBridge = createSessionBridge( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) const currentSession = await sessionBridge.getCurrentSession() if (currentSession) { const adminUrl = process.env.NEXT_PUBLIC_ADMIN_APP_URL! const secureUrl = sessionBridge.createCrossDomainUrl( adminUrl, currentSession ) window.open(secureUrl, '_blank') } } catch (error) { console.error('Failed to open admin portal:', error) alert('Failed to open admin portal') } } if (!user || !['admin', 'super_admin'].includes(profile?.role)) { return null } return ( <button onClick={openAdminPortal} className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700" > Open Admin Portal </button> ) } ``` --> ## 🎛️ Admin Dashboard Implementation ### User Management Component ```tsx // components/admin/UserManagement.tsx import { useState, useEffect } from 'react' import { userManagementService } from '@/lib/user-management-service' export function UserManagement() { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [searchQuery, setSearchQuery] = useState('') useEffect(() => { loadUsers() }, [searchQuery]) const loadUsers = async () => { try { setLoading(true) const result = await userManagementService.searchUsers({ query: searchQuery, limit: 50, }) setUsers(result.users) } catch (error) { console.error('Failed to load users:', error) } finally { setLoading(false) } } const handleImpersonate = async (userId: string) => { try { const session = await userImpersonationService.startImpersonation({ targetUserId: userId, reason: 'Admin support', duration: 30 * 60 * 1000, // 30 minutes }) const webAppUrl = `${process.env.NEXT_PUBLIC_WEB_APP_URL}?impersonation_token=${session.token}` window.open(webAppUrl, '_blank') } catch (error) { console.error('Impersonation failed:', error) alert('Failed to start impersonation') } } return ( <div className="space-y-6"> <input type="text" placeholder="Search users..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="w-full p-2 border rounded" /> {loading ? ( <div>Loading users...</div> ) : ( <div className="grid gap-4"> {users.map((user) => ( <div key={user.id} className="p-4 border rounded flex justify-between items-center" > <div> <h3 className="font-medium">{user.full_name}</h3> <p className="text-sm text-gray-600">{user.email}</p> <span className="inline-block px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded"> {user.role} </span> </div> <div className="space-x-2"> <button onClick={() => handleImpersonate(user.id)} className="px-3 py-1 bg-yellow-600 text-white rounded text-sm" > Impersonate </button> </div> </div> ))} </div> )} </div> ) } ``` --> ## 🎭 User Impersonation Implementation ### Impersonation Banner (Web App) ```tsx // components/ImpersonationBanner.tsx import { useState, useEffect } from 'react' import { userImpersonationService } from '@/lib/user-impersonation-service' import { AlertTriangle } from 'lucide-react' export function ImpersonationBanner() { const [session, setSession] = useState(null) useEffect(() => { checkImpersonation() }, []) const checkImpersonation = async () => { const urlParams = new URLSearchParams(window.location.search) const token = urlParams.get('impersonation_token') if (token) { try { const validSession = await userImpersonationService.validateSession(token) if (validSession) { setSession(validSession) sessionStorage.setItem( 'impersonation_session', JSON.stringify(validSession) ) window.history.replaceState({}, '', window.location.pathname) } } catch (error) { console.error('Invalid impersonation token:', error) } } } const handleEndImpersonation = async () => { if (session) { try { await userImpersonationService.endImpersonation(session.id, 'manual') setSession(null) sessionStorage.removeItem('impersonation_session') window.location.href = '/login' } catch (error) { console.error('Failed to end impersonation:', error) } } } if (!session) return null return ( <div className="bg-yellow-500 text-yellow-900 px-4 py-2 text-center"> <div className="flex items-center justify-center gap-4"> <AlertTriangle className="h-5 w-5" /> <span> You are being impersonated by an administrator. Session expires at{' '} {new Date(session.expiresAt).toLocaleTimeString()} </span> <button onClick={handleEndImpersonation} className="px-3 py-1 bg-yellow-700 text-yellow-100 rounded text-sm" > End Session </button> </div> </div> ) } ``` --> ## 🔧 Development Tips ### Environment Setup ```bash # Install dependencies npm install # Set up environment variables cp .env.example .env.local # Start development servers npm run dev ``` --> ### Common Patterns ```tsx // Custom hook for admin checks export function useIsAdmin() { const { profile } = useAuth() return ['admin', 'super_admin'].includes(profile?.role) } // Role-based rendering export function RoleGuard({ role, children, fallback = null }) { const { profile } = useAuth() if (profile?.role !== role) { return fallback } return children } ``` --> ## 🚨 Troubleshooting ### Common Issues 1. **Session not persisting across apps** - Check environment variables are set correctly - Verify Supabase configuration - Check browser console for errors 2. **Admin dashboard not accessible** - Verify user role in database - Check admin permissions - Review authentication flow 3. **Impersonation not working** - Check service role permissions - Verify JWT secret configuration - Review audit logs This implementation guide provides practical examples for integrating the authentication system across your MyStoryFlow applications.