flora is a fast and secure runtime that lets you write discord bots for your servers, with a rich TypeScript SDK, without worrying about running infrastructure. [mirror]
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(frontend): add dedicated login route with auth redirects

Session-Id: 97fac837-2eda-4766-9d59-31b72205a23e

+127 -33
+25 -5
apps/frontend/src/App.tsx
··· 1 1 import { Dashboard } from '@/components/dashboard' 2 2 import { DeploymentsPage } from '@/components/deployments-page' 3 3 import { EditorPage } from '@/components/editor-page' 4 + import { LoginPage } from '@/components/login-page' 4 5 import { OverviewPage } from '@/components/overview-page' 5 6 import { PrivacyPolicyPage } from '@/components/privacy-policy-page' 7 + import { RequireAuth } from '@/components/require-auth' 6 8 import { Settings } from '@/components/settings' 7 9 import { TermsOfServicePage } from '@/components/terms-of-service-page' 8 10 import { AppProvider } from '@/contexts/AppContext' 9 11 import { Seo } from '@/lib/seo' 10 12 import { ThemeProvider } from '@/lib/theme' 13 + import type { ComponentType } from 'react' 11 14 import { Route, Switch } from 'wouter' 12 15 13 16 function NotFoundPage() { ··· 19 22 ) 20 23 } 21 24 25 + function ProtectedRoute({ 26 + path, 27 + component: Component 28 + }: { 29 + path: string 30 + component: ComponentType 31 + }) { 32 + return ( 33 + <Route path={path}> 34 + <RequireAuth> 35 + <Component /> 36 + </RequireAuth> 37 + </Route> 38 + ) 39 + } 40 + 22 41 export default function App() { 23 42 return ( 24 43 <ThemeProvider> 25 44 <AppProvider> 26 45 <Switch> 27 - <Route path='/' component={Dashboard} /> 46 + <Route path='/login' component={LoginPage} /> 28 47 <Route path='/terms-of-service' component={TermsOfServicePage} /> 29 48 <Route path='/privacy-policy' component={PrivacyPolicyPage} /> 30 - <Route path='/:guildId/editor' component={EditorPage} /> 31 - <Route path='/:guildId/deployments' component={DeploymentsPage} /> 32 - <Route path='/:guildId/settings' component={Settings} /> 33 - <Route path='/:guildId' component={OverviewPage} /> 49 + <ProtectedRoute path='/' component={Dashboard} /> 50 + <ProtectedRoute path='/:guildId/editor' component={EditorPage} /> 51 + <ProtectedRoute path='/:guildId/deployments' component={DeploymentsPage} /> 52 + <ProtectedRoute path='/:guildId/settings' component={Settings} /> 53 + <ProtectedRoute path='/:guildId' component={OverviewPage} /> 34 54 <Route component={NotFoundPage} /> 35 55 </Switch> 36 56 </AppProvider>
+1 -28
apps/frontend/src/components/dashboard.tsx
··· 5 5 import { Seo } from '@/lib/seo' 6 6 import { Server } from 'lucide-react' 7 7 8 - import { LoginForm } from './login-page' 9 - 10 8 export function Dashboard() { 11 - const { session, sessionError, view } = useApp() 12 - 13 - if (sessionError) { 14 - return <FullScreenMessage /> 15 - } 16 - 17 - if (!session) { 18 - return <FullScreenMessage /> 19 - } 9 + const { view } = useApp() 20 10 21 11 return ( 22 12 <> ··· 65 55 </> 66 56 ) 67 57 } 68 - 69 - function FullScreenMessage() { 70 - return ( 71 - <> 72 - <Seo 73 - title='Guild dashboard' 74 - description='Sign in with Discord to access the flora guild dashboard.' 75 - path='/' 76 - /> 77 - <div className='flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10'> 78 - <div className='w-full max-w-sm md:max-w-4xl'> 79 - <LoginForm /> 80 - </div> 81 - </div> 82 - </> 83 - ) 84 - }
+47
apps/frontend/src/components/login-page.tsx
··· 1 1 import { Button } from '@/components/ui/button' 2 2 import { Card, CardContent } from '@/components/ui/card' 3 3 import { FieldDescription, FieldGroup } from '@/components/ui/field' 4 + import { useApp } from '@/contexts/AppContext' 4 5 import { useLoginRedirect } from '@/hooks/use-login-redirect' 6 + import { Seo } from '@/lib/seo' 5 7 import { cn } from '@/lib/utils' 8 + import { useEffect } from 'react' 9 + import { useLocation, useSearch } from 'wouter' 10 + 11 + function toSafeNext(next: string | null) { 12 + if (!next) return '/' 13 + if (!next.startsWith('/')) return '/' 14 + if (next.startsWith('//')) return '/' 15 + return next 16 + } 17 + 18 + export function LoginPage() { 19 + const { session, sessionLoading } = useApp() 20 + const [, setLocation] = useLocation() 21 + const search = useSearch() 22 + const next = toSafeNext(new URLSearchParams(search).get('next')) 23 + 24 + useEffect(() => { 25 + if (sessionLoading || !session) return 26 + setLocation(next, { replace: true }) 27 + }, [next, session, sessionLoading, setLocation]) 28 + 29 + if (sessionLoading || session) { 30 + return ( 31 + <div className='flex min-h-svh items-center justify-center p-6 text-sm text-muted-foreground'> 32 + Loading… 33 + </div> 34 + ) 35 + } 36 + 37 + return ( 38 + <> 39 + <Seo 40 + title='Login' 41 + description='Sign in with Discord to access the flora guild dashboard.' 42 + path='/login' 43 + noindex 44 + /> 45 + <div className='flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10'> 46 + <div className='w-full max-w-sm md:max-w-4xl'> 47 + <LoginForm /> 48 + </div> 49 + </div> 50 + </> 51 + ) 52 + } 6 53 7 54 export function LoginForm({ 8 55 className,
+45
apps/frontend/src/components/require-auth.tsx
··· 1 + import { useApp } from '@/contexts/AppContext' 2 + import { Seo } from '@/lib/seo' 3 + import { type ReactNode, useEffect } from 'react' 4 + import { useLocation, useSearch } from 'wouter' 5 + 6 + function toSafeNext(next: string | null) { 7 + if (!next) return '/' 8 + if (!next.startsWith('/')) return '/' 9 + if (next.startsWith('//')) return '/' 10 + return next 11 + } 12 + 13 + export function RequireAuth({ children }: { children: ReactNode }) { 14 + const { session, sessionError, sessionLoading } = useApp() 15 + const [location, setLocation] = useLocation() 16 + const search = useSearch() 17 + 18 + useEffect(() => { 19 + if (sessionLoading || session) return 20 + 21 + const next = toSafeNext(`${location}${search}`) 22 + setLocation(`/login?next=${encodeURIComponent(next)}`, { replace: true }) 23 + }, [location, search, session, sessionLoading, setLocation]) 24 + 25 + if (sessionLoading) { 26 + return ( 27 + <div className='flex min-h-svh items-center justify-center p-6 text-sm text-muted-foreground'> 28 + Loading session… 29 + </div> 30 + ) 31 + } 32 + 33 + if (sessionError) { 34 + return ( 35 + <div className='flex min-h-svh items-center justify-center p-6 text-sm text-muted-foreground'> 36 + <Seo title='Auth error' path='/login' noindex /> 37 + Failed to load session: {sessionError} 38 + </div> 39 + ) 40 + } 41 + 42 + if (!session) return null 43 + 44 + return <>{children}</> 45 + }
+9
apps/frontend/src/contexts/AppContext.tsx
··· 19 19 20 20 interface AppContextType { 21 21 session: AuthUser | null 22 + sessionLoading: boolean 22 23 sessionError: string | null 23 24 guilds: LoadState<Guild[]> 24 25 deployments: LoadState<Deployment[]> ··· 43 44 44 45 export function AppProvider({ children }: { children: ReactNode }) { 45 46 const [session, setSession] = useState<AuthUser | null>(null) 47 + const [sessionLoading, setSessionLoading] = useState(true) 46 48 const [sessionError, setSessionError] = useState<string | null>(null) 47 49 48 50 const [guilds, setGuilds] = useState<LoadState<Guild[]>>({ ...initialState }) ··· 54 56 const [view, setView] = useState<AppView>('guild') 55 57 56 58 const refreshSession = useCallback((): Promise<void> => { 59 + setSessionLoading(true) 60 + 57 61 return api 58 62 .GET('/auth/me', {}) 59 63 .then((res) => { ··· 64 68 .catch((err: any) => { 65 69 if (err.status === 401) { 66 70 setSession(null) 71 + setSessionError(null) 67 72 } else { 68 73 setSessionError(err.message || 'Failed to load session') 69 74 } 75 + }) 76 + .finally(() => { 77 + setSessionLoading(false) 70 78 }) 71 79 }, []) 72 80 ··· 136 144 <AppContext.Provider 137 145 value={{ 138 146 session, 147 + sessionLoading, 139 148 sessionError, 140 149 guilds, 141 150 deployments,