feat: auto-provision Matrix Space per org + migration 021 + /api/matrix-space endpoint
This commit is contained in:
@@ -164,6 +164,9 @@
|
|||||||
accessToken: credentials.accessToken,
|
accessToken: credentials.accessToken,
|
||||||
deviceId: credentials.deviceId || null,
|
deviceId: credentials.deviceId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if org has a Matrix Space, auto-create if not
|
||||||
|
await ensureOrgSpace(credentials);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.error("Failed to init Matrix client:", e);
|
console.error("Failed to init Matrix client:", e);
|
||||||
toasts.error("Failed to connect to chat. Please re-login.");
|
toasts.error("Failed to connect to chat. Please re-login.");
|
||||||
@@ -173,6 +176,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureOrgSpace(credentials: LoginCredentials) {
|
||||||
|
try {
|
||||||
|
const spaceRes = await fetch(`/api/matrix-space?org_id=${data.org.id}`);
|
||||||
|
const spaceResult = await spaceRes.json();
|
||||||
|
|
||||||
|
if (!spaceResult.spaceId) {
|
||||||
|
// No Space yet — create one using the user's credentials
|
||||||
|
const createRes = await fetch("/api/matrix-space", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
org_id: data.org.id,
|
||||||
|
action: "create",
|
||||||
|
homeserver_url: credentials.homeserverUrl,
|
||||||
|
access_token: credentials.accessToken,
|
||||||
|
org_name: data.org.name,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const createResult = await createRes.json();
|
||||||
|
if (createResult.spaceId) {
|
||||||
|
toasts.success(`Organization space created`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to ensure org space:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMatrixLogin() {
|
async function handleMatrixLogin() {
|
||||||
if (!matrixUsername.trim() || !matrixPassword.trim()) {
|
if (!matrixUsername.trim() || !matrixPassword.trim()) {
|
||||||
toasts.error("Please enter username and password");
|
toasts.error("Please enter username and password");
|
||||||
|
|||||||
172
src/routes/api/matrix-space/+server.ts
Normal file
172
src/routes/api/matrix-space/+server.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
// Cast supabase to any — matrix_space_id column added in migration 021
|
||||||
|
// TODO: Remove after running `supabase gen types`
|
||||||
|
const db = (supabase: any) => supabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: Retrieve the Matrix Space ID for an org
|
||||||
|
*/
|
||||||
|
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||||
|
const session = await locals.safeGetSession();
|
||||||
|
if (!session.user) {
|
||||||
|
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = url.searchParams.get('org_id');
|
||||||
|
if (!orgId) {
|
||||||
|
return json({ error: 'org_id is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await db(locals.supabase)
|
||||||
|
.from('organizations')
|
||||||
|
.select('matrix_space_id')
|
||||||
|
.eq('id', orgId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ spaceId: data?.matrix_space_id ?? null });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: Create a Matrix Space for an org, or link an existing one.
|
||||||
|
*
|
||||||
|
* Body options:
|
||||||
|
* - { org_id, action: "create", homeserver_url, access_token, org_name }
|
||||||
|
* Creates a new Space on the homeserver and stores the ID.
|
||||||
|
* - { org_id, action: "link", space_id }
|
||||||
|
* Links an existing Matrix Space ID to the org.
|
||||||
|
*/
|
||||||
|
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, action } = body;
|
||||||
|
|
||||||
|
if (!org_id || !action) {
|
||||||
|
return json({ error: 'org_id and action are required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'create') {
|
||||||
|
const { homeserver_url, access_token, org_name } = body;
|
||||||
|
if (!homeserver_url || !access_token || !org_name) {
|
||||||
|
return json({ error: 'homeserver_url, access_token, and org_name are required for create' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a Matrix Space via the Client-Server API
|
||||||
|
const createRes = await fetch(`${homeserver_url}/_matrix/client/v3/createRoom`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: org_name,
|
||||||
|
topic: `Organization space for ${org_name}`,
|
||||||
|
visibility: 'private',
|
||||||
|
creation_content: {
|
||||||
|
type: 'm.space',
|
||||||
|
},
|
||||||
|
initial_state: [
|
||||||
|
{
|
||||||
|
type: 'm.room.guest_access',
|
||||||
|
state_key: '',
|
||||||
|
content: { guest_access: 'can_join' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
power_level_content_override: {
|
||||||
|
invite: 50,
|
||||||
|
kick: 50,
|
||||||
|
ban: 50,
|
||||||
|
events_default: 0,
|
||||||
|
state_default: 50,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createRes.ok) {
|
||||||
|
const err = await createRes.json().catch(() => ({}));
|
||||||
|
return json({ error: err.error || 'Failed to create Matrix Space' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { room_id: spaceId } = await createRes.json();
|
||||||
|
|
||||||
|
// Also create default #general room inside the space
|
||||||
|
const generalRes = await fetch(`${homeserver_url}/_matrix/client/v3/createRoom`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'General',
|
||||||
|
topic: 'General discussion',
|
||||||
|
visibility: 'private',
|
||||||
|
preset: 'private_chat',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (generalRes.ok) {
|
||||||
|
const { room_id: generalRoomId } = await generalRes.json();
|
||||||
|
|
||||||
|
// Add #general as a child of the space
|
||||||
|
await fetch(
|
||||||
|
`${homeserver_url}/_matrix/client/v3/rooms/${encodeURIComponent(spaceId)}/state/m.space.child/${encodeURIComponent(generalRoomId)}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${access_token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
via: [new URL(homeserver_url).hostname],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store space ID in org record
|
||||||
|
const { error: updateError } = await db(locals.supabase)
|
||||||
|
.from('organizations')
|
||||||
|
.update({ matrix_space_id: spaceId })
|
||||||
|
.eq('id', org_id);
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
return json({ error: updateError.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ spaceId, created: true });
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to create Matrix Space:', e);
|
||||||
|
return json({ error: e.message || 'Failed to create Matrix Space' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'link') {
|
||||||
|
const { space_id } = body;
|
||||||
|
if (!space_id) {
|
||||||
|
return json({ error: 'space_id is required for link action' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: updateError } = await db(locals.supabase)
|
||||||
|
.from('organizations')
|
||||||
|
.update({ matrix_space_id: space_id })
|
||||||
|
.eq('id', org_id);
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
return json({ error: updateError.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ spaceId: space_id, linked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({ error: 'Invalid action. Use "create" or "link".' }, { status: 400 });
|
||||||
|
};
|
||||||
2
supabase/migrations/021_org_matrix_space.sql
Normal file
2
supabase/migrations/021_org_matrix_space.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add Matrix Space ID to organizations for org <-> space mapping
|
||||||
|
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS matrix_space_id TEXT;
|
||||||
Reference in New Issue
Block a user