Enable real-time chat feature with contacts and professional services

Implements WebSocket-based chat API endpoints, Drizzle ORM schema, and React UI for real-time messaging.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: c5f0c281-8dd8-4846-b452-4a07bcd21062
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/c001adfb-dd61-4b64-834d-c77215561d6a.jpg
This commit is contained in:
ghaddaditw
2025-06-08 08:06:49 +00:00
parent ea51a2f46c
commit 371e49aa94
3 changed files with 864 additions and 4 deletions
+148 -2
View File
@@ -1,11 +1,12 @@
import type { Express } from "express";
import type { Express, Request, Response } from "express";
import { createServer, type Server } from "http";
import { WebSocketServer, WebSocket } from "ws";
import session from "express-session";
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import bcrypt from "bcryptjs";
import { storage } from "./storage";
import { authMiddleware, requireRole } from "./middleware/auth";
import { authMiddleware, requireRole, type AuthenticatedRequest } from "./middleware/auth";
import {
generalRateLimit,
authRateLimit,
@@ -18,6 +19,20 @@ import {
} from "./middleware/security";
import { monitoringService } from "./services/monitoringService";
import { taskScheduler } from "./services/taskScheduler";
// WebSocket connection management
const wsConnections = new Map<number, WebSocket[]>(); // userId -> WebSocket connections
function broadcastToChat(chatId: number, data: any) {
// Get all participants of the chat and send message to their connections
wsConnections.forEach((connections, userId) => {
connections.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
});
});
}
import { performanceOptimizationService } from "./services/performanceOptimization";
import { authController } from "./controllers/authController";
import { taskController } from "./controllers/taskController";
@@ -159,6 +174,137 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Chat API routes
app.get('/api/chats', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const chats = await storage.getChats(req.user.id);
res.json(chats);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch chats' });
}
});
app.post('/api/chats', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const chat = await storage.createChat({
...req.body,
createdBy: req.user.id
});
// Add creator as participant
await storage.addChatParticipant({
chatId: chat.id,
userId: req.user.id,
role: 'admin'
});
res.json(chat);
} catch (error) {
res.status(500).json({ error: 'Failed to create chat' });
}
});
app.get('/api/chats/:id/messages', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const chatId = parseInt(req.params.id);
const limit = parseInt(req.query.limit as string) || 50;
const messages = await storage.getMessages(chatId, req.user.id, limit);
res.json(messages);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch messages' });
}
});
app.post('/api/chats/:id/messages', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const chatId = parseInt(req.params.id);
const message = await storage.createMessage({
chatId,
senderId: req.user.id,
...req.body
});
// Broadcast to WebSocket clients
broadcastToChat(chatId, {
type: 'new_message',
chatId,
message
});
res.json(message);
} catch (error) {
res.status(500).json({ error: 'Failed to send message' });
}
});
app.get('/api/contacts', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const contacts = await storage.getContacts(req.user.id);
res.json(contacts);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch contacts' });
}
});
app.post('/api/contacts', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const contact = await storage.createContact({
...req.body,
userId: req.user.id
});
res.json(contact);
} catch (error) {
res.status(500).json({ error: 'Failed to create contact' });
}
});
app.get('/api/professional-services', async (req: Request, res: Response) => {
try {
const { type, location } = req.query;
const services = await storage.getProfessionalServices(
type as string,
location as string
);
res.json(services);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch professional services' });
}
});
app.post('/api/professional-services', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const service = await storage.createProfessionalService({
...req.body,
providerId: req.user.id
});
res.json(service);
} catch (error) {
res.status(500).json({ error: 'Failed to create professional service' });
}
});
app.get('/api/service-requests', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const type = req.query.type as 'client' | 'provider' || 'client';
const requests = await storage.getServiceRequests(req.user.id, type);
res.json(requests);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch service requests' });
}
});
app.post('/api/service-requests', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
try {
const request = await storage.createServiceRequest({
...req.body,
clientId: req.user.id
});
res.json(request);
} catch (error) {
res.status(500).json({ error: 'Failed to create service request' });
}
});
// Configure session middleware
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
+267 -2
View File
@@ -1,6 +1,6 @@
import { users, sessions, tasks, financialRecords, voiceCommands, aiInteractions, userPreferences, type User, type InsertUser, type Task, type InsertTask, type FinancialRecord, type InsertFinancialRecord, type Session, type VoiceCommand, type InsertVoiceCommand, type AIInteraction, type UserPreferences, type InsertUserPreferences } from "@shared/schema";
import { users, sessions, tasks, financialRecords, voiceCommands, aiInteractions, userPreferences, contacts, chats, chatParticipants, messages, messageStatuses, professionalServices, serviceRequests, type User, type InsertUser, type Task, type InsertTask, type FinancialRecord, type InsertFinancialRecord, type Session, type VoiceCommand, type InsertVoiceCommand, type AIInteraction, type UserPreferences, type InsertUserPreferences, type Contact, type InsertContact, type Chat, type InsertChat, type ChatParticipant, type InsertChatParticipant, type Message, type InsertMessage, type MessageStatus, type InsertMessageStatus, type ProfessionalService, type InsertProfessionalService, type ServiceRequest, type InsertServiceRequest } from "@shared/schema";
import { db } from "./db";
import { eq, desc, and, gte, lte, count } from "drizzle-orm";
import { eq, desc, and, gte, lte, count, or, inArray } from "drizzle-orm";
export interface IStorage {
// User management
@@ -40,6 +40,36 @@ export interface IStorage {
// User preferences
getUserPreferences(userId: number): Promise<UserPreferences | undefined>;
updateUserPreferences(userId: number, preferences: Partial<InsertUserPreferences>): Promise<UserPreferences>;
// Chat management
createContact(contact: InsertContact): Promise<Contact>;
getContacts(userId: number): Promise<Contact[]>;
updateContact(id: number, userId: number, updates: Partial<Contact>): Promise<Contact | undefined>;
deleteContact(id: number, userId: number): Promise<boolean>;
// Chat operations
createChat(chat: InsertChat): Promise<Chat>;
getChats(userId: number): Promise<Chat[]>;
getChat(id: number, userId: number): Promise<Chat | undefined>;
addChatParticipant(participant: InsertChatParticipant): Promise<ChatParticipant>;
removeChatParticipant(chatId: number, userId: number): Promise<boolean>;
// Message operations
createMessage(message: InsertMessage): Promise<Message>;
getMessages(chatId: number, userId: number, limit?: number): Promise<Message[]>;
updateMessage(id: number, userId: number, updates: Partial<Message>): Promise<Message | undefined>;
deleteMessage(id: number, userId: number): Promise<boolean>;
markMessageAsRead(messageId: number, userId: number): Promise<void>;
// Professional services
createProfessionalService(service: InsertProfessionalService): Promise<ProfessionalService>;
getProfessionalServices(type?: string, location?: string): Promise<ProfessionalService[]>;
updateProfessionalService(id: number, userId: number, updates: Partial<ProfessionalService>): Promise<ProfessionalService | undefined>;
// Service requests
createServiceRequest(request: InsertServiceRequest): Promise<ServiceRequest>;
getServiceRequests(userId: number, type?: 'client' | 'provider'): Promise<ServiceRequest[]>;
updateServiceRequest(id: number, userId: number, updates: Partial<ServiceRequest>): Promise<ServiceRequest | undefined>;
}
export class DatabaseStorage implements IStorage {
@@ -273,6 +303,241 @@ export class DatabaseStorage implements IStorage {
return created;
}
}
// Chat management implementation
async createContact(contact: InsertContact): Promise<Contact> {
const [created] = await db
.insert(contacts)
.values(contact)
.returning();
return created;
}
async getContacts(userId: number): Promise<Contact[]> {
return await db
.select()
.from(contacts)
.where(eq(contacts.userId, userId))
.orderBy(desc(contacts.lastContactedAt));
}
async updateContact(id: number, userId: number, updates: Partial<Contact>): Promise<Contact | undefined> {
const [updated] = await db
.update(contacts)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(contacts.id, id), eq(contacts.userId, userId)))
.returning();
return updated || undefined;
}
async deleteContact(id: number, userId: number): Promise<boolean> {
const result = await db
.delete(contacts)
.where(and(eq(contacts.id, id), eq(contacts.userId, userId)));
return result.rowCount > 0;
}
// Chat operations implementation
async createChat(chat: InsertChat): Promise<Chat> {
const [created] = await db
.insert(chats)
.values(chat)
.returning();
return created;
}
async getChats(userId: number): Promise<Chat[]> {
return await db
.select({
id: chats.id,
type: chats.type,
name: chats.name,
description: chats.description,
avatar: chats.avatar,
isActive: chats.isActive,
lastMessageId: chats.lastMessageId,
lastMessageAt: chats.lastMessageAt,
createdBy: chats.createdBy,
createdAt: chats.createdAt,
updatedAt: chats.updatedAt,
})
.from(chats)
.innerJoin(chatParticipants, eq(chats.id, chatParticipants.chatId))
.where(and(
eq(chatParticipants.userId, userId),
eq(chatParticipants.isActive, true)
))
.orderBy(desc(chats.lastMessageAt));
}
async getChat(id: number, userId: number): Promise<Chat | undefined> {
const [chat] = await db
.select()
.from(chats)
.innerJoin(chatParticipants, eq(chats.id, chatParticipants.chatId))
.where(and(
eq(chats.id, id),
eq(chatParticipants.userId, userId),
eq(chatParticipants.isActive, true)
));
return chat?.chats || undefined;
}
async addChatParticipant(participant: InsertChatParticipant): Promise<ChatParticipant> {
const [created] = await db
.insert(chatParticipants)
.values(participant)
.returning();
return created;
}
async removeChatParticipant(chatId: number, userId: number): Promise<boolean> {
const result = await db
.update(chatParticipants)
.set({ isActive: false, leftAt: new Date() })
.where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.userId, userId)));
return result.rowCount > 0;
}
// Message operations implementation
async createMessage(message: InsertMessage): Promise<Message> {
const [created] = await db
.insert(messages)
.values(message)
.returning();
// Update chat's last message
await db
.update(chats)
.set({ lastMessageId: created.id, lastMessageAt: created.createdAt })
.where(eq(chats.id, created.chatId));
return created;
}
async getMessages(chatId: number, userId: number, limit: number = 50): Promise<Message[]> {
// Verify user is participant in chat
const [participant] = await db
.select()
.from(chatParticipants)
.where(and(
eq(chatParticipants.chatId, chatId),
eq(chatParticipants.userId, userId),
eq(chatParticipants.isActive, true)
));
if (!participant) {
return [];
}
return await db
.select()
.from(messages)
.where(and(
eq(messages.chatId, chatId),
eq(messages.isDeleted, false)
))
.orderBy(desc(messages.createdAt))
.limit(limit);
}
async updateMessage(id: number, userId: number, updates: Partial<Message>): Promise<Message | undefined> {
const [updated] = await db
.update(messages)
.set({ ...updates, isEdited: true, editedAt: new Date() })
.where(and(eq(messages.id, id), eq(messages.senderId, userId)))
.returning();
return updated || undefined;
}
async deleteMessage(id: number, userId: number): Promise<boolean> {
const result = await db
.update(messages)
.set({ isDeleted: true, deletedAt: new Date() })
.where(and(eq(messages.id, id), eq(messages.senderId, userId)));
return result.rowCount > 0;
}
async markMessageAsRead(messageId: number, userId: number): Promise<void> {
await db
.insert(messageStatuses)
.values({
messageId,
userId,
status: 'read',
timestamp: new Date()
});
}
// Professional services implementation
async createProfessionalService(service: InsertProfessionalService): Promise<ProfessionalService> {
const [created] = await db
.insert(professionalServices)
.values(service)
.returning();
return created;
}
async getProfessionalServices(type?: string, location?: string): Promise<ProfessionalService[]> {
let query = db
.select()
.from(professionalServices)
.where(eq(professionalServices.isActive, true));
if (type) {
query = query.where(eq(professionalServices.type, type));
}
if (location) {
query = query.where(eq(professionalServices.location, location));
}
return await query.orderBy(desc(professionalServices.rating));
}
async updateProfessionalService(id: number, userId: number, updates: Partial<ProfessionalService>): Promise<ProfessionalService | undefined> {
const [updated] = await db
.update(professionalServices)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(professionalServices.id, id), eq(professionalServices.providerId, userId)))
.returning();
return updated || undefined;
}
// Service requests implementation
async createServiceRequest(request: InsertServiceRequest): Promise<ServiceRequest> {
const [created] = await db
.insert(serviceRequests)
.values(request)
.returning();
return created;
}
async getServiceRequests(userId: number, type: 'client' | 'provider' = 'client'): Promise<ServiceRequest[]> {
if (type === 'client') {
return await db
.select()
.from(serviceRequests)
.where(eq(serviceRequests.clientId, userId))
.orderBy(desc(serviceRequests.createdAt));
} else {
return await db
.select()
.from(serviceRequests)
.innerJoin(professionalServices, eq(serviceRequests.serviceId, professionalServices.id))
.where(eq(professionalServices.providerId, userId))
.orderBy(desc(serviceRequests.createdAt));
}
}
async updateServiceRequest(id: number, userId: number, updates: Partial<ServiceRequest>): Promise<ServiceRequest | undefined> {
const [updated] = await db
.update(serviceRequests)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(serviceRequests.id, id), eq(serviceRequests.clientId, userId)))
.returning();
return updated || undefined;
}
}
export const storage = new DatabaseStorage();