···
import { env } from '#/env'
import { handler } from '#/lib/http'
import { page } from '#/lib/view'
20
-
import { home } from '#/pages/home'
20
+
import { home, STATUS_OPTIONS } from '#/pages/home'
import { login } from '#/pages/login'
22
+
import { ifString } from './lib/util'
type Session = { did?: string }
···
handler(async (req: Request, res: Response) => {
const params = new URLSearchParams(req.originalUrl.split('?')[1])
77
-
const { session } = await ctx.oauthClient.callback(params)
78
-
const clientSession = await getIronSession<Session>(req, res, {
78
+
// Load the session cookie
79
+
const session = await getIronSession<Session>(req, res, {
password: env.COOKIE_SECRET,
82
-
assert(!clientSession.did, 'session already exists')
83
-
clientSession.did = session.did
84
-
await clientSession.save()
84
+
// If the user is already signed in, destroy the old credentials
87
+
const oauthSession = await ctx.oauthClient.restore(session.did)
88
+
if (oauthSession) oauthSession.signOut()
90
+
ctx.logger.warn({ err }, 'oauth restore failed')
94
+
// Complete the OAuth flow
95
+
const oauth = await ctx.oauthClient.callback(params)
97
+
// Update the session cookie
98
+
session.did = oauth.session.did
99
+
await session.save()
101
+
if (oauth.state?.startsWith('status:')) {
102
+
const status = oauth.state.slice(7)
103
+
const agent = new Agent(oauth.session)
105
+
await updateStatus(agent, status)
107
+
const message = err instanceof Error ? err.message : 'Unknown error'
108
+
return res.redirect(`/?error=${encodeURIComponent(message)}`)
112
+
// Redirect to the homepage
ctx.logger.error({ err }, 'oauth callback failed')
···
handler((req: Request, res: Response) => {
97
-
res.type('html').send(page(login({})))
125
+
const state = ifString(req.query.state)
126
+
res.type('html').send(page(login({ state })))
···
express.urlencoded({ extended: true }),
handler(async (req: Request, res: Response) => {
135
+
const input = ifString(req.body.input)
136
+
const state = ifString(req.body.state)
107
-
const input = req.body?.input
108
-
if (typeof input !== 'string') {
.send(page(login({ error: 'invalid input' })))
···
const url = await ctx.oauthClient.authorize(input, {
scope: 'atproto transition:generic',
res.redirect(url.toString())
ctx.logger.error({ err }, 'oauth authorize failed')
122
-
res.type('html').send(
126
-
err instanceof OAuthResolverError
128
-
: "couldn't initiate login",
156
+
err instanceof OAuthResolverError
158
+
: "couldn't initiate login"
160
+
res.type('html').send(page(login({ state, error })))
···
const service = env.PDS_URL ?? 'https://bsky.social'
const url = await ctx.oauthClient.authorize(service, {
scope: 'atproto transition:generic',
173
+
state: ifString(req.query.state),
res.redirect(url.toString())
···
handler(async (req: Request, res: Response) => {
221
+
const error = ifString(req.query.error)
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
···
// Serve the logged-out view
229
-
.send(page(home({ statuses, didHandleMap })))
261
+
.send(page(home({ error, statuses, didHandleMap })))
// Fetch additional information about the logged-in user
···
// Serve the logged-in view
251
-
res.type('html').send(
285
+
.send(page(home({ error, statuses, didHandleMap, profile, myStatus })))
···
express.urlencoded({ extended: true }),
handler(async (req: Request, res: Response) => {
294
+
const status = req.body?.status
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
275
-
.send('<h1>Error: Session required</h1>')
299
+
return void res.redirect(
300
+
`/login?state=status:${encodeURIComponent(status)}`,
278
-
// Construct & validate their status record
279
-
const rkey = TID.nextStr()
281
-
$type: 'xyz.statusphere.status',
282
-
status: req.body?.status,
283
-
createdAt: new Date().toISOString(),
285
-
if (!Status.validateRecord(record).success) {
289
-
.send('<h1>Error: Invalid status</h1>')
294
-
// Write the status record to the user's repository
295
-
const res = await agent.com.atproto.repo.putRecord({
296
-
repo: agent.assertDid,
297
-
collection: 'xyz.statusphere.status',
305
+
await updateStatus(agent, status)
306
+
return res.redirect('/')
304
-
ctx.logger.warn({ err }, 'failed to write record')
308
-
.send('<h1>Error: Failed to write record</h1>')
308
+
const message = err instanceof Error ? err.message : 'Unknown error'
309
+
return res.redirect(`/?error=${encodeURIComponent(message)}`)
312
-
// Optimistically update our SQLite
313
-
// This isn't strictly necessary because the write event will be
314
-
// handled in #/firehose/ingestor.ts, but it ensures that future reads
315
-
// will be up-to-date after this method finishes.
317
-
.insertInto('status')
320
-
authorDid: agent.assertDid,
321
-
status: record.status,
322
-
createdAt: record.createdAt,
323
-
indexedAt: new Date().toISOString(),
329
-
'failed to update computed view; ignoring as it should be caught by the firehose',
333
-
return res.redirect('/')
316
+
async function updateStatus(agent: Agent, status: unknown) {
317
+
if (typeof status !== 'string' || !STATUS_OPTIONS.includes(status)) {
318
+
throw new Error('Invalid status')
321
+
// Construct & validate their status record
322
+
const rkey = TID.nextStr()
324
+
$type: 'xyz.statusphere.status',
326
+
createdAt: new Date().toISOString(),
329
+
if (!Status.validateRecord(record).success) {
330
+
throw new Error('Invalid status record')
333
+
// Write the status record to the user's repository
334
+
const res = await agent.com.atproto.repo
336
+
repo: agent.assertDid,
337
+
collection: 'xyz.statusphere.status',
343
+
ctx.logger.error({ err }, 'failed to write record')
344
+
throw new Error('Failed to write record', { cause: err })
348
+
// Optimistically update our SQLite
349
+
// This isn't strictly necessary because the write event will be
350
+
// handled in #/firehose/ingestor.ts, but it ensures that future reads
351
+
// will be up-to-date after this method finishes.
353
+
.insertInto('status')
356
+
authorDid: agent.assertDid,
357
+
status: record.status,
358
+
createdAt: record.createdAt,
359
+
indexedAt: new Date().toISOString(),
365
+
'failed to update computed view; ignoring as it should be caught by the firehose',