feat: public invite landing page with federation redirect

Make /join/:code a public route with a standalone JoinPage that shows a
space preview and handles authenticated, unauthenticated, and cross-instance
users. Adds GET /api/spaces/invite/:code/preview (no auth) endpoint,
?redirect= param support on login/register, and cleans up dead invite
handling from AppLayout and JoinSpace modal.
This commit is contained in:
Jannis Braun
2026-03-13 03:04:52 +01:00
parent 07ef49eac0
commit c3617de832
9 changed files with 484 additions and 29 deletions
+27
View File
@@ -1085,6 +1085,33 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
return reply.code(200).send(spaceData);
});
// GET /api/spaces/invite/:code/preview — Public invite preview (no auth)
app.get<{ Params: { code: string } }>('/api/spaces/invite/:code/preview', async (request, reply) => {
const { code } = request.params;
const db = getDb();
const space = db.select().from(schema.spaces).where(eq(schema.spaces.inviteCode, code)).get();
if (!space) {
return reply.code(404).send({ error: 'Invalid invite code', statusCode: 404 });
}
const memberCount = db.select().from(schema.spaceMembers)
.where(eq(schema.spaceMembers.spaceId, space.id))
.all().length;
const settings = db.select().from(schema.instanceSettings).where(eq(schema.instanceSettings.id, 1)).get();
const instanceName = settings?.instanceName ?? 'Backspace';
return reply.code(200).send({
spaceName: space.name,
description: space.description ?? null,
icon: space.icon ?? null,
avatarColor: space.avatarColor ?? null,
memberCount,
instanceName,
});
});
// ─── Ban Management ───────────────────────────────────────────────────────
// GET /api/spaces/:id/bans - List bans