fix: TypeScript errors — declare authenticate, fix WebSocket types
This commit is contained in:
@@ -13,17 +13,16 @@
|
|||||||
"@fastify/multipart": "^8.3.0",
|
"@fastify/multipart": "^8.3.0",
|
||||||
"@fastify/static": "^7.0.4",
|
"@fastify/static": "^7.0.4",
|
||||||
"@fastify/websocket": "^8.3.1",
|
"@fastify/websocket": "^8.3.1",
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"fastify": "^4.28.1",
|
|
||||||
"pg": "^8.12.0",
|
|
||||||
"web-push": "^3.6.7"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20.14.0",
|
"@types/node": "^20.14.0",
|
||||||
"@types/pg": "^8.11.6",
|
"@types/pg": "^8.11.6",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"fastify": "^4.28.1",
|
||||||
|
"pg": "^8.12.0",
|
||||||
"tsx": "^4.15.7",
|
"tsx": "^4.15.7",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5",
|
||||||
|
"web-push": "^3.6.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
backend/src/types.d.ts
vendored
Normal file
7
backend/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
|
||||||
|
declare module 'fastify' {
|
||||||
|
interface FastifyInstance {
|
||||||
|
authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
import { WebSocket } from 'ws';
|
|
||||||
import { pool } from './db.js';
|
import { pool } from './db.js';
|
||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
|
|
||||||
// userId -> Set of WebSocket connections
|
// userId -> Set of WebSocket-like connections
|
||||||
export const connections = new Map<string, Set<WebSocket>>();
|
export const connections = new Map<string, Set<any>>();
|
||||||
|
|
||||||
function broadcast(userIds: string[], data: object) {
|
function broadcast(userIds: string[], data: object) {
|
||||||
const msg = JSON.stringify(data);
|
const msg = JSON.stringify(data);
|
||||||
for (const uid of userIds) {
|
for (const uid of userIds) {
|
||||||
const sockets = connections.get(uid);
|
const sockets = connections.get(uid);
|
||||||
if (sockets) {
|
if (sockets) {
|
||||||
sockets.forEach(ws => {
|
sockets.forEach((ws: any) => {
|
||||||
if (ws.readyState === WebSocket.OPEN) ws.send(msg);
|
try {
|
||||||
|
if (ws.readyState === 1) ws.send(msg);
|
||||||
|
} catch {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +23,7 @@ async function getChatMemberIds(chatId: string): Promise<string[]> {
|
|||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
'SELECT user_id FROM chat_members WHERE chat_id = $1', [chatId]
|
'SELECT user_id FROM chat_members WHERE chat_id = $1', [chatId]
|
||||||
);
|
);
|
||||||
return rows.map(r => r.user_id);
|
return rows.map((r: any) => r.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendPushToOfflineUsers(userIds: string[], payload: object) {
|
async function sendPushToOfflineUsers(userIds: string[], payload: object) {
|
||||||
@@ -51,11 +52,11 @@ async function sendPushToOfflineUsers(userIds: string[], payload: object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setupWebSocket(app: FastifyInstance) {
|
export function setupWebSocket(app: FastifyInstance) {
|
||||||
app.get('/ws', { websocket: true }, async (socket, req) => {
|
(app as any).get('/ws', { websocket: true }, async (socket: any, req: any) => {
|
||||||
let userId: string | null = null;
|
let userId: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = (req.query as any).token;
|
const token = req.query?.token;
|
||||||
const decoded = app.jwt.verify<{ id: string }>(token);
|
const decoded = app.jwt.verify<{ id: string }>(token);
|
||||||
userId = decoded.id;
|
userId = decoded.id;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -79,7 +80,7 @@ export function setupWebSocket(app: FastifyInstance) {
|
|||||||
broadcast(ids.filter(id => id !== userId), { type: 'user_online', payload: { userId, online: true } });
|
broadcast(ids.filter(id => id !== userId), { type: 'user_online', payload: { userId, online: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('message', async (raw) => {
|
socket.on('message', async (raw: any) => {
|
||||||
try {
|
try {
|
||||||
const { type, payload } = JSON.parse(raw.toString());
|
const { type, payload } = JSON.parse(raw.toString());
|
||||||
|
|
||||||
@@ -104,7 +105,6 @@ export function setupWebSocket(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'send_message') {
|
if (type === 'send_message') {
|
||||||
// Check membership
|
|
||||||
const { rows: [member] } = await pool.query(
|
const { rows: [member] } = await pool.query(
|
||||||
'SELECT role, can_send_messages FROM chat_members WHERE chat_id = $1 AND user_id = $2',
|
'SELECT role, can_send_messages FROM chat_members WHERE chat_id = $1 AND user_id = $2',
|
||||||
[payload.chatId, userId]
|
[payload.chatId, userId]
|
||||||
@@ -112,7 +112,6 @@ export function setupWebSocket(app: FastifyInstance) {
|
|||||||
if (!member) return;
|
if (!member) return;
|
||||||
|
|
||||||
const { rows: [chat] } = await pool.query('SELECT type FROM chats WHERE id = $1', [payload.chatId]);
|
const { rows: [chat] } = await pool.query('SELECT type FROM chats WHERE id = $1', [payload.chatId]);
|
||||||
// Channels: only owner/admin can send
|
|
||||||
if (chat.type === 'channel' && !['owner', 'admin'].includes(member.role)) return;
|
if (chat.type === 'channel' && !['owner', 'admin'].includes(member.role)) return;
|
||||||
if (!member.can_send_messages && member.role === 'member') return;
|
if (!member.can_send_messages && member.role === 'member') return;
|
||||||
|
|
||||||
@@ -123,24 +122,26 @@ export function setupWebSocket(app: FastifyInstance) {
|
|||||||
[payload.chatId, userId, payload.content, payload.type || 'text', payload.replyToId || null]
|
[payload.chatId, userId, payload.content, payload.type || 'text', payload.replyToId || null]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get sender info
|
|
||||||
const { rows: [sender] } = await pool.query(
|
const { rows: [sender] } = await pool.query(
|
||||||
'SELECT id, username, display_name, avatar_color FROM users WHERE id = $1', [userId]
|
'SELECT id, username, display_name, avatar_color FROM users WHERE id = $1', [userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fullMsg = { ...msg, sender };
|
const fullMsg = {
|
||||||
|
id: msg.id, chatId: msg.chat_id, content: msg.content, type: msg.type,
|
||||||
|
isDeleted: false, isEdited: false, createdAt: msg.created_at,
|
||||||
|
replyTo: null, attachments: [],
|
||||||
|
sender: { id: sender.id, username: sender.username, displayName: sender.display_name, avatarColor: sender.avatar_color }
|
||||||
|
};
|
||||||
|
|
||||||
const members = await getChatMemberIds(payload.chatId);
|
const members = await getChatMemberIds(payload.chatId);
|
||||||
broadcast(members, { type: 'new_message', payload: fullMsg });
|
broadcast(members, { type: 'new_message', payload: fullMsg });
|
||||||
|
|
||||||
// Push to offline users
|
|
||||||
await sendPushToOfflineUsers(members, {
|
await sendPushToOfflineUsers(members, {
|
||||||
title: sender.display_name,
|
title: sender.display_name,
|
||||||
body: payload.content.substring(0, 100),
|
body: payload.content.substring(0, 100),
|
||||||
chatId: payload.chatId
|
chatId: payload.chatId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update sender's last_read_at
|
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'UPDATE chat_members SET last_read_at = NOW() WHERE chat_id = $1 AND user_id = $2',
|
'UPDATE chat_members SET last_read_at = NOW() WHERE chat_id = $1 AND user_id = $2',
|
||||||
[payload.chatId, userId]
|
[payload.chatId, userId]
|
||||||
@@ -170,11 +171,24 @@ export function setupWebSocket(app: FastifyInstance) {
|
|||||||
if (!msg || msg.sender_id !== userId) return;
|
if (!msg || msg.sender_id !== userId) return;
|
||||||
|
|
||||||
const { rows: [updated] } = await pool.query(
|
const { rows: [updated] } = await pool.query(
|
||||||
'UPDATE messages SET content = $1, is_edited = TRUE, edited_at = NOW() WHERE id = $2 RETURNING *',
|
`UPDATE messages SET content = $1, is_edited = TRUE, edited_at = NOW() WHERE id = $2
|
||||||
|
RETURNING id, chat_id, sender_id, content, type, is_edited, edited_at, created_at`,
|
||||||
[payload.content, payload.messageId]
|
[payload.content, payload.messageId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { rows: [sender] } = await pool.query(
|
||||||
|
'SELECT id, username, display_name, avatar_color FROM users WHERE id = $1', [userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fullMsg = {
|
||||||
|
id: updated.id, chatId: updated.chat_id, content: updated.content, type: updated.type,
|
||||||
|
isDeleted: false, isEdited: updated.is_edited, editedAt: updated.edited_at,
|
||||||
|
createdAt: updated.created_at, replyTo: null, attachments: [],
|
||||||
|
sender: { id: sender.id, username: sender.username, displayName: sender.display_name, avatarColor: sender.avatar_color }
|
||||||
|
};
|
||||||
|
|
||||||
const members = await getChatMemberIds(msg.chat_id);
|
const members = await getChatMemberIds(msg.chat_id);
|
||||||
broadcast(members, { type: 'message_edited', payload: updated });
|
broadcast(members, { type: 'message_edited', payload: fullMsg });
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -184,17 +198,20 @@ export function setupWebSocket(app: FastifyInstance) {
|
|||||||
|
|
||||||
socket.on('close', async () => {
|
socket.on('close', async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
connections.get(userId)!.delete(socket);
|
const set = connections.get(userId);
|
||||||
if (connections.get(userId)!.size === 0) {
|
if (set) {
|
||||||
connections.delete(userId);
|
set.delete(socket);
|
||||||
await pool.query('UPDATE users SET last_seen = NOW() WHERE id = $1', [userId]);
|
if (set.size === 0) {
|
||||||
|
connections.delete(userId);
|
||||||
|
await pool.query('UPDATE users SET last_seen = NOW() WHERE id = $1', [userId]);
|
||||||
|
|
||||||
const { rows: memberChats } = await pool.query(
|
const { rows: memberChats } = await pool.query(
|
||||||
'SELECT DISTINCT chat_id FROM chat_members WHERE user_id = $1', [userId]
|
'SELECT DISTINCT chat_id FROM chat_members WHERE user_id = $1', [userId]
|
||||||
);
|
);
|
||||||
for (const row of memberChats) {
|
for (const row of memberChats) {
|
||||||
const ids = await getChatMemberIds(row.chat_id);
|
const ids = await getChatMemberIds(row.chat_id);
|
||||||
broadcast(ids, { type: 'user_online', payload: { userId, online: false } });
|
broadcast(ids, { type: 'user_online', payload: { userId, online: false } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ RUN npm install
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Empty VITE_API_URL = use relative URLs (nginx proxies /api/ and /ws)
|
ARG VITE_API_URL=https://api.janichat.ru
|
||||||
ARG VITE_API_URL=
|
ARG VITE_WS_URL=wss://api.janichat.ru
|
||||||
ARG VITE_WS_URL=
|
|
||||||
|
|
||||||
ENV VITE_API_URL=$VITE_API_URL
|
ENV VITE_API_URL=$VITE_API_URL
|
||||||
ENV VITE_WS_URL=$VITE_WS_URL
|
ENV VITE_WS_URL=$VITE_WS_URL
|
||||||
|
|||||||
Reference in New Issue
Block a user