import type { FastifyInstance } from 'fastify'; import { Nip98Error } from '../services/nip98.js'; import type { User } from '../types.js'; function nowSecs(): number { return Math.floor(Date.now() / 1000); } export default async function authRoutes(app: FastifyInstance) { const { db, settings } = app.ctx; const upsertUser = db.prepare(` INSERT INTO users (pubkey, created_at, last_login_at) VALUES (?, ?, ?) ON CONFLICT(pubkey) DO UPDATE SET last_login_at = excluded.last_login_at `); const selectUser = db.prepare('SELECT * FROM users WHERE pubkey = ?'); const updateProfile = db.prepare('UPDATE users SET display_name = ?, lud16 = ? WHERE pubkey = ?'); app.post('/api/auth/login', async (req, reply) => { let pubkey: string; try { pubkey = app.verifyNip98Request(req); } catch (err) { if (err instanceof Nip98Error) return reply.code(401).send({ error: err.message }); throw err; } if (!settings.isLoginAllowed(pubkey)) { return reply.code(403).send({ error: 'this account is not on the login allowlist' }); } upsertUser.run(pubkey, nowSecs(), nowSecs()); settings.claimAdminIfUnset(pubkey); // Optional profile hints from the client (it has the user's kind-0 metadata). const body = req.body as { displayName?: string; lud16?: string } | null; if (body && (body.displayName || body.lud16)) { const existing = selectUser.get(pubkey) as User; updateProfile.run( body.displayName ?? existing.display_name, body.lud16 ?? existing.lud16, pubkey, ); } app.createSession(pubkey, reply); return { pubkey, isAdmin: settings.isAdmin(pubkey) }; }); app.post('/api/auth/logout', async (req, reply) => { app.destroySession(req, reply); return { ok: true }; }); app.get('/api/auth/me', { preHandler: app.requireAuth }, async (req) => { const user = selectUser.get(req.userPubkey) as User | undefined; return { pubkey: req.userPubkey, displayName: user?.display_name ?? null, lud16: user?.lud16 ?? null, isAdmin: settings.isAdmin(req.userPubkey!), }; }); }