diff --git a/backend/src/db.ts b/backend/src/db.ts index 0a49cea..8bedd01 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -86,10 +86,11 @@ export async function initDB() { CREATE INDEX IF NOT EXISTS idx_chat_members_user_id ON chat_members(user_id); `); - // Migrations: add avatar columns if not exist + // Migrations await pool.query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT DEFAULT NULL; ALTER TABLE chats ADD COLUMN IF NOT EXISTS avatar TEXT DEFAULT NULL; + ALTER TABLE users ADD COLUMN IF NOT EXISTS position TEXT DEFAULT NULL; `); // Seed admin if no users diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 2b3ddd8..cdf72bd 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -16,19 +16,20 @@ export default async function adminRoutes(app: FastifyInstance) { // List users app.get('/users', async () => { const { rows } = await pool.query( - `SELECT id, username, display_name, avatar_color, is_admin, is_active, last_seen, created_at + `SELECT id, username, display_name, avatar_color, is_admin, is_active, last_seen, created_at, phone, position FROM users ORDER BY created_at DESC` ); return rows.map(u => ({ id: u.id, username: u.username, displayName: u.display_name, avatarColor: u.avatar_color, isAdmin: u.is_admin, isActive: u.is_active, lastSeen: u.last_seen, createdAt: u.created_at, + phone: u.phone || '', position: u.position || '', })); }); // Create user app.post('/users', async (req, reply) => { - const { username, displayName, password, isAdmin } = req.body as any; + const { username, displayName, password, isAdmin, phone, position } = req.body as any; const { id: createdBy } = req.user as { id: string }; if (!username || !displayName || !password) { @@ -40,9 +41,9 @@ export default async function adminRoutes(app: FastifyInstance) { try { const { rows: [user] } = await pool.query( - `INSERT INTO users (username, display_name, password_hash, avatar_color, is_admin, created_by) - VALUES ($1,$2,$3,$4,$5,$6) RETURNING id, username, display_name, avatar_color, is_admin`, - [username, displayName, hash, color, !!isAdmin, createdBy] + `INSERT INTO users (username, display_name, password_hash, avatar_color, is_admin, phone, position, created_by) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id, username, display_name, avatar_color, is_admin`, + [username, displayName, hash, color, !!isAdmin, phone || '', position || null, createdBy] ); return reply.status(201).send({ id: user.id, username: user.username, displayName: user.display_name, @@ -57,7 +58,7 @@ export default async function adminRoutes(app: FastifyInstance) { // Update user app.put('/users/:id', async (req, reply) => { const { id } = req.params as { id: string }; - const { displayName, isAdmin, isActive, password } = req.body as any; + const { displayName, isAdmin, isActive, password, phone, position } = req.body as any; if (password) { const hash = await bcrypt.hash(password, 10); @@ -68,9 +69,11 @@ export default async function adminRoutes(app: FastifyInstance) { `UPDATE users SET display_name = COALESCE($1, display_name), is_admin = COALESCE($2, is_admin), - is_active = COALESCE($3, is_active) - WHERE id = $4`, - [displayName, isAdmin, isActive, id] + is_active = COALESCE($3, is_active), + phone = COALESCE($4, phone), + position = COALESCE($5, position) + WHERE id = $6`, + [displayName, isAdmin, isActive, phone, position, id] ); return { ok: true }; @@ -85,6 +88,29 @@ export default async function adminRoutes(app: FastifyInstance) { return { ok: true }; }); + // All chats (admin view) + app.get('/chats', async () => { + const { rows } = await pool.query(` + SELECT c.id, c.type, c.title, c.avatar_color, c.avatar, c.is_public, c.created_at, + (SELECT COUNT(*) FROM chat_members WHERE chat_id = c.id) AS member_count, + (SELECT COUNT(*) FROM messages WHERE chat_id = c.id AND is_deleted = FALSE) AS message_count + FROM chats c ORDER BY c.created_at DESC + `); + return rows.map(r => ({ + id: r.id, type: r.type, title: r.title, + avatarColor: r.avatar_color, avatar: r.avatar || null, isPublic: r.is_public, + memberCount: parseInt(r.member_count), messageCount: parseInt(r.message_count), + createdAt: r.created_at, + })); + }); + + // Delete chat (admin) + app.delete('/chats/:id', async (req) => { + const { id } = req.params as { id: string }; + await pool.query('DELETE FROM chats WHERE id = $1', [id]); + return { ok: true }; + }); + // Stats app.get('/stats', async () => { const [users, chats, messages] = await Promise.all([ diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 49e2179..a8675cb 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -9,16 +9,19 @@ function userDto(u: any) { return { id: u.id, username: u.username, displayName: u.display_name, avatarColor: u.avatar_color, avatar: u.avatar || null, - bio: u.bio, phone: u.phone, isAdmin: u.is_admin, + bio: u.bio, phone: u.phone, position: u.position || null, isAdmin: u.is_admin, }; } export default async function authRoutes(app: FastifyInstance) { app.post('/login', async (req, reply) => { - const { username, password } = req.body as { username: string; password: string }; + const { username, phone, password } = req.body as { username?: string; phone?: string; password: string }; + const login = phone || username || ''; const { rows: [user] } = await pool.query( - 'SELECT * FROM users WHERE username = $1 AND is_active = TRUE', [username] + `SELECT * FROM users WHERE is_active = TRUE AND ( + (phone != '' AND phone = $1) OR username = $1 + )`, [login] ); if (!user) return reply.status(401).send({ error: 'Неверный логин или пароль' }); @@ -35,14 +38,14 @@ export default async function authRoutes(app: FastifyInstance) { app.get('/me', { preHandler: [app.authenticate] }, async (req) => { const { id } = req.user as { id: string }; const { rows: [user] } = await pool.query( - 'SELECT id, username, display_name, avatar_color, avatar, bio, phone, is_admin, last_seen FROM users WHERE id = $1', [id] + 'SELECT id, username, display_name, avatar_color, avatar, bio, phone, position, is_admin, last_seen FROM users WHERE id = $1', [id] ); return { ...userDto(user), lastSeen: user.last_seen }; }); app.put('/me', { preHandler: [app.authenticate] }, async (req, reply) => { const { id } = req.user as { id: string }; - const { displayName, bio, phone, password, newPassword } = req.body as any; + const { displayName, bio, phone, position, password, newPassword } = req.body as any; if (newPassword) { const { rows: [user] } = await pool.query('SELECT password_hash FROM users WHERE id = $1', [id]); @@ -53,12 +56,12 @@ export default async function authRoutes(app: FastifyInstance) { } await pool.query( - 'UPDATE users SET display_name = COALESCE($1, display_name), bio = COALESCE($2, bio), phone = COALESCE($3, phone) WHERE id = $4', - [displayName, bio, phone, id] + 'UPDATE users SET display_name = COALESCE($1, display_name), bio = COALESCE($2, bio), phone = COALESCE($3, phone), position = COALESCE($4, position) WHERE id = $5', + [displayName, bio, phone, position, id] ); const { rows: [user] } = await pool.query( - 'SELECT id, username, display_name, avatar_color, avatar, bio, phone, is_admin FROM users WHERE id = $1', [id] + 'SELECT id, username, display_name, avatar_color, avatar, bio, phone, position, is_admin FROM users WHERE id = $1', [id] ); return userDto(user); }); diff --git a/backend/src/routes/chats.ts b/backend/src/routes/chats.ts index dffe6f8..90d876d 100644 --- a/backend/src/routes/chats.ts +++ b/backend/src/routes/chats.ts @@ -80,7 +80,7 @@ export default async function chatRoutes(app: FastifyInstance) { if (!chat) return reply.status(404).send({ error: 'Not found' }); const { rows: members } = await pool.query(` - SELECT u.id, u.username, u.display_name, u.avatar_color, u.avatar, u.last_seen, cm.role, cm.can_send_messages + SELECT u.id, u.username, u.display_name, u.avatar_color, u.avatar, u.position, u.last_seen, cm.role, cm.can_send_messages FROM chat_members cm JOIN users u ON u.id = cm.user_id WHERE cm.chat_id = $1 @@ -104,6 +104,7 @@ export default async function chatRoutes(app: FastifyInstance) { members: members.map(m => ({ id: m.id, username: m.username, displayName: m.display_name, avatarColor: m.avatar_color, avatar: m.avatar || null, + position: m.position || null, role: m.role, online: onlineSet.has(m.id), lastSeen: m.last_seen, canSendMessages: m.can_send_messages, })), diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts index edfd21a..585a747 100644 --- a/backend/src/routes/users.ts +++ b/backend/src/routes/users.ts @@ -11,9 +11,9 @@ export default async function userRoutes(app: FastifyInstance) { const { id: userId } = req.user as { id: string }; const { rows } = await pool.query( - `SELECT id, username, display_name, avatar_color, last_seen FROM users + `SELECT id, username, display_name, avatar_color, avatar, position, last_seen FROM users WHERE is_active = TRUE AND id != $1 - AND (username ILIKE $2 OR display_name ILIKE $2) + AND (username ILIKE $2 OR display_name ILIKE $2 OR phone ILIKE $2) ORDER BY display_name LIMIT 20`, [userId, `%${q}%`] ); @@ -21,7 +21,8 @@ export default async function userRoutes(app: FastifyInstance) { const online = connections; return rows.map(u => ({ id: u.id, username: u.username, displayName: u.display_name, - avatarColor: u.avatar_color, online: online.has(u.id), lastSeen: u.last_seen, + avatarColor: u.avatar_color, avatar: u.avatar || null, + position: u.position || null, online: online.has(u.id), lastSeen: u.last_seen, })); }); @@ -29,14 +30,15 @@ export default async function userRoutes(app: FastifyInstance) { app.get('/', async (req) => { const { id: userId } = req.user as { id: string }; const { rows } = await pool.query( - `SELECT id, username, display_name, avatar_color, last_seen FROM users + `SELECT id, username, display_name, avatar_color, avatar, position, last_seen FROM users WHERE is_active = TRUE AND id != $1 ORDER BY display_name`, [userId] ); const online = connections; return rows.map(u => ({ id: u.id, username: u.username, displayName: u.display_name, - avatarColor: u.avatar_color, online: online.has(u.id), lastSeen: u.last_seen, + avatarColor: u.avatar_color, avatar: u.avatar || null, + position: u.position || null, online: online.has(u.id), lastSeen: u.last_seen, })); }); @@ -44,12 +46,13 @@ export default async function userRoutes(app: FastifyInstance) { app.get('/:id', async (req, reply) => { const { id } = req.params as { id: string }; const { rows: [u] } = await pool.query( - 'SELECT id, username, display_name, avatar_color, bio, last_seen FROM users WHERE id = $1 AND is_active = TRUE', [id] + 'SELECT id, username, display_name, avatar_color, avatar, bio, position, last_seen FROM users WHERE id = $1 AND is_active = TRUE', [id] ); if (!u) return reply.status(404).send({ error: 'Not found' }); return { id: u.id, username: u.username, displayName: u.display_name, - avatarColor: u.avatar_color, bio: u.bio, online: connections.has(u.id), lastSeen: u.last_seen, + avatarColor: u.avatar_color, avatar: u.avatar || null, + position: u.position || null, bio: u.bio, online: connections.has(u.id), lastSeen: u.last_seen, }; }); } diff --git a/frontend/src/components/ChatInfoPanel.tsx b/frontend/src/components/ChatInfoPanel.tsx index 07cd620..cf2a7ca 100644 --- a/frontend/src/components/ChatInfoPanel.tsx +++ b/frontend/src/components/ChatInfoPanel.tsx @@ -204,12 +204,15 @@ export default function ChatInfoPanel({ chat, onClose, onRefresh }: Props) {
{success}
}