Files
podsteadr/server/src/routes/auth.ts
T
ssmithxandClaude Sonnet 5 69241a28b1 feat(auth): add an admin-managed login allowlist
Lets the admin restrict which pubkeys may log in, enforced server-side
at /api/auth/login before a session is issued. Disabled by default;
the admin and the bootstrap (no-admin-claimed-yet) case always pass.
Manageable via the existing settings UI/API (npub or hex, one per line).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 18:33:21 +00:00

63 lines
2.1 KiB
TypeScript

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!),
};
});
}