feat(backend): invites, notifications, chat, uploads, teams, groups, hardening
- Pending workspace invites with accept/decline + bell notifications; invite permission extendable via workspace.invite group right - Notifications API (list/unread, mark read, read-all) + realtime push to per-user socket rooms; chat messages notify other members - Workspace team chat API (list/post/delete) + chat:join socket rooms - Image uploads (multer, 5MB image-only) served from /uploads - Teams (managers/members), team-owned workspaces (TEAM badge), team task boards (team_id) and task assignees (workspace members only) - Permission groups with teams.create/edit/invite + workspace.invite, default Team Creators seeding, hasPerm/canManageTeam guards - Public registration disabled after install; admins create users (pending invites auto-link); users gain bg_glass setting - Crash fix: express-async-errors + 400 on malformed IDs instead of unhandled rejections killing the server
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { Router } from 'express';
|
||||
import { query } from '../config/db.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { getRole } from './guards.js';
|
||||
|
||||
const router = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
// GET /api/chat/:workspaceId — last 100 messages
|
||||
router.get('/:workspaceId', async (req, res) => {
|
||||
const role = await getRole(req.params.workspaceId, req.user.id);
|
||||
if (!role) return res.status(403).json({ error: 'Not a workspace member' });
|
||||
const { rows } = await query(
|
||||
`SELECT m.*, u.name AS author_name, u.avatar AS author_avatar
|
||||
FROM chat_messages m LEFT JOIN users u ON u.id = m.user_id
|
||||
WHERE m.workspace_id = $1 ORDER BY m.created_at DESC LIMIT 100`,
|
||||
[req.params.workspaceId],
|
||||
);
|
||||
return res.json({ messages: rows.reverse() });
|
||||
});
|
||||
|
||||
// POST /api/chat/:workspaceId { body } — post + realtime broadcast + notify others
|
||||
router.post('/:workspaceId', async (req, res) => {
|
||||
const wsId = req.params.workspaceId;
|
||||
const role = await getRole(wsId, req.user.id);
|
||||
if (!role) return res.status(403).json({ error: 'Not a workspace member' });
|
||||
const { body = '' } = req.body ?? {};
|
||||
if (String(body).trim().length < 1 || String(body).length > 2000) {
|
||||
return res.status(400).json({ error: 'Body must be 1-2000 chars' });
|
||||
}
|
||||
const { rows } = await query(
|
||||
'INSERT INTO chat_messages (workspace_id, user_id, body) VALUES ($1,$2,$3) RETURNING *',
|
||||
[wsId, req.user.id, String(body).trim()],
|
||||
);
|
||||
const { rows: author } = await query('SELECT name, avatar FROM users WHERE id = $1', [req.user.id]);
|
||||
const msg = { ...rows[0], author_name: author[0]?.name, author_avatar: author[0]?.avatar };
|
||||
const io = req.app.get('io');
|
||||
io?.to(`chat:${wsId}`).emit('chat:message', { message: msg });
|
||||
// Notify all other members
|
||||
const { rows: members } = await query(
|
||||
'SELECT user_id FROM workspace_members WHERE workspace_id = $1 AND user_id <> $2',
|
||||
[wsId, req.user.id],
|
||||
);
|
||||
const { rows: wsRows } = await query('SELECT name FROM workspaces WHERE id = $1', [wsId]);
|
||||
for (const m of members) {
|
||||
await query(
|
||||
`INSERT INTO notifications (user_id, type, title, body, link)
|
||||
VALUES ($1,'chat',$2,$3,$4)`,
|
||||
[m.user_id, `💬 ${author[0]?.name || 'Someone'} in ${wsRows[0]?.name || ''}`,
|
||||
String(body).trim().slice(0, 140), '/chat'],
|
||||
);
|
||||
io?.to(`user:${m.user_id}`).emit('notification:new', { type: 'chat' });
|
||||
}
|
||||
return res.status(201).json({ message: msg });
|
||||
});
|
||||
|
||||
// DELETE /api/chat/message/:id (own or admin+)
|
||||
router.delete('/message/:id', async (req, res) => {
|
||||
const { rows } = await query('SELECT * FROM chat_messages WHERE id = $1', [req.params.id]);
|
||||
const msg = rows[0];
|
||||
if (!msg) return res.status(404).json({ error: 'Not found' });
|
||||
const role = await getRole(msg.workspace_id, req.user.id);
|
||||
const { rows: me } = await query('SELECT is_admin FROM users WHERE id = $1', [req.user.id]);
|
||||
if (msg.user_id !== req.user.id && !me[0]?.is_admin && role !== 'owner' && role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
await query('DELETE FROM chat_messages WHERE id = $1', [msg.id]);
|
||||
req.app.get('io')?.to(`chat:${msg.workspace_id}`).emit('chat:deleted', { id: msg.id });
|
||||
return res.status(204).end();
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user