Scratch space for learning atproto app development

tidy

Changed files
+64 -23
src
+1
src/context.ts
···
logger,
oauthClient,
resolver,
+
async destroy() {
await ingester.destroy()
await db.destroy()
+1 -1
src/index.ts
···
// Gracefully shutdown the http server
await terminate()
-
// Close the firehose connection
+
// Gracefully shutdown the application context
await ctx.destroy()
})
+62 -22
src/routes.ts
···
import { handler } from '#/lib/http'
import { ifString } from '#/lib/util'
import { page } from '#/lib/view'
-
import { home, STATUS_OPTIONS } from '#/pages/home'
+
import { home } from '#/pages/home'
import { login } from '#/pages/login'
+
+
const MAX_AGE = env.NODE_ENV === 'production' ? 60 : 0
type Session = { did?: string }
···
}
}
-
export function createRouter(ctx: AppContext): RequestListener {
+
export const createRouter = (ctx: AppContext): RequestListener => {
const router = express()
// Static assets
-
router.use('/public', express.static(path.join(__dirname, 'pages', 'public')))
+
router.use(
+
'/public',
+
express.static(path.join(__dirname, 'pages', 'public'), {
+
maxAge: MAX_AGE * 1000,
+
}),
+
)
// OAuth metadata
router.get(
'/oauth-client-metadata.json',
handler((req: Request, res: Response) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
res.json(ctx.oauthClient.clientMetadata)
}),
)
···
router.get(
'/.well-known/jwks.json',
handler((req: Request, res: Response) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
res.json(ctx.oauthClient.jwks)
}),
)
···
router.get(
'/oauth/callback',
handler(async (req: Request, res: Response) => {
+
res.setHeader('cache-control', 'no-store')
+
const params = new URLSearchParams(req.originalUrl.split('?')[1])
try {
// Load the session cookie
···
router.get(
'/login',
handler(async (req: Request, res: Response) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
+
return res.type('html').send(page(login({})))
}),
)
···
'/login',
express.urlencoded(),
handler(async (req: Request, res: Response) => {
+
res.setHeader('cache-control', 'no-store')
+
const input = ifString(req.body.input)
// Validate
if (!input) {
return res.type('html').send(page(login({ error: 'invalid input' })))
}
+
+
// @NOTE "input" can be a handle, a DID or a service URL (PDS).
+
+
// Initiate the OAuth flow
+
try {
+
const url = await ctx.oauthClient.authorize(input, {
+
scope: 'atproto transition:generic',
+
})
+
res.redirect(url.toString())
+
} catch (err) {
+
ctx.logger.error({ err }, 'oauth authorize failed')
+
+
const error =
+
err instanceof OAuthResolverError
+
? err.message
+
: "couldn't initiate login"
+
+
return res.type('html').send(page(login({ error })))
+
}
}),
)
···
router.get(
'/signup',
handler(async (req: Request, res: Response) => {
+
res.setHeader('cache-control', `max-age=${MAX_AGE}, public`)
+
try {
const service = env.PDS_URL ?? 'https://bsky.social'
const url = await ctx.oauthClient.authorize(service, {
···
router.post(
'/logout',
handler(async (req: Request, res: Response) => {
+
// Never store this route
+
res.setHeader('cache-control', 'no-store')
+
const session = await getIronSession<Session>(req, res, {
cookieName: 'sid',
password: env.COOKIE_SECRET,
···
router.get(
'/',
handler(async (req: Request, res: Response) => {
+
// Prevent caching of this page when the credentials change
+
res.setHeader('Vary', 'Cookie')
+
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
···
return res.type('html').send(page(home({ statuses, didHandleMap })))
}
+
// Make sure this page does not get cached in public caches (proxies)
+
res.setHeader('cache-control', 'private')
+
// Fetch additional information about the logged-in user
const profileResponse = await agent.com.atproto.repo
.getRecord({
···
: {}
// Serve the logged-in view
-
res.type('html').send(
-
page(
-
home({
-
statuses,
-
didHandleMap,
-
profile,
-
myStatus,
-
}),
-
),
-
)
+
res
+
.type('html')
+
.send(page(home({ statuses, didHandleMap, profile, myStatus })))
}),
)
···
'/status',
express.urlencoded(),
handler(async (req: Request, res: Response) => {
+
// Never store this route
+
res.setHeader('cache-control', 'no-store')
+
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
if (!agent) {
-
return res.redirect(`/login`)
-
}
-
-
const status = ifString(req.body?.status)
-
if (!status || !STATUS_OPTIONS.includes(status)) {
-
throw new Error('Invalid status')
+
return res
+
.status(401)
+
.type('html')
+
.send('<h1>Error: Session required</h1>')
}
// Construct & validate their status record
const rkey = TID.nextStr()
const record = {
$type: 'xyz.statusphere.status',
-
status,
+
status: req.body?.status,
createdAt: new Date().toISOString(),
}
+
// Make sure the record generated from the input is valid
if (!Status.validateRecord(record).success) {
return res
.status(400)
···
.send('<h1>Error: Invalid status</h1>')
}
-
// Write the status record to the user's repository
let uri
try {
+
// Write the status record to the user's repository
const res = await agent.com.atproto.repo.putRecord({
repo: agent.assertDid,
collection: 'xyz.statusphere.status',
···
)
}
-
res.redirect('/')
+
return res.redirect('/')
}),
)