diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 6096423..971ae5e 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -9,6 +9,7 @@ import { testWebhook } from '../engine/webhook-test.js' import { rateLimit } from '../middleware/rate-limit.js' import { isCreatorPubkey } from '../lib/constants.js' import { loginSchema, registerSchema, registerHumanSchema, updateBotSchema, pubkeySchema, formatZodError } from '../lib/validators.js' +import { extractPubkeyFromAuth } from '../middleware/jwt.js' export const authRouter = new Hono() @@ -25,6 +26,69 @@ authRouter.get("/check-name/:name", async (c) => { return c.json({ available: existing.length === 0 }) }) +// GET /me — restore the caller's own identity from their JWT alone. +// This is the ONLY session-restore path: it derives the pubkey from a +// verified, non-expired, non-blacklisted Bearer token (extractPubkeyFromAuth +// delegates to verifyJwt, which covers all of those cases) and never trusts +// a client-claimed pubkey. Read-only — performs no writes of any kind. +// Covered by global /api/* rate limiting (see app.ts); no per-route limiter +// needed for a session-restore call issued on every page load. +authRouter.get('/me', async (c) => { + const pubkey = extractPubkeyFromAuth(c.req.header('Authorization')) + if (!pubkey) { + return c.json({ error: 'Authentication required.' }, 401) + } + + const rows = await db.select({ + id: schema.bots.id, + name: schema.bots.name, + avatarSeed: schema.bots.avatarSeed, + archetype: schema.bots.archetype, + profilePicUrl: schema.bots.profilePicUrl, + eloRating: schema.bots.eloRating, + wins: schema.bots.wins, + losses: schema.bots.losses, + winStreak: schema.bots.winStreak, + bestStreak: schema.bots.bestStreak, + tier: schema.bots.tier, + isActive: schema.bots.isActive, + customization: schema.bots.customization, + webhookUrl: schema.bots.webhookUrl, + satsWon: schema.bots.satsWon, + satsWagered: schema.bots.satsWagered, + }).from(schema.bots).where(eq(schema.bots.publicKey, pubkey)).limit(1) + + if (rows.length === 0) { + return c.json({ exists: false }) + } + + const bot = rows[0] + const isHuman = bot.webhookUrl === 'http://human.local/' + + return c.json({ + exists: true, + bot: { + id: bot.id, + name: bot.name, + avatarSeed: bot.avatarSeed, + archetype: bot.archetype, + profilePicUrl: bot.profilePicUrl, + eloRating: bot.eloRating, + wins: bot.wins, + losses: bot.losses, + winStreak: bot.winStreak, + bestStreak: bot.bestStreak, + tier: bot.tier, + isActive: bot.isActive, + isHuman, + customization: bot.customization ? JSON.parse(bot.customization) : null, + satsWon: bot.satsWon ?? 0, + satsWagered: bot.satsWagered ?? 0, + hasWallet: false, + }, + }) +}) + // Login with Nostr pubkey (rate limited: 10 per minute per IP) authRouter.post('/login', rateLimit(60_000, 10), async (c) => { const parsed = loginSchema.safeParse(await c.req.json().catch(() => ({}))) @@ -369,7 +433,7 @@ authRouter.post('/update', rateLimit(60_000, 10), async (c) => { // --- NIP-98 Authenticated Session --- import { verifyNip98Token } from '../middleware/nip98.js' -import { createJwt, extractPubkeyFromAuth } from '../middleware/jwt.js' +import { createJwt } from '../middleware/jwt.js' // POST /nostr/session — authenticate with NIP-98, receive JWT authRouter.post('/nostr/session', rateLimit(60_000, 10), async (c) => {