feat: Matrix Space membership sync API (invite/kick members from org space + child rooms)

This commit is contained in:
AlacrisDevs
2026-02-07 02:02:11 +02:00
parent 23035b6ab4
commit 45ab939b7f

View File

@@ -0,0 +1,186 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
const db = (supabase: any) => supabase;
/**
* POST: Invite a user to the org's Matrix Space (and its child rooms).
*
* Body: { org_id, matrix_user_id, homeserver_url, access_token }
*/
export const POST: RequestHandler = async ({ request, locals }) => {
const session = await locals.safeGetSession();
if (!session.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { org_id, matrix_user_id, homeserver_url, access_token } = body;
if (!org_id || !matrix_user_id || !homeserver_url || !access_token) {
return json({ error: 'Missing required fields' }, { status: 400 });
}
// Get org's Matrix Space ID
const { data: org } = await db(locals.supabase)
.from('organizations')
.select('matrix_space_id')
.eq('id', org_id)
.single();
if (!org?.matrix_space_id) {
return json({ error: 'Organization has no Matrix Space' }, { status: 404 });
}
const spaceId = org.matrix_space_id;
const errors: string[] = [];
// Invite to the Space itself
const inviteRes = await matrixInvite(homeserver_url, access_token, spaceId, matrix_user_id);
if (!inviteRes.ok) {
const err = await inviteRes.json().catch(() => ({}));
// M_FORBIDDEN means already joined, which is fine
if (err.errcode !== 'M_FORBIDDEN') {
errors.push(`Space invite: ${err.error || 'failed'}`);
}
}
// Also invite to all child rooms of the space
try {
const stateRes = await fetch(
`${homeserver_url}/_matrix/client/v3/rooms/${encodeURIComponent(spaceId)}/state`,
{
headers: { 'Authorization': `Bearer ${access_token}` },
}
);
if (stateRes.ok) {
const stateEvents = await stateRes.json();
const childRoomIds = stateEvents
.filter((e: any) => e.type === 'm.space.child' && e.content?.via)
.map((e: any) => e.state_key);
for (const childRoomId of childRoomIds) {
const childInvite = await matrixInvite(homeserver_url, access_token, childRoomId, matrix_user_id);
if (!childInvite.ok) {
const err = await childInvite.json().catch(() => ({}));
if (err.errcode !== 'M_FORBIDDEN') {
errors.push(`Room ${childRoomId}: ${err.error || 'failed'}`);
}
}
}
}
} catch (e) {
errors.push('Failed to fetch space children');
}
return json({
success: errors.length === 0,
invited: matrix_user_id,
spaceId,
errors: errors.length > 0 ? errors : undefined,
});
};
/**
* DELETE: Kick a user from the org's Matrix Space (and its child rooms).
*
* Query: ?org_id=...&matrix_user_id=...&homeserver_url=...&access_token=...
*/
export const DELETE: RequestHandler = async ({ url, locals }) => {
const session = await locals.safeGetSession();
if (!session.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const org_id = url.searchParams.get('org_id');
const matrix_user_id = url.searchParams.get('matrix_user_id');
const homeserver_url = url.searchParams.get('homeserver_url');
const access_token = url.searchParams.get('access_token');
if (!org_id || !matrix_user_id || !homeserver_url || !access_token) {
return json({ error: 'Missing required fields' }, { status: 400 });
}
const { data: org } = await db(locals.supabase)
.from('organizations')
.select('matrix_space_id')
.eq('id', org_id)
.single();
if (!org?.matrix_space_id) {
return json({ error: 'Organization has no Matrix Space' }, { status: 404 });
}
const spaceId = org.matrix_space_id;
const errors: string[] = [];
// Kick from child rooms first, then from the space
try {
const stateRes = await fetch(
`${homeserver_url}/_matrix/client/v3/rooms/${encodeURIComponent(spaceId)}/state`,
{
headers: { 'Authorization': `Bearer ${access_token}` },
}
);
if (stateRes.ok) {
const stateEvents = await stateRes.json();
const childRoomIds = stateEvents
.filter((e: any) => e.type === 'm.space.child' && e.content?.via)
.map((e: any) => e.state_key);
for (const childRoomId of childRoomIds) {
const kickRes = await matrixKick(homeserver_url, access_token, childRoomId, matrix_user_id);
if (!kickRes.ok) {
const err = await kickRes.json().catch(() => ({}));
if (err.errcode !== 'M_FORBIDDEN') {
errors.push(`Room ${childRoomId}: ${err.error || 'failed'}`);
}
}
}
}
} catch (e) {
errors.push('Failed to fetch space children');
}
// Kick from the space itself
const kickRes = await matrixKick(homeserver_url, access_token, spaceId, matrix_user_id);
if (!kickRes.ok) {
const err = await kickRes.json().catch(() => ({}));
if (err.errcode !== 'M_FORBIDDEN') {
errors.push(`Space kick: ${err.error || 'failed'}`);
}
}
return json({
success: errors.length === 0,
kicked: matrix_user_id,
spaceId,
errors: errors.length > 0 ? errors : undefined,
});
};
// Helper: invite a user to a room
async function matrixInvite(homeserver: string, token: string, roomId: string, userId: string) {
return fetch(`${homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId }),
});
}
// Helper: kick a user from a room
async function matrixKick(homeserver: string, token: string, roomId: string, userId: string) {
return fetch(`${homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/kick`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId, reason: 'Removed from organization' }),
});
}