feat(server): podpuddle scaffold + Fastify backend (nostr auth, RSS, streams)
- docker-compose stack: podpuddle + MediaMTX (RTMP/WHIP/HLS) + blossom-server - NIP-98 nostr-only login with session cookies, replay guard, clock-skew window - podcasts/episodes CRUD; episodes register browser-uploaded blossom blobs - RSS 2.0 + itunes + podcast namespace feeds with lnaddress value blocks (podcast:guid UUIDv5 verified against the spec vector) - streams API with hashed stream keys; MediaMTX http-auth webhook (query/password/bearer forms); API poller flips live/ended status - NIP-53 kind 30311 live events published with the server's nostr identity - recordings: ffmpeg remux + server-key blossom upload → podcast episode - 35 vitest tests green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
}
|
||||
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!),
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user