feat: account deletion, username reuse, and real-time username availability

- Add account deletion with tombstone (isDeleted flag), password/username
  confirmation, owned-space guard, and full cleanup transaction
- Free deleted usernames by renaming to !deleted:<id> so they can be reused
- Add migration to retroactively free usernames from already-tombstoned users
- Add GET /api/auth/check-username endpoint with rate limiting for real-time
  availability checking during registration
- Add debounced username availability indicator on registration Step 1
- Add DeleteAccountModal with federation-aware remote account cleanup
- Add federation ops utility for remote instance management
- Update sanitizeUser to anonymize deleted user profiles
- Add instance store improvements and connected instances modal updates
This commit is contained in:
Jannis Braun
2026-03-11 16:29:25 +01:00
parent c8e2945c07
commit 8c8767ba2c
18 changed files with 1343 additions and 21 deletions
+48
View File
@@ -953,6 +953,54 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
return reply.code(200).send({ success: true });
});
// PATCH /api/spaces/:id/transfer-ownership — Transfer space ownership
app.patch<{ Params: { id: string }; Body: { newOwnerId: string } }>('/api/spaces/:id/transfer-ownership', {
preHandler: authenticate,
}, async (request, reply) => {
const { id } = request.params;
const { newOwnerId } = request.body;
const db = getDb();
if (!newOwnerId || typeof newOwnerId !== 'string') {
return reply.code(400).send({ error: 'newOwnerId is required', statusCode: 400 });
}
const server = db.select().from(schema.spaces).where(eq(schema.spaces.id, id)).get();
if (!server) {
return reply.code(404).send({ error: 'Space not found', statusCode: 404 });
}
if (!isSpaceOwner(id, request.userId)) {
return reply.code(403).send({ error: 'Only the space owner can transfer ownership', statusCode: 403 });
}
if (newOwnerId === request.userId) {
return reply.code(400).send({ error: 'You are already the owner', statusCode: 400 });
}
// Verify new owner is a member
if (!isMember(id, newOwnerId)) {
return reply.code(400).send({ error: 'New owner must be a member of the space', statusCode: 400 });
}
db.update(schema.spaces).set({ ownerId: newOwnerId }).where(eq(schema.spaces.id, id)).run();
const updated = db.select().from(schema.spaces).where(eq(schema.spaces.id, id)).get();
if (!updated) {
return reply.code(500).send({ error: 'Failed to transfer ownership', statusCode: 500 });
}
const spaceData = rowToSpace(updated);
// Broadcast space_updated so all clients see the new owner
connectionManager.sendToSpace(id, {
type: 'space_updated',
space: spaceData,
});
return reply.code(200).send(spaceData);
});
// ─── Ban Management ───────────────────────────────────────────────────────
// GET /api/spaces/:id/bans - List bans