Scratch space for learning atproto app development

Merge pull request #5 from bluesky-social/paul/backend-simplifications

Backend simplifications

Changed files
+42 -94
src
+32 -4
src/auth/session.ts
···
import type { IncomingMessage, ServerResponse } from 'node:http'
import { getIronSession } from 'iron-session'
import { env } from '#/env'
+
import { AppContext } from '#/config'
export type Session = { did: string }
-
export async function createSession(req: IncomingMessage, res: ServerResponse<IncomingMessage>, did: string) {
+
export async function createSession(
+
req: IncomingMessage,
+
res: ServerResponse<IncomingMessage>,
+
did: string
+
) {
const session = await getSessionRaw(req, res)
assert(!session.did, 'session already exists')
session.did = did
···
return { did: session.did }
}
-
export async function destroySession(req: IncomingMessage, res: ServerResponse<IncomingMessage>) {
+
export async function destroySession(
+
req: IncomingMessage,
+
res: ServerResponse<IncomingMessage>
+
) {
const session = await getSessionRaw(req, res)
await session.destroy()
return null
}
-
export async function getSession(req: IncomingMessage, res: ServerResponse<IncomingMessage>) {
+
export async function getSession(
+
req: IncomingMessage,
+
res: ServerResponse<IncomingMessage>
+
) {
const session = await getSessionRaw(req, res)
if (!session.did) return null
return { did: session.did }
}
-
async function getSessionRaw(req: IncomingMessage, res: ServerResponse<IncomingMessage>) {
+
export async function getSessionAgent(
+
req: IncomingMessage,
+
res: ServerResponse<IncomingMessage>,
+
ctx: AppContext
+
) {
+
const session = await getSessionRaw(req, res)
+
if (!session.did) return null
+
return await ctx.oauthClient.restore(session.did).catch(async (err) => {
+
ctx.logger.warn({ err }, 'oauth restore failed')
+
await destroySession(req, res)
+
return null
+
})
+
}
+
+
async function getSessionRaw(
+
req: IncomingMessage,
+
res: ServerResponse<IncomingMessage>
+
) {
return await getIronSession<Session>(req, res, {
cookieName: 'sid',
password: env.COOKIE_SECRET,
-7
src/db/migrations.ts
···
migrations['001'] = {
async up(db: Kysely<unknown>) {
await db.schema
-
.createTable('did_cache')
-
.addColumn('did', 'varchar', (col) => col.primaryKey())
-
.addColumn('doc', 'varchar', (col) => col.notNull())
-
.addColumn('updatedAt', 'varchar', (col) => col.notNull())
-
.execute()
-
await db.schema
.createTable('status')
.addColumn('authorDid', 'varchar', (col) => col.primaryKey())
.addColumn('status', 'varchar', (col) => col.notNull())
···
await db.schema.dropTable('auth_state').execute()
await db.schema.dropTable('auth_session').execute()
await db.schema.dropTable('status').execute()
-
await db.schema.dropTable('did_cache').execute()
},
}
-7
src/db/schema.ts
···
export type DatabaseSchema = {
-
did_cache: DidCache
status: Status
auth_session: AuthSession
auth_state: AuthState
-
}
-
-
export type DidCache = {
-
did: string
-
doc: string
-
updatedAt: string
}
export type Status = {
+3 -57
src/ident/resolver.ts
···
-
import { IdResolver, DidDocument, CacheResult } from '@atproto/identity'
-
import type { Database } from '#/db'
+
import { IdResolver, MemoryCache } from '@atproto/identity'
const HOUR = 60e3 * 60
const DAY = HOUR * 24
-
export function createResolver(db: Database) {
+
export function createResolver() {
const resolver = new IdResolver({
-
didCache: {
-
async cacheDid(did: string, doc: DidDocument): Promise<void> {
-
await db
-
.insertInto('did_cache')
-
.values({
-
did,
-
doc: JSON.stringify(doc),
-
updatedAt: new Date().toISOString(),
-
})
-
.onConflict((oc) =>
-
oc.column('did').doUpdateSet({
-
doc: JSON.stringify(doc),
-
updatedAt: new Date().toISOString(),
-
})
-
)
-
.execute()
-
},
-
-
async checkCache(did: string): Promise<CacheResult | null> {
-
const row = await db
-
.selectFrom('did_cache')
-
.selectAll()
-
.where('did', '=', did)
-
.executeTakeFirst()
-
if (!row) return null
-
const now = Date.now()
-
const updatedAt = +new Date(row.updatedAt)
-
return {
-
did,
-
doc: JSON.parse(row.doc),
-
updatedAt,
-
stale: now > updatedAt + HOUR,
-
expired: now > updatedAt + DAY,
-
}
-
},
-
-
async refreshCache(
-
did: string,
-
getDoc: () => Promise<DidDocument | null>
-
): Promise<void> {
-
const doc = await getDoc()
-
if (doc) {
-
await this.cacheDid(did, doc)
-
}
-
},
-
-
async clearEntry(did: string): Promise<void> {
-
await db.deleteFrom('did_cache').where('did', '=', did).execute()
-
},
-
-
async clear(): Promise<void> {
-
await db.deleteFrom('did_cache').execute()
-
},
-
},
+
didCache: new MemoryCache(HOUR, DAY),
})
return {
+6 -18
src/routes/index.ts
···
import { OAuthResolverError } from '@atproto/oauth-client-node'
import { isValidHandle } from '@atproto/syntax'
import express from 'express'
-
import { createSession, destroySession, getSession } from '#/auth/session'
+
import { createSession, destroySession, getSessionAgent } from '#/auth/session'
import type { AppContext } from '#/config'
import { home } from '#/pages/home'
import { login } from '#/pages/login'
···
router.get(
'/',
handler(async (req, res) => {
-
const session = await getSession(req, res)
-
const agent =
-
session &&
-
(await ctx.oauthClient.restore(session.did).catch(async (err) => {
-
ctx.logger.warn({ err }, 'oauth restore failed')
-
await destroySession(req, res)
-
return null
-
}))
+
const agent = await getSessionAgent(req, res, ctx)
const statuses = await ctx.db
.selectFrom('status')
.selectAll()
···
if (!agent) {
return res.type('html').send(page(home({ statuses, didHandleMap })))
}
-
const { data: profile } = await agent.getProfile({ actor: session.did })
+
const { data: profile } = await agent.getProfile({
+
actor: agent.accountDid,
+
})
return res
.type('html')
.send(page(home({ statuses, didHandleMap, profile, myStatus })))
···
router.post(
'/status',
handler(async (req, res) => {
-
const session = await getSession(req, res)
-
const agent =
-
session &&
-
(await ctx.oauthClient.restore(session.did).catch(async (err) => {
-
ctx.logger.warn({ err }, 'oauth restore failed')
-
await destroySession(req, res)
-
return null
-
}))
+
const agent = await getSessionAgent(req, res, ctx)
if (!agent) {
return res.status(401).json({ error: 'Session required' })
}
+1 -1
src/server.ts
···
await migrateToLatest(db)
const ingester = new Ingester(db)
const oauthClient = await createClient(db)
-
const resolver = await createResolver(db)
+
const resolver = createResolver()
ingester.start()
const ctx = {
db,