feat: add space visibility/description, instance re-auth, and per-channel permissions

- Accept visibility and description fields when creating spaces
- Register creator in connectionManager on space creation for immediate WS broadcasts
- Return per-channel myPermissions and space-level myPermissions from GET /spaces/:id
- Populate permission maps in spaceStore from REST response
- Add reauthenticateInstance flow for tokenless federation placeholders
- Handle expired/missing tokens gracefully in autoConnectAll with visible error state
- Guard syncInstanceList against premature runs before autoConnectAll completes
This commit is contained in:
Jannis Braun
2026-03-08 22:08:48 +01:00
parent 0aba5ef8c1
commit a7b5d819bf
5 changed files with 325 additions and 139 deletions
+28 -6
View File
@@ -54,7 +54,7 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
app.post<{ Body: CreateSpaceRequest }>('/api/spaces', {
preHandler: authenticate,
}, async (request, reply) => {
const { name, icon } = request.body;
const { name, icon, visibility, description } = request.body;
if (!name || typeof name !== 'string') {
return reply.code(400).send({ error: 'Space name is required', statusCode: 400 });
@@ -65,6 +65,13 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
return reply.code(400).send({ error: 'Space name must be between 1 and 100 characters', statusCode: 400 });
}
// Validate visibility
const validVisibilities = ['public', 'request', 'private'];
const safeVisibility = visibility && validVisibilities.includes(visibility) ? visibility : 'private';
// Validate description
const safeDescription = description ? description.trim().slice(0, 200) || null : null;
const db = getDb();
const spaceId = generateSnowflake();
const channelId = generateSnowflake();
@@ -79,6 +86,8 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
icon: icon ?? null,
ownerId: request.userId,
inviteCode,
visibility: safeVisibility,
description: safeDescription,
createdAt: now,
}).run();
@@ -114,6 +123,9 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
return reply.code(500).send({ error: 'Failed to create space', statusCode: 500 });
}
// Register the creator in connectionManager so they receive WS broadcasts for this space
connectionManager.addUserSpace(request.userId, spaceId);
return reply.code(201).send(rowToSpace(server));
});
@@ -216,15 +228,24 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
})
.filter((m): m is MemberWithUser => m !== null);
// Filter channels by VIEW_CHANNEL permission before returning
const visibleChannels = channels.filter(ch => {
// Compute space-level permissions for the requesting user
const spacePerms = computePermissions(request.userId, id);
// Filter channels by VIEW_CHANNEL permission and attach per-channel myPermissions
const visibleChannels: (Channel & { myPermissions: string })[] = [];
for (const ch of channels) {
const perms = computePermissions(request.userId, id, ch.id);
return (perms & PermissionBits.VIEW_CHANNEL) !== 0n;
});
if ((perms & PermissionBits.VIEW_CHANNEL) !== 0n) {
visibleChannels.push({
...rowToChannel(ch),
myPermissions: permissionsToString(perms),
});
}
}
const result: SpaceWithChannelsAndMembers = {
...rowToSpace(server),
channels: visibleChannels.map(rowToChannel),
channels: visibleChannels,
members,
roles: roles.map(r => ({
id: r.id,
@@ -234,6 +255,7 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
position: r.position ?? 0,
createdAt: r.createdAt,
})),
myPermissions: permissionsToString(spacePerms),
};
return reply.code(200).send(result);