feat(federation): add POST /api/federation/peers/:id/reset

Admin-only endpoint for recovering from needs_attention. Deletes the
local peer row; FK cascade removes queued outbox entries. Gated to
peers in needs_attention to prevent accidental resets of healthy
peerings (use /peers/:id for revoke on active peers).

Also extends the Task 8.5 test mock of '../db/index.js' to re-export
`schema`. federation.ts imports `schema` from the re-export alongside
`getDb`; the previous mock only exposed `getDb`, causing the route
handler to blow up with 500s before reaching any assertion. This is
a scaffolding fix — no test assertions were changed.
This commit is contained in:
Jannis Braun
2026-04-21 20:56:26 +02:00
parent 54f32657e5
commit 0c2864a3d2
2 changed files with 41 additions and 0 deletions
@@ -16,8 +16,10 @@ let currentUserId = 'admin-user';
let currentUserIsAdmin = true; let currentUserIsAdmin = true;
// Mock getDb BEFORE importing the route module so the module reads our test DB. // Mock getDb BEFORE importing the route module so the module reads our test DB.
// Must also re-export `schema` because federation.ts imports it from '../db/index.js'.
vi.mock('../db/index.js', () => ({ vi.mock('../db/index.js', () => ({
getDb: () => testDb, getDb: () => testDb,
schema,
})); }));
// Mock the auth middleware to honour test-controlled currentUserId / currentUserIsAdmin. // Mock the auth middleware to honour test-controlled currentUserId / currentUserIsAdmin.
+39
View File
@@ -845,6 +845,45 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
}, },
); );
// ─── POST /api/federation/peers/:id/reset ──────────────────────────────────
// Admin-only: reset a peer that has transitioned to needs_attention.
// Deletes the local peer row (cascade-deletes outbox entries via FK).
// Admin must re-initiate peering out of band after reset.
app.post<{ Params: { id: string } }>(
'/api/federation/peers/:id/reset',
{ preHandler: [authenticate, requireAdmin] },
async (request, reply) => {
const { id } = request.params;
const db = getDb();
const peer = db
.select()
.from(schema.federationPeers)
.where(eq(schema.federationPeers.id, id))
.get();
if (!peer) {
return reply.code(404).send({ error: 'Peer not found', statusCode: 404 });
}
if (peer.status !== 'needs_attention') {
return reply.code(400).send({
error: 'Reset is only available for peers in the needs_attention state. Use revoke for active peers.',
statusCode: 400,
});
}
// Cascade-delete handles federation_outbox entries (FK onDelete: 'cascade').
db.delete(schema.federationPeers)
.where(eq(schema.federationPeers.id, id))
.run();
connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });
return reply.code(200).send({ success: true });
},
);
// ─── PATCH /api/federation/peers/:id ──────────────────────────────────────── // ─── PATCH /api/federation/peers/:id ────────────────────────────────────────
// Admin-only: update peer settings (e.g. auto-rotation interval). // Admin-only: update peer settings (e.g. auto-rotation interval).
app.patch<{ Params: { id: string }; Body: { autoRotateIntervalDays?: number } }>( app.patch<{ Params: { id: string }; Body: { autoRotateIntervalDays?: number } }>(