feat: require owner login and add onboarding

This commit is contained in:
2026-09-28 19:14:14 +09:00
parent 57882cf5f9
commit 85d23a328b
42 changed files with 2400 additions and 23 deletions
+103
View File
@@ -0,0 +1,103 @@
import {
AuthError,
completeOnboarding,
createOwner,
getSession,
readAuthState,
SESSION_MAX_AGE_SECONDS,
signIn,
signOut,
} from './auth.server'
const SESSION_COOKIE = 'workspace_session'
export function sessionToken(request: Request) {
return request.headers
.get('cookie')
?.split(';')
.map((part) => part.trim())
.find((part) => part.startsWith(`${SESSION_COOKIE}=`))
?.slice(SESSION_COOKIE.length + 1)
}
export function authState(request: Request) {
return { ...readAuthState(), owner: getSession(sessionToken(request)) }
}
function cookie(token: string, clear = false) {
const secure = process.env.TWITTER_LITE_ORIGIN?.startsWith('https:')
return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${clear ? 0 : SESSION_MAX_AGE_SECONDS}${secure ? '; Secure' : ''}`
}
export async function authEndpoint(request: Request) {
const headers = new Headers({
'cache-control': 'no-store',
vary: 'Cookie',
'content-type': 'application/json',
})
try {
if (request.method === 'GET')
return Response.json(authState(request), { headers })
if (request.method !== 'POST')
return new Response(null, { status: 405, headers })
if (!request.headers.get('content-type')?.startsWith('application/json'))
throw new AuthError(400, 'Use a JSON request.')
const body = await request.text()
if (body.length > 8192) throw new AuthError(413, 'Request is too large.')
let data: Record<string, unknown>
try {
const parsed = JSON.parse(body)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
throw new Error('invalid body')
data = parsed
} catch {
throw new AuthError(400, 'Check the form and try again.')
}
const text = (key: string) =>
typeof data[key] === 'string' ? data[key] : ''
const oldToken = sessionToken(request)
switch (data.action) {
case 'setup':
case 'login': {
const result =
data.action === 'setup'
? await createOwner({
email: text('email'),
password: text('password'),
name: text('name'),
setupToken: text('setupToken'),
})
: await signIn({ email: text('email'), password: text('password') })
signOut(oldToken)
headers.set('set-cookie', cookie(result.sessionToken))
return Response.json(
{ needsSetup: false, owner: result.owner },
{ headers },
)
}
case 'onboard': {
const owner = completeOnboarding(oldToken ?? '', { name: text('name') })
return Response.json({ needsSetup: false, owner }, { headers })
}
case 'logout':
signOut(oldToken)
headers.set('set-cookie', cookie('', true))
return Response.json(
{ needsSetup: readAuthState().needsSetup, owner: null },
{ headers },
)
default:
throw new AuthError(400, 'Unknown action.')
}
} catch (error) {
if (error instanceof AuthError)
return Response.json(
{ error: error.message },
{ status: error.status, headers },
)
return Response.json(
{ error: 'Unable to complete the request. Please try again.' },
{ status: 500, headers },
)
}
}