feat: lift DM gates for federated users
Remove requireLocalUser from DM routes, include DMs in federated ready payload, replace blanket dm_* WS gate with call-only blocklist. DM calls remain gated (separate scope).
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import { eq, and, or, desc, lt, inArray, isNull, sql } from 'drizzle-orm';
|
import { eq, and, or, desc, lt, inArray, isNull, sql } from 'drizzle-orm';
|
||||||
import { getDb, schema } from '../db/index.js';
|
import { getDb, schema } from '../db/index.js';
|
||||||
import { authenticate, requireLocalUser } from '../utils/auth.js';
|
import { authenticate } from '../utils/auth.js';
|
||||||
import { generateSnowflake } from '../utils/snowflake.js';
|
import { generateSnowflake } from '../utils/snowflake.js';
|
||||||
import { isDmMember } from '../utils/permissions.js';
|
import { isDmMember } from '../utils/permissions.js';
|
||||||
import { connectionManager } from '../ws/handler.js';
|
import { connectionManager } from '../ws/handler.js';
|
||||||
@@ -243,9 +243,8 @@ export function broadcastDmMessage(dmChannelId: string, message: DmMessageWithUs
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
||||||
// Centralized auth + federation gating for all DM routes
|
// Centralized auth for all DM routes
|
||||||
app.addHook('preHandler', authenticate);
|
app.addHook('preHandler', authenticate);
|
||||||
app.addHook('preHandler', requireLocalUser);
|
|
||||||
|
|
||||||
// GET /api/dm - List user's DM channels
|
// GET /api/dm - List user's DM channels
|
||||||
app.get('/api/dm', async (request, reply) => {
|
app.get('/api/dm', async (request, reply) => {
|
||||||
|
|||||||
@@ -145,11 +145,12 @@ export function handleClientEvent(
|
|||||||
): void {
|
): void {
|
||||||
const type = event.type as string;
|
const type = event.type as string;
|
||||||
|
|
||||||
// Federation gating: federated users must use their home instance for DM operations
|
// Federation gating: DM calls remain blocked for federated users (separate scope)
|
||||||
if (isFederated && type.startsWith('dm_')) {
|
const DM_CALL_EVENTS = ['dm_call_start', 'dm_call_accept', 'dm_call_reject', 'dm_call_end'];
|
||||||
|
if (isFederated && DM_CALL_EVENTS.includes(type)) {
|
||||||
connectionManager.sendToUser(userId, {
|
connectionManager.sendToUser(userId, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: 'Federated users must use their home instance for DM operations',
|
message: 'Federated users cannot use DM calls on remote instances',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1278,7 +1279,6 @@ function handleChannelAck(event: Record<string, unknown>, userId: string, isFede
|
|||||||
if (spaceId) {
|
if (spaceId) {
|
||||||
if (!isMember(spaceId, userId)) return;
|
if (!isMember(spaceId, userId)) return;
|
||||||
} else {
|
} else {
|
||||||
if (isFederated) return;
|
|
||||||
if (!isDmMember(channelId, userId)) return;
|
if (!isDmMember(channelId, userId)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1333,7 +1333,6 @@ function handleMarkUnread(event: Record<string, unknown>, userId: string, isFede
|
|||||||
if (spaceId) {
|
if (spaceId) {
|
||||||
if (!isMember(spaceId, userId)) return;
|
if (!isMember(spaceId, userId)) return;
|
||||||
} else {
|
} else {
|
||||||
if (isFederated) return;
|
|
||||||
if (!isDmMember(channelId, userId)) return;
|
if (!isDmMember(channelId, userId)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1108,8 +1108,8 @@ function buildReadyPayload(userId: string): {
|
|||||||
// Store user's space IDs for broadcasting
|
// Store user's space IDs for broadcasting
|
||||||
connectionManager.setUserSpaces(userId, spaceIds);
|
connectionManager.setUserSpaces(userId, spaceIds);
|
||||||
|
|
||||||
// Get DM channels — skip entirely for federated users (they get DMs from their home instance)
|
// Get DM channels
|
||||||
const dmMemberships = isFederated ? [] : db.select()
|
const dmMemberships = db.select()
|
||||||
.from(schema.dmMembers)
|
.from(schema.dmMembers)
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(schema.dmMembers.userId, userId),
|
eq(schema.dmMembers.userId, userId),
|
||||||
@@ -1198,6 +1198,7 @@ function buildReadyPayload(userId: string): {
|
|||||||
|
|
||||||
dmChannels.push({
|
dmChannels.push({
|
||||||
id: dmChannel.id,
|
id: dmChannel.id,
|
||||||
|
federatedId: dmChannel.federatedId ?? null,
|
||||||
ownerId: dmChannel.ownerId ?? null,
|
ownerId: dmChannel.ownerId ?? null,
|
||||||
createdAt: dmChannel.createdAt,
|
createdAt: dmChannel.createdAt,
|
||||||
members,
|
members,
|
||||||
@@ -1214,6 +1215,11 @@ function buildReadyPayload(userId: string): {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include DM channel IDs in the visible set for read state filtering
|
||||||
|
for (const dm of dmChannels) {
|
||||||
|
visibleChannelIdSet.add(dm.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Get Space Folders
|
// Get Space Folders
|
||||||
const folderRows = db.select()
|
const folderRows = db.select()
|
||||||
.from(schema.spaceFolders)
|
.from(schema.spaceFolders)
|
||||||
|
|||||||
Reference in New Issue
Block a user