Files
backspace/packages/server/src/routes/users.ts
T
Jannis Braun 5ef502f2e3 fix: repair invite links, social features, messaging + Discord UI overhaul
Phase 1 - Feature Repair:
- Fix member kick/leave: add missing db.delete() call in servers.ts
- Stabilize invite codes: return existing code instead of regenerating
- Fix user search: use LIKE instead of exact match in social.ts
- Wire DM button on FriendsPage to create/navigate to DM channels
- Add cancel outgoing friend request (DELETE endpoint + frontend)
- Add accept/decline friend request actions with WS real-time events
- Fix replyToId persistence in message creation
- Hydrate reactions and replyTo in message queries
- Add joinByCode to API client and serverStore
- Add friend_request_received/accepted WebSocket events

Phase 2 - Discord UI Overhaul:
- Remove stray borders between layout columns
- Replace shadow-sm with shadow-header on content headers
- Replace all bg-gray-*/text-gray-* with Discord color tokens
- Ensure flat color contrast (#1E1F22, #2B2D31, #313338)

Testing:
- Set up vitest + @testing-library/react + jsdom
- Add 17 tests across InviteModal, JoinServer, FriendsPage (all passing)
- Fix vite resolve.extensions to prefer .tsx over stale .js files
2026-02-18 05:34:45 +01:00

118 lines
3.9 KiB
TypeScript

import type { FastifyInstance } from 'fastify';
import { eq } from 'drizzle-orm';
import { getDb, schema } from '../db/index.js';
import { authenticate } from '../utils/auth.js';
import { connectionManager } from '../ws/handler.js';
import type { User, UpdateUserRequest } from '@opencord/shared';
function sanitizeUser(row: typeof schema.users.$inferSelect): User {
return {
id: row.id,
username: row.username,
displayName: row.displayName,
avatar: row.avatar,
status: (row.status ?? 'offline') as User['status'],
customStatus: row.customStatus,
createdAt: row.createdAt,
};
}
export async function userRoutes(app: FastifyInstance): Promise<void> {
app.get('/api/users/@me', { preHandler: authenticate }, async (request, reply) => {
const db = getDb();
const user = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
if (!user) {
return reply.code(404).send({ error: 'User not found', statusCode: 404 });
}
return reply.code(200).send(sanitizeUser(user));
});
app.patch<{ Body: UpdateUserRequest }>('/api/users/@me', { preHandler: authenticate }, async (request, reply) => {
const { displayName, avatar, customStatus, status } = request.body;
const db = getDb();
const updateData: Record<string, string | null | undefined> = {};
if (displayName !== undefined) {
if (displayName !== null && typeof displayName === 'string') {
const trimmed = displayName.trim();
if (trimmed.length > 32) {
return reply.code(400).send({ error: 'Display name must be 32 characters or less', statusCode: 400 });
}
updateData.displayName = trimmed || null;
} else {
updateData.displayName = null;
}
}
if (avatar !== undefined) {
updateData.avatar = avatar;
}
if (customStatus !== undefined) {
if (customStatus !== null && typeof customStatus === 'string') {
const trimmed = customStatus.trim();
if (trimmed.length > 128) {
return reply.code(400).send({ error: 'Custom status must be 128 characters or less', statusCode: 400 });
}
updateData.customStatus = trimmed || null;
} else {
updateData.customStatus = null;
}
}
if (status !== undefined) {
if (!['online', 'idle', 'dnd', 'offline'].includes(status)) {
return reply.code(400).send({ error: 'Invalid status', statusCode: 400 });
}
updateData.status = status;
}
if (Object.keys(updateData).length === 0) {
return reply.code(400).send({ error: 'No fields to update', statusCode: 400 });
}
db.update(schema.users).set(updateData).where(eq(schema.users.id, request.userId)).run();
const updatedUser = db.select().from(schema.users).where(eq(schema.users.id, request.userId)).get();
if (!updatedUser) {
return reply.code(404).send({ error: 'User not found', statusCode: 404 });
}
const sanitized = sanitizeUser(updatedUser);
// Broadcast presence update if status changed
if (status !== undefined) {
const userServers = connectionManager.getUserServers(sanitized.id);
for (const serverId of userServers) {
connectionManager.sendToServer(serverId, {
type: 'presence_update',
userId: sanitized.id,
status: status,
}, sanitized.id);
}
connectionManager.sendToUser(sanitized.id, {
type: 'presence_update',
userId: sanitized.id,
status: status,
});
}
return reply.code(200).send(sanitized);
});
app.get<{ Params: { id: string } }>('/api/users/:id', { preHandler: authenticate }, async (request, reply) => {
const { id } = request.params;
const db = getDb();
const user = db.select().from(schema.users).where(eq(schema.users.id, id)).get();
if (!user) {
return reply.code(404).send({ error: 'User not found', statusCode: 404 });
}
return reply.code(200).send(sanitizeUser(user));
});
}