Scratch space for learning atproto app development

Improve logging & sign-up

-1
package.json
···
"@atproto/lexicon": "^0.4.11",
"@atproto/oauth-client-node": "^0.3.1",
"@atproto/sync": "^0.1.26",
-
"@atproto/syntax": "^0.4.0",
"@atproto/xrpc-server": "^0.8.0",
"better-sqlite3": "^11.1.2",
"dotenv": "^16.4.5",
+2
src/auth/client.ts
···
clientMetadata,
stateStore: new StateStore(db),
sessionStore: new SessionStore(db),
+
plcDirectoryUrl: env.PLC_URL,
+
handleResolver: env.PDS_URL,
})
}
+2 -1
src/context.ts
···
import { createOAuthClient } from '#/auth/client'
import { createDb, Database } from '#/db'
import { createIngester } from '#/ingester'
+
import { env } from '#/env'
/**
* Application state passed to the router and elsewhere
···
const db = await createDb()
const oauthClient = await createOAuthClient(db)
const ingester = createIngester(db)
-
const logger = pino({ name: 'server' })
+
const logger = pino({ name: 'server', level: env.LOG_LEVEL })
return {
db,
+5
src/env.ts
···
DB_PATH: str({ devDefault: ':memory:' }),
COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }),
PRIVATE_KEYS: envalidJsonWebKeys({ default: undefined }),
+
LOG_LEVEL: str({ default: 'info' }),
+
PDS_OWNER: str({ default: 'Bluesky' }),
+
PDS_URL: str({ default: 'https://bsky.social' }),
+
PLC_URL: str({ default: 'https://plc.directory' }),
+
FIREHOSE_URL: str({ default: 'wss://bsky.network' }),
})
+14 -1
src/ingester.ts
···
import { IdResolver, MemoryCache } from '@atproto/identity'
import { Event, Firehose } from '@atproto/sync'
import pino from 'pino'
+
import { env } from './env'
const HOUR = 60e3 * 60
const DAY = HOUR * 24
export function createIngester(db: Database) {
-
const logger = pino({ name: 'firehose ingestion' })
+
const logger = pino({ name: 'firehose', level: env.LOG_LEVEL })
return new Firehose({
+
service: env.FIREHOSE_URL,
idResolver: new IdResolver({
+
plcUrl: env.PLC_URL,
didCache: new MemoryCache(HOUR, DAY),
}),
handleEvent: async (evt: Event) => {
···
Status.isRecord(record) &&
Status.validateRecord(record).success
) {
+
logger.debug(
+
{ uri: evt.uri.toString(), status: record.status },
+
'ingesting status',
+
)
+
// Store the status in our SQLite
await db
.insertInto('status')
···
evt.event === 'delete' &&
evt.collection === 'xyz.statusphere.status'
) {
+
logger.debug(
+
{ uri: evt.uri.toString(), did: evt.did },
+
'deleting status',
+
)
+
// Remove the status from our SQLite
await db
.deleteFrom('status')
+8 -6
src/pages/login.ts
···
+
import { env } from '#/env'
import { html } from '../lib/view'
import { shell } from './shell'
···
<form action="/login" method="post" class="login-form">
<input
type="text"
-
name="handle"
+
name="input"
placeholder="Enter your handle (eg alice.bsky.social)"
required
/>
<button type="submit">Log in</button>
-
${error ? html`<p>Error: <i>${error}</i></p>` : undefined}
</form>
-
<div class="signup-cta">
-
Don't have an account on the Atmosphere?
-
<a href="https://bsky.app">Sign up for Bluesky</a> to create one now!
-
</div>
+
+
<a href="/signup" class="button signup-cta">
+
Login or Sign up with a ${env.PDS_OWNER} account
+
</a>
+
+
${error ? html`<p>Error: <i>${error}</i></p>` : undefined}
</div>
</div>`
}
+3 -2
src/pages/public/styles.css
···
.signup-cta {
text-align: center;
-
text-wrap: balance;
+
width: 100%;
+
display: block;
margin-top: 1rem;
-
}
+
}
+29 -5
src/routes.ts
···
import { Agent } from '@atproto/api'
import { TID } from '@atproto/common'
import { OAuthResolverError } from '@atproto/oauth-client-node'
-
import { isValidHandle } from '@atproto/syntax'
import express, { Request, Response } from 'express'
import { getIronSession } from 'iron-session'
import assert from 'node:assert'
···
express.urlencoded({ extended: true }),
handler(async (req: Request, res: Response) => {
// Validate
-
const handle = req.body?.handle
-
if (typeof handle !== 'string' || !isValidHandle(handle)) {
+
const input = req.body?.input
+
if (typeof input !== 'string') {
return void res
.type('html')
-
.send(page(login({ error: 'invalid handle' })))
+
.send(page(login({ error: 'invalid input' })))
}
// Initiate the OAuth flow
try {
-
const url = await ctx.oauthClient.authorize(handle, {
+
const url = await ctx.oauthClient.authorize(input, {
+
scope: 'atproto transition:generic',
+
})
+
res.redirect(url.toString())
+
} catch (err) {
+
ctx.logger.error({ err }, 'oauth authorize failed')
+
res.type('html').send(
+
page(
+
login({
+
error:
+
err instanceof OAuthResolverError
+
? err.message
+
: "couldn't initiate login",
+
}),
+
),
+
)
+
}
+
}),
+
)
+
+
// Signup
+
app.get(
+
'/signup',
+
handler(async (req: Request, res: Response) => {
+
try {
+
const url = await ctx.oauthClient.authorize(env.PDS_URL, {
scope: 'atproto transition:generic',
})
res.redirect(url.toString())