fix: multiple federation peering bugs
1. queueOutboxEvent no longer creates pending peer placeholders when autoAcceptPeering is disabled — prevents bypassing the admin's peering control 2. Approval endpoint checks for 202 before response.ok — when the remote also has autoAcceptPeering off, sets peer to awaiting_approval instead of incorrectly activating it 3. awaiting_approval status added to Federation panel UI — status label, colors, filter options so these peers are visible and manageable
This commit is contained in:
@@ -996,6 +996,24 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
signal: AbortSignal.timeout(10_000),
|
signal: AbortSignal.timeout(10_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.status === 202) {
|
||||||
|
// Remote instance also has autoAcceptPeering off — they queued our request.
|
||||||
|
// Don't activate our peer. Set to awaiting_approval until their admin also approves.
|
||||||
|
db.update(schema.federationPeers)
|
||||||
|
.set({ status: 'awaiting_approval' })
|
||||||
|
.where(eq(schema.federationPeers.id, peerId))
|
||||||
|
.run();
|
||||||
|
// Delete the approval request since we already acted on it
|
||||||
|
db.delete(schema.peerApprovalRequests)
|
||||||
|
.where(eq(schema.peerApprovalRequests.id, id))
|
||||||
|
.run();
|
||||||
|
return reply.code(200).send({
|
||||||
|
success: true,
|
||||||
|
awaitingRemoteApproval: true,
|
||||||
|
message: 'Remote instance also requires admin approval. Your request has been queued on their side.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorMessage = `Remote instance rejected handshake (HTTP ${response.status})`;
|
let errorMessage = `Remote instance rejected handshake (HTTP ${response.status})`;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -152,12 +152,23 @@ export function queueOutboxEvent(
|
|||||||
: peers;
|
: peers;
|
||||||
|
|
||||||
// For targeted origins with no existing peer record, create pending placeholders
|
// For targeted origins with no existing peer record, create pending placeholders
|
||||||
|
// (only when autoAcceptPeering is enabled — otherwise the admin controls all peering)
|
||||||
if (targetPeerOrigins) {
|
if (targetPeerOrigins) {
|
||||||
|
const autoAcceptSettings = db
|
||||||
|
.select({ autoAcceptPeering: schema.instanceSettings.autoAcceptPeering })
|
||||||
|
.from(schema.instanceSettings)
|
||||||
|
.where(eq(schema.instanceSettings.id, 1))
|
||||||
|
.get();
|
||||||
|
const autoAcceptPeering = (autoAcceptSettings?.autoAcceptPeering ?? 1) === 1;
|
||||||
|
|
||||||
const matchedOrigins = new Set(matchedPeers.map(p => p.origin));
|
const matchedOrigins = new Set(matchedPeers.map(p => p.origin));
|
||||||
|
|
||||||
for (const origin of targetPeerOrigins) {
|
for (const origin of targetPeerOrigins) {
|
||||||
if (matchedOrigins.has(origin)) continue;
|
if (matchedOrigins.has(origin)) continue;
|
||||||
|
|
||||||
|
// Don't auto-create placeholders when autoAcceptPeering is off
|
||||||
|
if (!autoAcceptPeering) continue;
|
||||||
|
|
||||||
// Check if there's a rejected/revoked peer we should skip
|
// Check if there's a rejected/revoked peer we should skip
|
||||||
const existingPeer = db
|
const existingPeer = db
|
||||||
.select({ status: schema.federationPeers.status })
|
.select({ status: schema.federationPeers.status })
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ function peerStatusColor(status: string): string {
|
|||||||
case 'pending': return 'bg-accent-lavender/15 text-accent-lavender';
|
case 'pending': return 'bg-accent-lavender/15 text-accent-lavender';
|
||||||
case 'unreachable': return 'bg-accent-amber/15 text-accent-amber';
|
case 'unreachable': return 'bg-accent-amber/15 text-accent-amber';
|
||||||
case 'rejected': return 'bg-accent-rose/15 text-accent-rose';
|
case 'rejected': return 'bg-accent-rose/15 text-accent-rose';
|
||||||
|
case 'awaiting_approval': return 'bg-accent-amber/15 text-accent-amber';
|
||||||
case 'revoked': return 'bg-white/5 text-txt-tertiary';
|
case 'revoked': return 'bg-white/5 text-txt-tertiary';
|
||||||
default: return 'bg-white/5 text-txt-tertiary';
|
default: return 'bg-white/5 text-txt-tertiary';
|
||||||
}
|
}
|
||||||
@@ -184,6 +185,7 @@ function peerStatusDotColor(status: string): string {
|
|||||||
case 'pending': return 'bg-accent-lavender';
|
case 'pending': return 'bg-accent-lavender';
|
||||||
case 'unreachable': return 'bg-accent-amber';
|
case 'unreachable': return 'bg-accent-amber';
|
||||||
case 'rejected': return 'bg-accent-rose';
|
case 'rejected': return 'bg-accent-rose';
|
||||||
|
case 'awaiting_approval': return 'bg-accent-amber';
|
||||||
default: return 'bg-txt-tertiary';
|
default: return 'bg-txt-tertiary';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,13 +197,14 @@ function peerStatusLabel(status: string): string {
|
|||||||
case 'unreachable': return 'Unreachable';
|
case 'unreachable': return 'Unreachable';
|
||||||
case 'rejected': return 'Rejected (auto-peering denied)';
|
case 'rejected': return 'Rejected (auto-peering denied)';
|
||||||
case 'revoked': return 'Revoked';
|
case 'revoked': return 'Revoked';
|
||||||
|
case 'awaiting_approval': return 'Awaiting Approval';
|
||||||
default: return status;
|
default: return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerView = 'active' | 'revoked';
|
type PeerView = 'active' | 'revoked';
|
||||||
type SortBy = 'name' | 'lastSeen' | 'dateAdded' | 'failures';
|
type SortBy = 'name' | 'lastSeen' | 'dateAdded' | 'failures';
|
||||||
type StatusFilter = 'active' | 'unreachable' | 'pending' | 'rejected';
|
type StatusFilter = 'active' | 'unreachable' | 'pending' | 'rejected' | 'awaiting_approval';
|
||||||
|
|
||||||
// ─── Filter Dropdown ─────────────────────────────────────────────────────────
|
// ─── Filter Dropdown ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -263,7 +266,7 @@ function FilterDropdown({
|
|||||||
{view === 'active' && (
|
{view === 'active' && (
|
||||||
<>
|
<>
|
||||||
<div className="text-[10px] font-semibold text-txt-tertiary uppercase tracking-wider px-2 py-1">Status</div>
|
<div className="text-[10px] font-semibold text-txt-tertiary uppercase tracking-wider px-2 py-1">Status</div>
|
||||||
{(['active', 'unreachable', 'pending', 'rejected'] as StatusFilter[]).map((s) => (
|
{(['active', 'unreachable', 'pending', 'rejected', 'awaiting_approval'] as StatusFilter[]).map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -273,7 +276,7 @@ function FilterDropdown({
|
|||||||
} hover:bg-white/[0.06] transition-colors`}
|
} hover:bg-white/[0.06] transition-colors`}
|
||||||
>
|
>
|
||||||
<div className={`w-2 h-2 rounded-full ${peerStatusDotColor(s)}`} />
|
<div className={`w-2 h-2 rounded-full ${peerStatusDotColor(s)}`} />
|
||||||
<span className="capitalize">{s}</span>
|
<span className="capitalize">{s === 'awaiting_approval' ? 'Awaiting Approval' : s}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<div className="h-px bg-white/[0.06] my-1" />
|
<div className="h-px bg-white/[0.06] my-1" />
|
||||||
@@ -734,7 +737,7 @@ export function FederationPanel({ onApprovalCountChange }: { onApprovalCountChan
|
|||||||
const [peersLoading, setPeersLoading] = useState(false);
|
const [peersLoading, setPeersLoading] = useState(false);
|
||||||
const [peersError, setPeersError] = useState('');
|
const [peersError, setPeersError] = useState('');
|
||||||
const [view, setView] = useState<PeerView>('active');
|
const [view, setView] = useState<PeerView>('active');
|
||||||
const [statusFilter, setStatusFilter] = useState<Set<StatusFilter>>(new Set(['active', 'unreachable', 'pending', 'rejected']));
|
const [statusFilter, setStatusFilter] = useState<Set<StatusFilter>>(new Set(['active', 'unreachable', 'pending', 'rejected', 'awaiting_approval']));
|
||||||
const [sortBy, setSortBy] = useState<SortBy>('name');
|
const [sortBy, setSortBy] = useState<SortBy>('name');
|
||||||
const [expandedPeerId, setExpandedPeerId] = useState<string | null>(null);
|
const [expandedPeerId, setExpandedPeerId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user