import { json, redirect, redirectDocument, type ActionFunctionArgs, type LoaderFunctionArgs, } from '@remix-run/cloudflare'; import { useFetcher, useLoaderData } from '@remix-run/react'; import { auth, type AuthAPI } from '@webcontainer/api'; import { useEffect, useState } from 'react'; import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions'; import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants'; import { request as doRequest } from '~/lib/fetch'; import { logger } from '~/utils/logger'; export async function loader({ request, context }: LoaderFunctionArgs) { const { authenticated, response } = await isAuthenticated(request, context.cloudflare.env); if (authenticated) { return redirect('/', response); } const url = new URL(request.url); return json( { redirected: url.searchParams.has('code') || url.searchParams.has('error'), }, response, ); } export async function action({ request, context }: ActionFunctionArgs) { const formData = await request.formData(); const payload = { access: String(formData.get('access')), refresh: String(formData.get('refresh')), }; let response: Awaited> | undefined; try { response = await doRequest(`${CLIENT_ORIGIN}/oauth/token/info`, { headers: { authorization: `Bearer ${payload.access}` }, }); if (!response.ok) { throw await response.json(); } } catch (error) { logger.warn('Authentication failed'); logger.warn(error); return json({ error: 'invalid-token' as const }, { status: 401 }); } const boltEnabled = validateAccessToken(payload.access); if (!boltEnabled) { return json({ error: 'bolt-access' as const }, { status: 401 }); } const tokenInfo: { expires_in: number; created_at: number } = await response.json(); const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo }); return redirectDocument('/', init); } type LoginState = | { kind: 'error'; error: string; description: string; } | { kind: 'pending' }; const ERRORS = { 'bolt-access': 'You do not have access to Bolt.', 'invalid-token': 'Authentication failed.', }; export default function Login() { const { redirected } = useLoaderData(); useEffect(() => { if (!import.meta.hot?.data.wcAuth) { auth.init({ clientId: CLIENT_ID, scope: 'public', editorOrigin: CLIENT_ORIGIN }); } if (import.meta.hot) { import.meta.hot.data.wcAuth = true; } }, []); return (

Login

{redirected ? 'Processing auth...' : }
); } function LoginForm() { const [login, setLogin] = useState(null); const fetcher = useFetcher(); useEffect(() => { auth.logout({ ignoreRevokeError: true }); }, []); useEffect(() => { if (fetcher.data?.error) { auth.logout({ ignoreRevokeError: true }); setLogin({ kind: 'error' as const, ...{ error: fetcher.data.error, description: ERRORS[fetcher.data.error] }, }); } }, [fetcher.data]); async function attemptLogin() { startAuthFlow(); function startAuthFlow() { auth.startAuthFlow({ popup: true }); Promise.race([authEvent(auth, 'auth-failed'), auth.loggedIn()]).then((error) => { if (error) { setLogin({ kind: 'error', ...error }); } else { onTokens(); } }); } function onTokens() { const tokens = auth.tokens()!; fetcher.submit(tokens, { method: 'POST', }); setLogin({ kind: 'pending' }); } } return ( <> {login?.kind === 'error' && (

{login.error}

{login.description}

)} ); } interface AuthError { error: string; description: string; } function authEvent(auth: AuthAPI, event: 'logged-out'): Promise; function authEvent(auth: AuthAPI, event: 'auth-failed'): Promise; function authEvent(auth: AuthAPI, event: 'logged-out' | 'auth-failed') { return new Promise((resolve) => { const unsubscribe = auth.on(event as any, (arg: any) => { unsubscribe(); resolve(arg); }); }); }