import type { FastifyInstance } from 'fastify'; import { Nip98Error } from '../services/nip98.js'; import type { Team, User } from '../types.js'; function nowSecs(): number { return Math.floor(Date.now() / 1000); } export default async function authRoutes(app: FastifyInstance) { const { db } = 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 = ? WHERE pubkey = ?'); const setTeam = db.prepare('UPDATE users SET team = ? WHERE pubkey = ? AND team IS NULL'); 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; } upsertUser.run(pubkey, nowSecs(), nowSecs()); const body = req.body as { displayName?: string } | null; if (body?.displayName) { updateProfile.run(body.displayName, pubkey); } app.createSession(pubkey, reply); const user = selectUser.get(pubkey) as User; return { pubkey, team: user.team }; }); 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, team: user?.team ?? null, }; }); // Faction choice is one-way once made — matches Ingress not letting you swap sides // on a whim. If this ever needs to change, it should be an explicit admin action, // not a self-service re-pick. app.post('/api/auth/team', { preHandler: app.requireAuth }, async (req, reply) => { const body = req.body as { team?: Team }; if (body?.team !== 'orange' && body?.team !== 'green') { return reply.code(400).send({ error: 'team must be "orange" or "green"' }); } const result = setTeam.run(body.team, req.userPubkey); if (result.changes === 0) { const user = selectUser.get(req.userPubkey) as User; if (user.team) return reply.code(409).send({ error: `already on team ${user.team}` }); return reply.code(500).send({ error: 'failed to set team' }); } return { pubkey: req.userPubkey, team: body.team }; }); }