diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..207dfb96fcb65af009baa430e7d071f7ab160053
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,21 @@
+# Ignore node modules and build output
+node_modules
+**/node_modules
+
+# Logs
+npm-debug.log
+*.log
+
+# Git
+.git
+.gitignore
+
+# dotenv files
+.env
+*.env
+
+# IDEs
+.vscode
+
+# local uploads
+server/uploads
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..9a3101a1344627c48e42fd2d40c407761c650a40
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,40 @@
+# Multi-stage build for ChatApp
+
+# ----- Build Stage -----
+FROM node:18 AS builder
+WORKDIR /app
+
+# Install root dependencies and workspaces
+COPY package.json package-lock.json* ./
+COPY client/package.json ./client/package.json
+COPY server/package.json ./server/package.json
+RUN npm install
+
+# Copy source code
+COPY . .
+
+# Build client and server
+RUN npm run build
+
+# Remove development dependencies to reduce size
+RUN npm prune --omit=dev --workspaces
+
+# ----- Production Stage -----
+FROM node:18-alpine AS runner
+WORKDIR /app
+
+# Copy built application from builder
+COPY --from=builder /app/package.json ./
+COPY --from=builder /app/client/package.json ./client/package.json
+COPY --from=builder /app/server/package.json ./server/package.json
+COPY --from=builder /app/client/dist ./client/dist
+COPY --from=builder /app/server/dist ./server/dist
+COPY --from=builder /app/client/node_modules ./client/node_modules
+COPY --from=builder /app/server/node_modules ./server/node_modules
+
+ENV NODE_ENV=production
+ENV PORT=3001
+
+EXPOSE 3001
+
+CMD ["node", "server/dist/index.js"]
diff --git a/client/.env.example b/client/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..f5acbb2ffd8e2749fce70729c7ec7f8cadfcd4ae
--- /dev/null
+++ b/client/.env.example
@@ -0,0 +1,4 @@
+VITE_API_URL=http://localhost:3001
+VITE_SUPABASE_URL=your_supabase_url
+VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
+VITE_SOCKET_URL=http://localhost:3001
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..5f1fde10f751a93afaa66dae3e1808e0209ed9bb
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ ChatApp - 现代化聊天应用
+
+
+
+
+
+
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..98c02faa7c675735f9ae2c2b952396f8881679fa
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "chat-app-client",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.20.1",
+ "socket.io-client": "^4.7.4",
+ "zustand": "^4.4.7",
+ "@supabase/supabase-js": "^2.38.4",
+ "axios": "^1.6.2",
+ "lucide-react": "^0.294.0",
+ "clsx": "^2.0.0",
+ "tailwind-merge": "^2.0.0",
+ "@radix-ui/react-avatar": "^1.1.10",
+ "@radix-ui/react-dialog": "^1.1.4",
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-scroll-area": "^1.2.2",
+ "@radix-ui/react-separator": "^1.1.1",
+ "@radix-ui/react-tabs": "^1.1.3",
+ "@radix-ui/react-toast": "^1.2.4",
+ "class-variance-authority": "^0.7.0",
+ "react-hook-form": "^7.48.2",
+ "@hookform/resolvers": "^3.3.2",
+ "zod": "^3.22.4",
+ "date-fns": "^2.30.0",
+ "emoji-picker-react": "^4.5.16"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@typescript-eslint/eslint-plugin": "^6.10.0",
+ "@typescript-eslint/parser": "^6.10.0",
+ "@vitejs/plugin-react": "^4.1.1",
+ "autoprefixer": "^10.4.16",
+ "eslint": "^8.53.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.4",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.5",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0",
+ "vitest": "^0.34.6"
+ }
+}
diff --git a/client/postcss.config.js b/client/postcss.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d
--- /dev/null
+++ b/client/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0cd6713d43c049f54aec6f01a26280398c445ae3
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,89 @@
+import { Routes, Route, Navigate } from 'react-router-dom'
+import { useEffect } from 'react'
+import { useAuthStore } from './store/authStore'
+import { Toaster } from './components/ui/toaster'
+
+// Pages
+import LoginPage from './pages/LoginPage'
+import RegisterPage from './pages/RegisterPage'
+import ChatPage from './pages/ChatPage'
+import AdminPage from './pages/AdminPage'
+import ProfilePage from './pages/ProfilePage'
+
+// Components
+import ProtectedRoute from './components/ProtectedRoute'
+import LoadingSpinner from './components/LoadingSpinner'
+
+function App() {
+ const { user, loading, checkAuth } = useAuthStore()
+
+ useEffect(() => {
+ checkAuth()
+ }, [checkAuth])
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ {/* Public routes */}
+ : }
+ />
+ : }
+ />
+
+ {/* Protected routes */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+ {/* Default redirect */}
+ }
+ />
+
+ {/* 404 fallback */}
+ }
+ />
+
+
+
+
+ )
+}
+
+export default App
diff --git a/client/src/components/LoadingSpinner.tsx b/client/src/components/LoadingSpinner.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d915c3e5480f8018e35f071eabd9c592ad4d2d4e
--- /dev/null
+++ b/client/src/components/LoadingSpinner.tsx
@@ -0,0 +1,24 @@
+import { cn } from '@/lib/utils'
+
+interface LoadingSpinnerProps {
+ size?: 'sm' | 'md' | 'lg'
+ className?: string
+}
+
+export default function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
+ const sizeClasses = {
+ sm: 'w-4 h-4',
+ md: 'w-6 h-6',
+ lg: 'w-8 h-8'
+ }
+
+ return (
+
+ )
+}
diff --git a/client/src/components/ProtectedRoute.tsx b/client/src/components/ProtectedRoute.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7c8545d1c83db19942ee7e295a44c108e286d8c8
--- /dev/null
+++ b/client/src/components/ProtectedRoute.tsx
@@ -0,0 +1,31 @@
+import { ReactNode } from 'react'
+import { Navigate } from 'react-router-dom'
+import { useAuthStore } from '@/store/authStore'
+import LoadingSpinner from './LoadingSpinner'
+
+interface ProtectedRouteProps {
+ children: ReactNode
+ requireAdmin?: boolean
+}
+
+export default function ProtectedRoute({ children, requireAdmin = false }: ProtectedRouteProps) {
+ const { user, loading } = useAuthStore()
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ if (!user) {
+ return
+ }
+
+ if (requireAdmin && !user.isAdmin) {
+ return
+ }
+
+ return <>{children}>
+}
diff --git a/client/src/components/chat/ChatSidebar.tsx b/client/src/components/chat/ChatSidebar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d5d9570e823f5073ff0d041ae076d9e65a770337
--- /dev/null
+++ b/client/src/components/chat/ChatSidebar.tsx
@@ -0,0 +1,176 @@
+import { useState } from 'react'
+import { useChatStore } from '@/store/chatStore'
+import { useAuthStore } from '@/store/authStore'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Badge } from '@/components/ui/badge'
+import { ScrollArea } from '@/components/ui/scroll-area'
+// import { Separator } from '@/components/ui/separator'
+import {
+ Search,
+ Plus,
+ Settings,
+ MessageSquare,
+ Users,
+ // MoreVertical
+} from 'lucide-react'
+import { formatTime, getInitials } from '@/lib/utils'
+
+export default function ChatSidebar() {
+ const { user } = useAuthStore()
+ const { chats, currentChat, selectChat, loading } = useChatStore()
+ const [searchQuery, setSearchQuery] = useState('')
+
+ const filteredChats = chats.filter(chat => {
+ if (!searchQuery) return true
+
+ const searchLower = searchQuery.toLowerCase()
+
+ // Search by chat name
+ if (chat.name?.toLowerCase().includes(searchLower)) return true
+
+ // Search by participant names (for direct chats)
+ if (chat.type === 'direct') {
+ const otherParticipant = chat.participants.find(p => p.userId !== user?.id)
+ if (otherParticipant?.user.displayName?.toLowerCase().includes(searchLower)) return true
+ }
+
+ return false
+ })
+
+ const getChatDisplayName = (chat: any) => {
+ if (chat.type === 'group') {
+ return chat.name || 'Unnamed Group'
+ } else {
+ const otherParticipant = chat.participants.find((p: any) => p.userId !== user?.id)
+ return otherParticipant?.user.displayName || 'Unknown User'
+ }
+ }
+
+ const getChatAvatar = (chat: any) => {
+ if (chat.type === 'group') {
+ return chat.avatar
+ } else {
+ const otherParticipant = chat.participants.find((p: any) => p.userId !== user?.id)
+ return otherParticipant?.user.avatar
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ {/* Search */}
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+ {/* Chat List */}
+
+
+ {loading ? (
+
+ ) : filteredChats.length === 0 ? (
+
+
+
No chats found
+
+ {searchQuery ? 'Try a different search term' : 'Start a new conversation'}
+
+
+
+ ) : (
+ filteredChats.map((chat) => (
+
selectChat(chat.id)}
+ className={`
+ flex items-center p-3 rounded-lg cursor-pointer transition-colors
+ hover:bg-accent hover:text-accent-foreground
+ ${currentChat?.id === chat.id ? 'bg-accent text-accent-foreground' : ''}
+ `}
+ >
+
+
+
+
+ {chat.type === 'group' ? (
+
+ ) : (
+ getInitials(getChatDisplayName(chat))
+ )}
+
+
+ {/* Online indicator for direct chats */}
+ {chat.type === 'direct' && (
+
+ )}
+
+
+
+
+
+ {getChatDisplayName(chat)}
+
+
+ {chat.lastMessage && (
+
+ {formatTime(chat.lastMessage.createdAt)}
+
+ )}
+ {chat.unreadCount > 0 && (
+
+ {chat.unreadCount > 99 ? '99+' : chat.unreadCount}
+
+ )}
+
+
+
+ {chat.lastMessage && (
+
+ {chat.lastMessage.type === 'text'
+ ? chat.lastMessage.content
+ : `📎 ${chat.lastMessage.type}`
+ }
+
+ )}
+
+ {chat.type === 'group' && (
+
+ {chat.participants.length} members
+
+ )}
+
+
+ ))
+ )}
+
+
+
+ )
+}
diff --git a/client/src/components/chat/ChatWindow.tsx b/client/src/components/chat/ChatWindow.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d0d465d5ae4b6421a61e90632e200b4b4c10a1e4
--- /dev/null
+++ b/client/src/components/chat/ChatWindow.tsx
@@ -0,0 +1,255 @@
+import { useState, useRef, useEffect } from 'react'
+import { useChatStore } from '@/store/chatStore'
+import { useAuthStore } from '@/store/authStore'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { ScrollArea } from '@/components/ui/scroll-area'
+// import { Separator } from '@/components/ui/separator'
+import {
+ Send,
+ Paperclip,
+ Smile,
+ Phone,
+ Video,
+ MoreVertical,
+ Users,
+ Info
+} from 'lucide-react'
+import { formatTime, getInitials } from '@/lib/utils'
+
+export default function ChatWindow() {
+ const { user } = useAuthStore()
+ const { currentChat, messages, sendMessage, loading } = useChatStore()
+ const [messageText, setMessageText] = useState('')
+ // const [isTyping, setIsTyping] = useState(false)
+ const messagesEndRef = useRef(null)
+ const inputRef = useRef(null)
+
+ const chatMessages = currentChat ? messages[currentChat.id] || [] : []
+
+ useEffect(() => {
+ scrollToBottom()
+ }, [chatMessages])
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
+ }
+
+ const handleSendMessage = async () => {
+ if (!messageText.trim() || !currentChat) return
+
+ try {
+ await sendMessage(currentChat.id, messageText.trim())
+ setMessageText('')
+ inputRef.current?.focus()
+ } catch (error) {
+ console.error('Failed to send message:', error)
+ }
+ }
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault()
+ handleSendMessage()
+ }
+ }
+
+ const getChatDisplayName = () => {
+ if (!currentChat) return ''
+
+ if (currentChat.type === 'group') {
+ return currentChat.name || 'Unnamed Group'
+ } else {
+ const otherParticipant = currentChat.participants.find(p => p.userId !== user?.id)
+ return otherParticipant?.user.displayName || 'Unknown User'
+ }
+ }
+
+ const getChatAvatar = () => {
+ if (!currentChat) return undefined
+
+ if (currentChat.type === 'group') {
+ return currentChat.avatar
+ } else {
+ const otherParticipant = currentChat.participants.find(p => p.userId !== user?.id)
+ return otherParticipant?.user.avatar
+ }
+ }
+
+ const isMessageFromCurrentUser = (message: any) => {
+ return message.senderId === user?.id
+ }
+
+ if (!currentChat) {
+ return (
+
+
+
💬
+
Welcome to ChatApp
+
+ Select a chat to start messaging
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ {currentChat.type === 'group' ? (
+
+ ) : (
+ getInitials(getChatDisplayName())
+ )}
+
+
+
+
+
{getChatDisplayName()}
+
+ {currentChat.type === 'group'
+ ? `${currentChat.participants.length} members`
+ : 'Online'
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Messages */}
+
+
+ {loading ? (
+
+ ) : chatMessages.length === 0 ? (
+
+
+
👋
+
+ No messages yet. Start the conversation!
+
+
+
+ ) : (
+ chatMessages.map((message, index) => {
+ const isFromCurrentUser = isMessageFromCurrentUser(message)
+ const showAvatar = !isFromCurrentUser && (
+ index === 0 ||
+ chatMessages[index - 1]?.senderId !== message.senderId
+ )
+
+ return (
+
+
+ {showAvatar && !isFromCurrentUser && (
+
+
+
+ {getInitials(message.sender?.displayName || 'U')}
+
+
+ )}
+
+
+
+ {!isFromCurrentUser && showAvatar && currentChat.type === 'group' && (
+
+ {message.sender?.displayName}
+
+ )}
+
+
{message.content}
+
+
+
+ {formatTime(message.createdAt)}
+
+ {isFromCurrentUser && (
+
+ {/* message delivery status could be rendered here */}
+
+ )}
+
+
+
+
+
+ )
+ })
+ )}
+
+
+
+
+ {/* Message Input */}
+
+
+
+
+
+ setMessageText(e.target.value)}
+ onKeyPress={handleKeyPress}
+ className="pr-10"
+ />
+
+
+
+
+
+
+
+ )
+}
diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96
--- /dev/null
+++ b/client/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..991f56ecb117e96284bf0f6cad3b14ea2fdf5264
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,48 @@
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f000e3ef5176395b067dfc3f3e1256a80c450015
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0ba4277355fadde682ecc8399ee656838dc74049
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3964e8c2ec07e4140aded2bec57cce918a515536
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,71 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/client/src/components/ui/input.tsx b/client/src/components/ui/input.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..677d05fd6c1c88366e8d0cdcdc9dcdea723a2ebb
--- /dev/null
+++ b/client/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/client/src/components/ui/label.tsx b/client/src/components/ui/label.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..683faa793819982d64e21cb2939666fd6d4a7b13
--- /dev/null
+++ b/client/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/client/src/components/ui/scroll-area.tsx b/client/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..cf253cf17056ce04827219643e484ea99c77cf6b
--- /dev/null
+++ b/client/src/components/ui/scroll-area.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/client/src/components/ui/separator.tsx b/client/src/components/ui/separator.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6d7f12265ba0338704f013930ce4d52c56527dd1
--- /dev/null
+++ b/client/src/components/ui/separator.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/client/src/components/ui/tabs.tsx b/client/src/components/ui/tabs.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f57fffdb5a07cd21b12f8c014513475a6980a469
--- /dev/null
+++ b/client/src/components/ui/tabs.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/client/src/components/ui/textarea.tsx b/client/src/components/ui/textarea.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9f9a6dc56193b728feda6d9b1a3d9302b8880f0b
--- /dev/null
+++ b/client/src/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/client/src/components/ui/toaster.tsx b/client/src/components/ui/toaster.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6416794c2a66988b48e8da5b3721c982f7149e68
--- /dev/null
+++ b/client/src/components/ui/toaster.tsx
@@ -0,0 +1,135 @@
+import * as React from "react"
+import * as ToastPrimitives from "@radix-ui/react-toast"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const ToastProvider = ToastPrimitives.Provider
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ )
+})
+Toast.displayName = ToastPrimitives.Root.displayName
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastAction.displayName = ToastPrimitives.Action.displayName
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ToastClose.displayName = ToastPrimitives.Close.displayName
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastTitle.displayName = ToastPrimitives.Title.displayName
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastDescription.displayName = ToastPrimitives.Description.displayName
+
+type ToastProps = React.ComponentPropsWithoutRef
+
+type ToastActionElement = React.ReactElement
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+}
+
+export function Toaster() {
+ return (
+
+
+
+ )
+}
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..c0c7f309133b1c1a4c7e8058ef59a2dc57420e5c
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,123 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 221.2 83.2% 53.3%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96%;
+ --secondary-foreground: 222.2 84% 4.9%;
+ --muted: 210 40% 96%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96%;
+ --accent-foreground: 222.2 84% 4.9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 217.2 91.2% 59.8%;
+ --primary-foreground: 222.2 84% 4.9%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 94.1%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+ width: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: hsl(var(--muted-foreground) / 0.3);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: hsl(var(--muted-foreground) / 0.5);
+}
+
+/* Chat message animations */
+.message-enter {
+ opacity: 0;
+ transform: translateY(10px);
+}
+
+.message-enter-active {
+ opacity: 1;
+ transform: translateY(0);
+ transition: opacity 200ms, transform 200ms;
+}
+
+/* Typing indicator animation */
+.typing-dots {
+ display: inline-block;
+}
+
+.typing-dots span {
+ display: inline-block;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background-color: hsl(var(--muted-foreground));
+ margin: 0 1px;
+ animation: typing 1.4s infinite ease-in-out;
+}
+
+.typing-dots span:nth-child(1) {
+ animation-delay: -0.32s;
+}
+
+.typing-dots span:nth-child(2) {
+ animation-delay: -0.16s;
+}
+
+@keyframes typing {
+ 0%, 80%, 100% {
+ transform: scale(0.8);
+ opacity: 0.5;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..90be3f44ae77bbaa593ef2210fbaa19b99fa3818
--- /dev/null
+++ b/client/src/lib/utils.ts
@@ -0,0 +1,190 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
+
+export function formatDate(date: Date | string): string {
+ const d = new Date(date)
+ const now = new Date()
+ const diff = now.getTime() - d.getTime()
+
+ // Less than 1 minute
+ if (diff < 60000) {
+ return 'Just now'
+ }
+
+ // Less than 1 hour
+ if (diff < 3600000) {
+ const minutes = Math.floor(diff / 60000)
+ return `${minutes}m ago`
+ }
+
+ // Less than 24 hours
+ if (diff < 86400000) {
+ const hours = Math.floor(diff / 3600000)
+ return `${hours}h ago`
+ }
+
+ // Less than 7 days
+ if (diff < 604800000) {
+ const days = Math.floor(diff / 86400000)
+ return `${days}d ago`
+ }
+
+ // More than 7 days
+ return d.toLocaleDateString()
+}
+
+export function formatTime(date: Date | string): string {
+ const d = new Date(date)
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+}
+
+export function formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 Bytes'
+
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+}
+
+export function getFileIcon(mimeType: string): string {
+ if (mimeType.startsWith('image/')) return '🖼️'
+ if (mimeType.startsWith('video/')) return '🎥'
+ if (mimeType.startsWith('audio/')) return '🎵'
+ if (mimeType.includes('pdf')) return '📄'
+ if (mimeType.includes('word')) return '📝'
+ if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊'
+ if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📽️'
+ if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('archive')) return '📦'
+ return '📎'
+}
+
+export function generateAvatar(name: string): string {
+ const colors = [
+ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
+ '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
+ ]
+
+ const initials = name
+ .split(' ')
+ .map(word => word[0])
+ .join('')
+ .toUpperCase()
+ .slice(0, 2)
+
+ const colorIndex = name.charCodeAt(0) % colors.length
+ const color = colors[colorIndex]
+
+ return `data:image/svg+xml,${encodeURIComponent(`
+
+ `)}`
+}
+
+export function debounce any>(
+ func: T,
+ wait: number
+): (...args: Parameters) => void {
+ let timeout: NodeJS.Timeout | null = null
+
+ return (...args: Parameters) => {
+ if (timeout) {
+ clearTimeout(timeout)
+ }
+
+ timeout = setTimeout(() => {
+ func(...args)
+ }, wait)
+ }
+}
+
+export function throttle any>(
+ func: T,
+ limit: number
+): (...args: Parameters) => void {
+ let inThrottle: boolean
+
+ return (...args: Parameters) => {
+ if (!inThrottle) {
+ func(...args)
+ inThrottle = true
+ setTimeout(() => inThrottle = false, limit)
+ }
+ }
+}
+
+export function isValidEmail(email: string): boolean {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
+}
+
+export function isValidUsername(username: string): boolean {
+ const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/
+ return usernameRegex.test(username)
+}
+
+export function truncateText(text: string, maxLength: number): string {
+ if (text.length <= maxLength) return text
+ return text.slice(0, maxLength) + '...'
+}
+
+export function copyToClipboard(text: string): Promise {
+ if (navigator.clipboard) {
+ return navigator.clipboard.writeText(text)
+ } else {
+ // Fallback for older browsers
+ const textArea = document.createElement('textarea')
+ textArea.value = text
+ document.body.appendChild(textArea)
+ textArea.focus()
+ textArea.select()
+
+ try {
+ document.execCommand('copy')
+ return Promise.resolve()
+ } catch (err) {
+ return Promise.reject(err)
+ } finally {
+ document.body.removeChild(textArea)
+ }
+ }
+}
+
+export function downloadFile(url: string, filename: string): void {
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+}
+
+export function isImageFile(file: File): boolean {
+ return file.type.startsWith('image/')
+}
+
+export function isVideoFile(file: File): boolean {
+ return file.type.startsWith('video/')
+}
+
+export function isAudioFile(file: File): boolean {
+ return file.type.startsWith('audio/')
+}
+
+export function getInitials(name: string): string {
+ return name
+ .split(' ')
+ .map(word => word[0])
+ .join('')
+ .toUpperCase()
+ .slice(0, 2)
+}
diff --git a/client/src/main.tsx b/client/src/main.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..aa5122372905cba3cc49b83bc12af7d18052abb2
--- /dev/null
+++ b/client/src/main.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import App from './App.tsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+)
diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7cbf43de6a1907681b8c2650276bbe79f6e6d572
--- /dev/null
+++ b/client/src/pages/AdminPage.tsx
@@ -0,0 +1,304 @@
+import { useState, useEffect } from 'react'
+import { Routes, Route, Link, useLocation } from 'react-router-dom'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+// The following components are currently unused but kept for future
+// admin interface enhancements.
+// import { Button } from '@/components/ui/button'
+// import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+// import { Badge } from '@/components/ui/badge'
+import {
+ Users,
+ MessageSquare,
+ Settings,
+ BarChart3,
+ Shield,
+ Activity,
+ Database,
+ Server
+} from 'lucide-react'
+
+// Mock data - replace with real API calls
+const mockStats = {
+ totalUsers: 1234,
+ activeUsers: 567,
+ totalChats: 890,
+ totalMessages: 12345,
+ storageUsed: 2.5, // GB
+ serverUptime: 99.9 // %
+}
+
+export default function AdminPage() {
+ // Stats state will be updated once API integration is implemented
+ const [stats] = useState(mockStats)
+ const location = useLocation()
+
+ useEffect(() => {
+ // Load admin stats
+ // TODO: Implement API call
+ }, [])
+
+ return (
+
+
+
+
+ {/* Sidebar */}
+
+
+
+
+ {/* Main content */}
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ )
+}
+
+function AdminDashboard({ stats }: { stats: typeof mockStats }) {
+ return (
+
+
+
Dashboard
+
+ Overview of your ChatApp instance
+
+
+
+ {/* Stats Cards */}
+
+
+
+ Total Users
+
+
+
+ {stats.totalUsers.toLocaleString()}
+
+ +12% from last month
+
+
+
+
+
+
+ Active Users
+
+
+
+ {stats.activeUsers.toLocaleString()}
+
+ Currently online
+
+
+
+
+
+
+ Total Messages
+
+
+
+ {stats.totalMessages.toLocaleString()}
+
+ +5% from last week
+
+
+
+
+
+
+ Storage Used
+
+
+
+ {stats.storageUsed} GB
+
+ Of 10 GB available
+
+
+
+
+
+
+ Server Uptime
+
+
+
+ {stats.serverUptime}%
+
+ Last 30 days
+
+
+
+
+
+
+ Total Chats
+
+
+
+ {stats.totalChats.toLocaleString()}
+
+ Groups and direct messages
+
+
+
+
+
+ {/* Recent Activity */}
+
+
+ Recent Activity
+
+ Latest events in your ChatApp instance
+
+
+
+
+
+
+
+
New user registered: john_doe
+
2 minutes ago
+
+
+
+
+
+
New group created: "Project Team"
+
5 minutes ago
+
+
+
+
+
+
Server maintenance completed
+
1 hour ago
+
+
+
+
+
+
+ )
+}
+
+function AdminUsers() {
+ return (
+
+
+
User Management
+
+ Manage users and their permissions
+
+
+
+
+
+
+ User management interface coming soon...
+
+
+
+
+ )
+}
+
+function AdminChats() {
+ return (
+
+
+
Chat Management
+
+ Monitor and manage chats and groups
+
+
+
+
+
+
+ Chat management interface coming soon...
+
+
+
+
+ )
+}
+
+function AdminSettings() {
+ return (
+
+
+
System Settings
+
+ Configure your ChatApp instance
+
+
+
+
+
+
+ Settings interface coming soon...
+
+
+
+
+ )
+}
diff --git a/client/src/pages/ChatPage.tsx b/client/src/pages/ChatPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a7b4acf17ddd82f70d3fdb7d72b5a6a95728f888
--- /dev/null
+++ b/client/src/pages/ChatPage.tsx
@@ -0,0 +1,59 @@
+import { useEffect } from 'react'
+import { useAuthStore } from '@/store/authStore'
+import { useChatStore } from '@/store/chatStore'
+import { socketService } from '@/services/socketService'
+import ChatSidebar from '@/components/chat/ChatSidebar'
+import ChatWindow from '@/components/chat/ChatWindow'
+import { Separator } from '@/components/ui/separator'
+
+export default function ChatPage() {
+ const { user, token } = useAuthStore()
+ const { loadChats, currentChat } = useChatStore()
+
+ useEffect(() => {
+ if (token && user) {
+ // Connect to socket
+ socketService.connect(token).then(() => {
+ console.log('Socket connected successfully')
+ }).catch((error) => {
+ console.error('Socket connection failed:', error)
+ })
+
+ // Load initial data
+ loadChats()
+
+ // Cleanup on unmount
+ return () => {
+ socketService.disconnect()
+ }
+ }
+ }, [token, user, loadChats])
+
+ return (
+
+ {/* Sidebar */}
+
+
+
+
+
+
+ {/* Main chat area */}
+
+ {currentChat ? (
+
+ ) : (
+
+
+
💬
+
Welcome to ChatApp
+
+ Select a chat to start messaging
+
+
+
+ )}
+
+
+ )
+}
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5a0e3fe8524a2fbe625739ae57c1bec29d6f1b50
--- /dev/null
+++ b/client/src/pages/LoginPage.tsx
@@ -0,0 +1,109 @@
+import { useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import { useAuthStore } from '@/store/authStore'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import LoadingSpinner from '@/components/LoadingSpinner'
+import { MessageCircle } from 'lucide-react'
+
+export default function LoginPage() {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const { login, loading, error, clearError } = useAuthStore()
+ const navigate = useNavigate()
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ clearError()
+
+ try {
+ await login(email, password)
+ navigate('/chat')
+ } catch (error) {
+ // Error is handled by the store
+ }
+ }
+
+ return (
+
+
+
+
+ Welcome Back
+
+ Sign in to your ChatApp account
+
+
+
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+
+ )
+}
diff --git a/client/src/pages/ProfilePage.tsx b/client/src/pages/ProfilePage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5bc6a97589e603bce7b60ab1c5acd4437d29ecae
--- /dev/null
+++ b/client/src/pages/ProfilePage.tsx
@@ -0,0 +1,173 @@
+import { useState } from 'react'
+import { useAuthStore } from '@/store/authStore'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Textarea } from '@/components/ui/textarea'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { getInitials } from '@/lib/utils'
+import { User, Settings } from 'lucide-react'
+
+export default function ProfilePage() {
+ const { user, updateUser } = useAuthStore()
+ const [isEditing, setIsEditing] = useState(false)
+ const [formData, setFormData] = useState({
+ displayName: user?.displayName || '',
+ bio: user?.bio || '',
+ })
+
+ const handleSave = () => {
+ if (user) {
+ updateUser(formData)
+ setIsEditing(false)
+ }
+ }
+
+ const handleCancel = () => {
+ setFormData({
+ displayName: user?.displayName || '',
+ bio: user?.bio || '',
+ })
+ setIsEditing(false)
+ }
+
+ if (!user) return null
+
+ return (
+
+
+
+
+
Profile
+
+
+
+
+ Personal Information
+
+ Manage your account details and preferences
+
+
+
+
+
+
+
+ {getInitials(user.displayName)}
+
+
+
+
+
+ JPG, PNG or GIF. Max size 2MB.
+
+
+
+
+
+
+
+
+ setFormData(prev => ({ ...prev, displayName: e.target.value }))}
+ disabled={!isEditing}
+ />
+
+
+
+
+
+
+
+ {isEditing ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+
+
+
+ Account Settings
+
+ Security and privacy settings
+
+
+
+
+
+
Change Password
+
+ Update your password to keep your account secure
+
+
+
+
+
+
+
+
Two-Factor Authentication
+
+ Add an extra layer of security to your account
+
+
+
+
+
+
+
+
Privacy Settings
+
+ Control who can see your information
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/client/src/pages/RegisterPage.tsx b/client/src/pages/RegisterPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..592f752b9a16ac597bc3d6139312eede778aca9d
--- /dev/null
+++ b/client/src/pages/RegisterPage.tsx
@@ -0,0 +1,204 @@
+import { useState } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import { useAuthStore } from '@/store/authStore'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import LoadingSpinner from '@/components/LoadingSpinner'
+import { MessageCircle } from 'lucide-react'
+
+export default function RegisterPage() {
+ const [formData, setFormData] = useState({
+ email: '',
+ username: '',
+ displayName: '',
+ password: '',
+ confirmPassword: ''
+ })
+ const [validationError, setValidationError] = useState('')
+ const { register, loading, error, clearError } = useAuthStore()
+ const navigate = useNavigate()
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }))
+ setValidationError('')
+ }
+
+ const validateForm = () => {
+ if (formData.password !== formData.confirmPassword) {
+ setValidationError('Passwords do not match')
+ return false
+ }
+
+ if (formData.password.length < 6) {
+ setValidationError('Password must be at least 6 characters long')
+ return false
+ }
+
+ if (formData.username.length < 3) {
+ setValidationError('Username must be at least 3 characters long')
+ return false
+ }
+
+ if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
+ setValidationError('Username can only contain letters, numbers, and underscores')
+ return false
+ }
+
+ return true
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ clearError()
+ setValidationError('')
+
+ if (!validateForm()) {
+ return
+ }
+
+ try {
+ await register({
+ email: formData.email,
+ username: formData.username,
+ displayName: formData.displayName,
+ password: formData.password
+ })
+ navigate('/chat')
+ } catch (error) {
+ // Error is handled by the store
+ }
+ }
+
+ const displayError = validationError || error
+
+ return (
+
+
+
+
+ Create Account
+
+ Join ChatApp and start messaging
+
+
+
+
+
+
+
+ Already have an account?{' '}
+
+ Sign in
+
+
+
+
+
+
+ )
+}
diff --git a/client/src/services/api.ts b/client/src/services/api.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df1425e91edb691f0ea8ead73231dfc204da7d85
--- /dev/null
+++ b/client/src/services/api.ts
@@ -0,0 +1,182 @@
+import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
+import { ApiResponse } from '../../../shared/types'
+
+class ApiService {
+ private api: AxiosInstance
+
+ constructor() {
+ this.api = axios.create({
+ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001',
+ timeout: 10000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ })
+
+ // Request interceptor to add auth token
+ this.api.interceptors.request.use(
+ (config) => {
+ const token = this.getToken()
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ (error) => {
+ return Promise.reject(error)
+ }
+ )
+
+ // Response interceptor to handle errors
+ this.api.interceptors.response.use(
+ (response) => {
+ return response
+ },
+ (error) => {
+ if (error.response?.status === 401) {
+ // Token expired or invalid
+ this.clearToken()
+ window.location.href = '/login'
+ }
+ return Promise.reject(this.handleError(error))
+ }
+ )
+ }
+
+ private getToken(): string | null {
+ try {
+ const authStorage = localStorage.getItem('auth-storage')
+ if (authStorage) {
+ const parsed = JSON.parse(authStorage)
+ return parsed.state?.token || null
+ }
+ } catch (error) {
+ console.error('Error getting token:', error)
+ }
+ return null
+ }
+
+ private clearToken(): void {
+ try {
+ localStorage.removeItem('auth-storage')
+ } catch (error) {
+ console.error('Error clearing token:', error)
+ }
+ }
+
+ private handleError(error: any): Error {
+ if (error.response) {
+ // Server responded with error status
+ const message = error.response.data?.message || error.response.data?.error || 'Server error'
+ return new Error(message)
+ } else if (error.request) {
+ // Request was made but no response received
+ return new Error('Network error - please check your connection')
+ } else {
+ // Something else happened
+ return new Error(error.message || 'An unexpected error occurred')
+ }
+ }
+
+ // Generic request methods
+ async get(url: string, config?: AxiosRequestConfig): Promise {
+ const response = await this.api.get>(url, config)
+ if (!response.data.success) {
+ throw new Error(response.data.error || 'Request failed')
+ }
+ return response.data.data!
+ }
+
+ async post(url: string, data?: any, config?: AxiosRequestConfig): Promise {
+ const response = await this.api.post>(url, data, config)
+ if (!response.data.success) {
+ throw new Error(response.data.error || 'Request failed')
+ }
+ return response.data.data!
+ }
+
+ async put(url: string, data?: any, config?: AxiosRequestConfig): Promise {
+ const response = await this.api.put>(url, data, config)
+ if (!response.data.success) {
+ throw new Error(response.data.error || 'Request failed')
+ }
+ return response.data.data!
+ }
+
+ async patch(url: string, data?: any, config?: AxiosRequestConfig): Promise {
+ const response = await this.api.patch>(url, data, config)
+ if (!response.data.success) {
+ throw new Error(response.data.error || 'Request failed')
+ }
+ return response.data.data!
+ }
+
+ async delete(url: string, config?: AxiosRequestConfig): Promise {
+ const response = await this.api.delete>(url, config)
+ if (!response.data.success) {
+ throw new Error(response.data.error || 'Request failed')
+ }
+ return response.data.data!
+ }
+
+ // File upload method
+ async upload(url: string, file: File, onProgress?: (progress: number) => void): Promise {
+ const formData = new FormData()
+ formData.append('file', file)
+
+ const config: AxiosRequestConfig = {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (onProgress && progressEvent.total) {
+ const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
+ onProgress(progress)
+ }
+ },
+ }
+
+ const response = await this.api.post>(url, formData, config)
+ if (!response.data.success) {
+ throw new Error(response.data.error || 'Upload failed')
+ }
+ return response.data.data!
+ }
+
+ // Multiple file upload method
+ async uploadMultiple(
+ url: string,
+ files: File[],
+ onProgress?: (progress: number) => void
+ ): Promise {
+ const formData = new FormData()
+ files.forEach((file, index) => {
+ formData.append(`files[${index}]`, file)
+ })
+
+ const config: AxiosRequestConfig = {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ onUploadProgress: (progressEvent) => {
+ if (onProgress && progressEvent.total) {
+ const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
+ onProgress(progress)
+ }
+ },
+ }
+
+ const response = await this.api.post>(url, formData, config)
+ if (!response.data.success) {
+ throw new Error(response.data.error || 'Upload failed')
+ }
+ return response.data.data!
+ }
+
+ // Get raw axios instance for custom requests
+ getAxiosInstance(): AxiosInstance {
+ return this.api
+ }
+}
+
+export const apiService = new ApiService()
diff --git a/client/src/services/authService.ts b/client/src/services/authService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4537445b3eebdeae3ac0dc72299f6d909c1c2f6a
--- /dev/null
+++ b/client/src/services/authService.ts
@@ -0,0 +1,62 @@
+import { User, LoginRequest, RegisterRequest } from '../../../shared/types'
+import { apiService } from './api'
+
+interface AuthResponse {
+ user: User
+ token: string
+}
+
+class AuthService {
+ async login(credentials: LoginRequest): Promise {
+ return await apiService.post('/api/auth/login', credentials)
+ }
+
+ async register(userData: RegisterRequest): Promise {
+ return await apiService.post('/api/auth/register', userData)
+ }
+
+ async logout(): Promise {
+ try {
+ await apiService.post('/api/auth/logout')
+ } catch (error) {
+ // Ignore logout errors, clear local storage anyway
+ console.warn('Logout request failed:', error)
+ }
+ }
+
+ async getCurrentUser(): Promise {
+ return await apiService.get('/api/auth/me')
+ }
+
+ async refreshToken(): Promise {
+ return await apiService.post('/api/auth/refresh')
+ }
+
+ async changePassword(data: {
+ currentPassword: string
+ newPassword: string
+ }): Promise {
+ await apiService.post('/api/auth/change-password', data)
+ }
+
+ async requestPasswordReset(email: string): Promise {
+ await apiService.post('/api/auth/forgot-password', { email })
+ }
+
+ async resetPassword(data: {
+ token: string
+ newPassword: string
+ }): Promise {
+ await apiService.post('/api/auth/reset-password', data)
+ }
+
+ async verifyEmail(token: string): Promise {
+ await apiService.post('/api/auth/verify-email', { token })
+ }
+
+ async resendVerificationEmail(): Promise {
+ await apiService.post('/api/auth/resend-verification')
+ }
+}
+
+export const authService = new AuthService()
diff --git a/client/src/services/chatService.ts b/client/src/services/chatService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..62e8a030b49aba0075437bf9ccb49cd8cd7d67db
--- /dev/null
+++ b/client/src/services/chatService.ts
@@ -0,0 +1,136 @@
+import { Chat, Message, CreateChatRequest, SendMessageData, PaginatedResponse } from '../../../shared/types'
+import { apiService } from './api'
+
+class ChatService {
+ // Chat management
+ async getChats(): Promise {
+ return await apiService.get('/api/chats')
+ }
+
+ async getChat(chatId: string): Promise {
+ return await apiService.get(`/api/chats/${chatId}`)
+ }
+
+ async createChat(data: CreateChatRequest): Promise {
+ return await apiService.post('/api/chats', data)
+ }
+
+ async updateChat(chatId: string, data: Partial): Promise {
+ return await apiService.put(`/api/chats/${chatId}`, data)
+ }
+
+ async deleteChat(chatId: string): Promise {
+ await apiService.delete(`/api/chats/${chatId}`)
+ }
+
+ async leaveChat(chatId: string): Promise {
+ await apiService.post(`/api/chats/${chatId}/leave`)
+ }
+
+ // Message management
+ async getMessages(chatId: string, page = 1, limit = 50): Promise {
+ const response = await apiService.get>(
+ `/api/chats/${chatId}/messages?page=${page}&limit=${limit}`
+ )
+ return response.data
+ }
+
+ async sendMessage(chatId: string, data: Omit): Promise {
+ if (data.attachments && data.attachments.length > 0) {
+ // Upload files first
+ const uploadedFiles = await this.uploadFiles(data.attachments)
+ return await apiService.post(`/api/chats/${chatId}/messages`, {
+ content: data.content,
+ type: data.type,
+ attachments: uploadedFiles,
+ replyTo: data.replyTo,
+ })
+ } else {
+ return await apiService.post(`/api/chats/${chatId}/messages`, {
+ content: data.content,
+ type: data.type,
+ replyTo: data.replyTo,
+ })
+ }
+ }
+
+ async editMessage(messageId: string, content: string): Promise {
+ return await apiService.put(`/api/messages/${messageId}`, { content })
+ }
+
+ async deleteMessage(messageId: string): Promise {
+ await apiService.delete(`/api/messages/${messageId}`)
+ }
+
+ async addReaction(messageId: string, emoji: string): Promise {
+ await apiService.post(`/api/messages/${messageId}/reactions`, { emoji })
+ }
+
+ async removeReaction(messageId: string, emoji: string): Promise {
+ await apiService.delete(`/api/messages/${messageId}/reactions/${emoji}`)
+ }
+
+ // Group management
+ async addMember(chatId: string, userId: string): Promise {
+ await apiService.post(`/api/chats/${chatId}/members`, { userId })
+ }
+
+ async removeMember(chatId: string, userId: string): Promise {
+ await apiService.delete(`/api/chats/${chatId}/members/${userId}`)
+ }
+
+ async updateMemberRole(chatId: string, userId: string, role: string): Promise {
+ await apiService.put(`/api/chats/${chatId}/members/${userId}`, { role })
+ }
+
+ async getChatMembers(chatId: string): Promise {
+ return await apiService.get(`/api/chats/${chatId}/members`)
+ }
+
+ // File upload
+ async uploadFiles(files: File[]): Promise {
+ const uploadPromises = files.map(file => this.uploadFile(file))
+ return await Promise.all(uploadPromises)
+ }
+
+ async uploadFile(file: File): Promise {
+ return await apiService.upload('/api/upload', file)
+ }
+
+ // Search
+ async searchMessages(query: string, chatId?: string): Promise {
+ const params = new URLSearchParams({ q: query })
+ if (chatId) {
+ params.append('chatId', chatId)
+ }
+ return await apiService.get(`/api/search/messages?${params}`)
+ }
+
+ async searchChats(query: string): Promise {
+ return await apiService.get(`/api/search/chats?q=${encodeURIComponent(query)}`)
+ }
+
+ // Chat settings
+ async updateChatSettings(chatId: string, settings: any): Promise {
+ await apiService.put(`/api/chats/${chatId}/settings`, settings)
+ }
+
+ async getChatSettings(chatId: string): Promise {
+ return await apiService.get(`/api/chats/${chatId}/settings`)
+ }
+
+ // Notifications
+ async markAsRead(chatId: string): Promise {
+ await apiService.post(`/api/chats/${chatId}/read`)
+ }
+
+ async muteChat(chatId: string, duration?: number): Promise {
+ await apiService.post(`/api/chats/${chatId}/mute`, { duration })
+ }
+
+ async unmuteChat(chatId: string): Promise {
+ await apiService.post(`/api/chats/${chatId}/unmute`)
+ }
+}
+
+export const chatService = new ChatService()
diff --git a/client/src/services/socketService.ts b/client/src/services/socketService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..00dc0b104a2dbedeec6525c1fec6b060c3174e4c
--- /dev/null
+++ b/client/src/services/socketService.ts
@@ -0,0 +1,198 @@
+import { io, Socket } from 'socket.io-client'
+import { Message, User, Chat } from '../../../shared/types'
+
+class SocketService {
+ private socket: Socket | null = null
+ private reconnectAttempts = 0
+ private maxReconnectAttempts = 5
+ private reconnectDelay = 1000
+
+ connect(token: string): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.socket?.connected) {
+ resolve()
+ return
+ }
+
+ const socketUrl = import.meta.env.VITE_SOCKET_URL || 'http://localhost:3001'
+
+ this.socket = io(socketUrl, {
+ auth: {
+ token
+ },
+ transports: ['websocket', 'polling'],
+ timeout: 10000,
+ })
+
+ this.socket.on('connect', () => {
+ console.log('Socket connected')
+ this.reconnectAttempts = 0
+ resolve()
+ })
+
+ this.socket.on('connect_error', (error) => {
+ console.error('Socket connection error:', error)
+ reject(error)
+ })
+
+ this.socket.on('disconnect', (reason) => {
+ console.log('Socket disconnected:', reason)
+ if (reason === 'io server disconnect') {
+ // Server disconnected, try to reconnect
+ this.handleReconnect()
+ }
+ })
+
+ this.socket.on('error', (error) => {
+ console.error('Socket error:', error)
+ })
+
+ // Set up event listeners
+ this.setupEventListeners()
+ })
+ }
+
+ disconnect(): void {
+ if (this.socket) {
+ this.socket.disconnect()
+ this.socket = null
+ }
+ }
+
+ private handleReconnect(): void {
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.reconnectAttempts++
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
+
+ setTimeout(() => {
+ console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
+ this.socket?.connect()
+ }, delay)
+ } else {
+ console.error('Max reconnection attempts reached')
+ }
+ }
+
+ private setupEventListeners(): void {
+ if (!this.socket) return
+
+ // Authentication events
+ this.socket.on('authenticated', (user: User) => {
+ console.log('Socket authenticated for user:', user.username)
+ })
+
+ this.socket.on('authentication_error', (error: string) => {
+ console.error('Socket authentication error:', error)
+ })
+ }
+
+ // Message events
+ onMessageReceive(callback: (message: Message) => void): void {
+ this.socket?.on('message:receive', callback)
+ }
+
+ onMessageEdit(callback: (message: Message) => void): void {
+ this.socket?.on('message:edit', callback)
+ }
+
+ onMessageDelete(callback: (messageId: string) => void): void {
+ this.socket?.on('message:delete', callback)
+ }
+
+ onMessageReaction(callback: (data: { messageId: string; reactions: any[] }) => void): void {
+ this.socket?.on('message:reaction', callback)
+ }
+
+ // Typing events
+ onTypingStart(callback: (data: { chatId: string; userId: string; user: User }) => void): void {
+ this.socket?.on('typing:start', callback)
+ }
+
+ onTypingStop(callback: (data: { chatId: string; userId: string }) => void): void {
+ this.socket?.on('typing:stop', callback)
+ }
+
+ sendTypingStart(chatId: string): void {
+ this.socket?.emit('typing:start', { chatId })
+ }
+
+ sendTypingStop(chatId: string): void {
+ this.socket?.emit('typing:stop', { chatId })
+ }
+
+ // User status events
+ onUserOnline(callback: (userId: string) => void): void {
+ this.socket?.on('user:online', callback)
+ }
+
+ onUserOffline(callback: (userId: string) => void): void {
+ this.socket?.on('user:offline', callback)
+ }
+
+ onUserStatusUpdate(callback: (data: { userId: string; isOnline: boolean; lastSeen?: Date }) => void): void {
+ this.socket?.on('user:status', callback)
+ }
+
+ // Chat events
+ onChatUpdate(callback: (chat: Chat) => void): void {
+ this.socket?.on('chat:update', callback)
+ }
+
+ onChatJoin(callback: (data: { chatId: string; user: User }) => void): void {
+ this.socket?.on('chat:join', callback)
+ }
+
+ onChatLeave(callback: (data: { chatId: string; userId: string }) => void): void {
+ this.socket?.on('chat:leave', callback)
+ }
+
+ joinChat(chatId: string): void {
+ this.socket?.emit('chat:join', chatId)
+ }
+
+ leaveChat(chatId: string): void {
+ this.socket?.emit('chat:leave', chatId)
+ }
+
+ // Group events
+ onGroupMemberAdd(callback: (data: { groupId: string; user: User; addedBy: User }) => void): void {
+ this.socket?.on('group:member:add', callback)
+ }
+
+ onGroupMemberRemove(callback: (data: { groupId: string; userId: string; removedBy: User }) => void): void {
+ this.socket?.on('group:member:remove', callback)
+ }
+
+ onGroupUpdate(callback: (group: any) => void): void {
+ this.socket?.on('group:update', callback)
+ }
+
+ // Notification events
+ onNotification(callback: (notification: any) => void): void {
+ this.socket?.on('notification', callback)
+ }
+
+ // Utility methods
+ isConnected(): boolean {
+ return this.socket?.connected || false
+ }
+
+ emit(event: string, data?: any): void {
+ this.socket?.emit(event, data)
+ }
+
+ on(event: string, callback: (...args: any[]) => void): void {
+ this.socket?.on(event, callback)
+ }
+
+ off(event: string, callback?: (...args: any[]) => void): void {
+ this.socket?.off(event, callback)
+ }
+
+ // Remove all listeners for cleanup
+ removeAllListeners(): void {
+ this.socket?.removeAllListeners()
+ }
+}
+
+export const socketService = new SocketService()
diff --git a/client/src/store/authStore.ts b/client/src/store/authStore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fb8a96c55cc7f5e51a91715452d9f42c6b086c42
--- /dev/null
+++ b/client/src/store/authStore.ts
@@ -0,0 +1,130 @@
+import { create } from 'zustand'
+import { persist } from 'zustand/middleware'
+import { User } from '../../../shared/types'
+import { authService } from '../services/authService'
+
+interface AuthState {
+ user: User | null
+ token: string | null
+ loading: boolean
+ error: string | null
+}
+
+interface AuthActions {
+ login: (email: string, password: string) => Promise
+ register: (data: {
+ email: string
+ username: string
+ password: string
+ displayName: string
+ }) => Promise
+ logout: () => void
+ checkAuth: () => Promise
+ updateUser: (user: Partial) => void
+ clearError: () => void
+}
+
+export const useAuthStore = create()(
+ persist(
+ (set, get) => ({
+ // State
+ user: null,
+ token: null,
+ loading: false,
+ error: null,
+
+ // Actions
+ login: async (email: string, password: string) => {
+ set({ loading: true, error: null })
+ try {
+ const response = await authService.login({ email, password })
+ set({
+ user: response.user,
+ token: response.token,
+ loading: false,
+ error: null,
+ })
+ } catch (error: any) {
+ set({
+ loading: false,
+ error: error.message || 'Login failed',
+ })
+ throw error
+ }
+ },
+
+ register: async (data) => {
+ set({ loading: true, error: null })
+ try {
+ const response = await authService.register(data)
+ set({
+ user: response.user,
+ token: response.token,
+ loading: false,
+ error: null,
+ })
+ } catch (error: any) {
+ set({
+ loading: false,
+ error: error.message || 'Registration failed',
+ })
+ throw error
+ }
+ },
+
+ logout: () => {
+ authService.logout()
+ set({
+ user: null,
+ token: null,
+ error: null,
+ })
+ },
+
+ checkAuth: async () => {
+ const { token } = get()
+ if (!token) {
+ set({ loading: false })
+ return
+ }
+
+ set({ loading: true })
+ try {
+ const user = await authService.getCurrentUser()
+ set({
+ user,
+ loading: false,
+ error: null,
+ })
+ } catch (error) {
+ set({
+ user: null,
+ token: null,
+ loading: false,
+ error: null,
+ })
+ }
+ },
+
+ updateUser: (userData) => {
+ const { user } = get()
+ if (user) {
+ set({
+ user: { ...user, ...userData },
+ })
+ }
+ },
+
+ clearError: () => {
+ set({ error: null })
+ },
+ }),
+ {
+ name: 'auth-storage',
+ partialize: (state) => ({
+ token: state.token,
+ user: state.user,
+ }),
+ }
+ )
+)
diff --git a/client/src/store/chatStore.ts b/client/src/store/chatStore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..668ee864c4d5324051c34b73d5442ead5b418691
--- /dev/null
+++ b/client/src/store/chatStore.ts
@@ -0,0 +1,285 @@
+import { create } from 'zustand'
+import { Chat, Message } from '../../../shared/types'
+import { chatService } from '../services/chatService'
+
+interface ChatState {
+ chats: Chat[]
+ currentChat: Chat | null
+ messages: Record
+ typingUsers: Record
+ onlineUsers: Set
+ loading: boolean
+ error: string | null
+}
+
+interface ChatActions {
+ // Chat management
+ loadChats: () => Promise
+ selectChat: (chatId: string) => void
+ createChat: (data: {
+ type: 'direct' | 'group'
+ name?: string
+ participantIds: string[]
+ }) => Promise
+ updateChat: (chatId: string, data: Partial) => Promise
+ deleteChat: (chatId: string) => Promise
+
+ // Message management
+ loadMessages: (chatId: string, page?: number) => Promise
+ sendMessage: (chatId: string, content: string, attachments?: File[]) => Promise
+ editMessage: (messageId: string, content: string) => Promise
+ deleteMessage: (messageId: string) => Promise
+ addReaction: (messageId: string, emoji: string) => Promise
+ removeReaction: (messageId: string, emoji: string) => Promise
+
+ // Real-time updates
+ addMessage: (message: Message) => void
+ updateMessage: (message: Message) => void
+ removeMessage: (messageId: string) => void
+ applyChatUpdate: (chat: Chat) => void
+
+ // Typing indicators
+ setTyping: (chatId: string, userId: string, isTyping: boolean) => void
+
+ // User status
+ setUserOnline: (userId: string, isOnline: boolean) => void
+
+ // Utility
+ clearError: () => void
+ reset: () => void
+}
+
+export const useChatStore = create((set, get) => ({
+ // State
+ chats: [],
+ currentChat: null,
+ messages: {},
+ typingUsers: {},
+ onlineUsers: new Set(),
+ loading: false,
+ error: null,
+
+ // Actions
+ loadChats: async () => {
+ set({ loading: true, error: null })
+ try {
+ const chats = await chatService.getChats()
+ set({ chats, loading: false })
+ } catch (error: any) {
+ set({ error: error.message, loading: false })
+ }
+ },
+
+ selectChat: (chatId: string) => {
+ const { chats } = get()
+ const chat = chats.find(c => c.id === chatId)
+ if (chat) {
+ set({ currentChat: chat })
+ // Load messages if not already loaded
+ if (!get().messages[chatId]) {
+ get().loadMessages(chatId)
+ }
+ }
+ },
+
+ createChat: async (data) => {
+ set({ loading: true, error: null })
+ try {
+ const chat = await chatService.createChat(data)
+ set(state => ({
+ chats: [chat, ...state.chats],
+ currentChat: chat,
+ loading: false
+ }))
+ return chat
+ } catch (error: any) {
+ set({ error: error.message, loading: false })
+ throw error
+ }
+ },
+
+ updateChat: async (chatId: string, data: Partial) => {
+ try {
+ const updatedChat = await chatService.updateChat(chatId, data)
+ set(state => ({
+ chats: state.chats.map(chat =>
+ chat.id === chatId ? updatedChat : chat
+ ),
+ currentChat: state.currentChat?.id === chatId ? updatedChat : state.currentChat
+ }))
+ } catch (error: any) {
+ set({ error: error.message })
+ }
+ },
+
+ deleteChat: async (chatId: string) => {
+ try {
+ await chatService.deleteChat(chatId)
+ set(state => ({
+ chats: state.chats.filter(chat => chat.id !== chatId),
+ currentChat: state.currentChat?.id === chatId ? null : state.currentChat,
+ messages: Object.fromEntries(
+ Object.entries(state.messages).filter(([id]) => id !== chatId)
+ )
+ }))
+ } catch (error: any) {
+ set({ error: error.message })
+ }
+ },
+
+ loadMessages: async (chatId: string, page = 1) => {
+ set({ loading: true, error: null })
+ try {
+ const messages = await chatService.getMessages(chatId, page)
+ set(state => ({
+ messages: {
+ ...state.messages,
+ [chatId]: page === 1 ? messages : [...(state.messages[chatId] || []), ...messages]
+ },
+ loading: false
+ }))
+ } catch (error: any) {
+ set({ error: error.message, loading: false })
+ }
+ },
+
+ sendMessage: async (chatId: string, content: string, attachments?: File[]) => {
+ try {
+ await chatService.sendMessage(chatId, {
+ content,
+ type: 'text',
+ attachments
+ })
+ // Message will be added via socket event
+ } catch (error: any) {
+ set({ error: error.message })
+ }
+ },
+
+ editMessage: async (messageId: string, content: string) => {
+ try {
+ await chatService.editMessage(messageId, content)
+ // Message will be updated via socket event
+ } catch (error: any) {
+ set({ error: error.message })
+ }
+ },
+
+ deleteMessage: async (messageId: string) => {
+ try {
+ await chatService.deleteMessage(messageId)
+ // Message will be removed via socket event
+ } catch (error: any) {
+ set({ error: error.message })
+ }
+ },
+
+ addReaction: async (messageId: string, emoji: string) => {
+ try {
+ await chatService.addReaction(messageId, emoji)
+ // Reaction will be updated via socket event
+ } catch (error: any) {
+ set({ error: error.message })
+ }
+ },
+
+ removeReaction: async (messageId: string, emoji: string) => {
+ try {
+ await chatService.removeReaction(messageId, emoji)
+ // Reaction will be updated via socket event
+ } catch (error: any) {
+ set({ error: error.message })
+ }
+ },
+
+ // Real-time updates
+ addMessage: (message: Message) => {
+ set(state => ({
+ messages: {
+ ...state.messages,
+ [message.chatId]: [...(state.messages[message.chatId] || []), message]
+ },
+ chats: state.chats.map(chat =>
+ chat.id === message.chatId
+ ? { ...chat, lastMessage: message, updatedAt: new Date() }
+ : chat
+ )
+ }))
+ },
+
+ updateMessage: (message: Message) => {
+ set(state => ({
+ messages: {
+ ...state.messages,
+ [message.chatId]: (state.messages[message.chatId] || []).map(msg =>
+ msg.id === message.id ? message : msg
+ )
+ }
+ }))
+ },
+
+ removeMessage: (messageId: string) => {
+ set(state => {
+ const newMessages = { ...state.messages }
+ Object.keys(newMessages).forEach(chatId => {
+ newMessages[chatId] = newMessages[chatId].filter(msg => msg.id !== messageId)
+ })
+ return { messages: newMessages }
+ })
+ },
+
+ applyChatUpdate: (chat: Chat) => {
+ set(state => ({
+ chats: state.chats.map(c => c.id === chat.id ? chat : c),
+ currentChat: state.currentChat?.id === chat.id ? chat : state.currentChat
+ }))
+ },
+
+ setTyping: (chatId: string, userId: string, isTyping: boolean) => {
+ set(state => {
+ const typingUsers = { ...state.typingUsers }
+ const currentTyping = typingUsers[chatId] || []
+
+ if (isTyping) {
+ if (!currentTyping.includes(userId)) {
+ typingUsers[chatId] = [...currentTyping, userId]
+ }
+ } else {
+ typingUsers[chatId] = currentTyping.filter(id => id !== userId)
+ if (typingUsers[chatId].length === 0) {
+ delete typingUsers[chatId]
+ }
+ }
+
+ return { typingUsers }
+ })
+ },
+
+ setUserOnline: (userId: string, isOnline: boolean) => {
+ set(state => {
+ const onlineUsers = new Set(state.onlineUsers)
+ if (isOnline) {
+ onlineUsers.add(userId)
+ } else {
+ onlineUsers.delete(userId)
+ }
+ return { onlineUsers }
+ })
+ },
+
+ clearError: () => {
+ set({ error: null })
+ },
+
+ reset: () => {
+ set({
+ chats: [],
+ currentChat: null,
+ messages: {},
+ typingUsers: {},
+ onlineUsers: new Set(),
+ loading: false,
+ error: null
+ })
+ }
+}))
diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9848496382660c30214fe5b17c6bb790c2f82047
--- /dev/null
+++ b/client/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_API_URL?: string
+ readonly VITE_SOCKET_URL?: string
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv
+}
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..7cab475ea510e21d2fecfbb20e2d445d4464ab7f
--- /dev/null
+++ b/client/tailwind.config.js
@@ -0,0 +1,76 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ darkMode: ["class"],
+ content: [
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
+ ],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: 0 },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: 0 },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..f91e30131d94486b1ed5a8f8af060f47c9f0f10d
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Path mapping */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json
new file mode 100644
index 0000000000000000000000000000000000000000..42872c59f5b01c9155864572bc2fbd5833a7406c
--- /dev/null
+++ b/client/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/client/vite.config.ts b/client/vite.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..77b1cd4e63d875c62a74b663699e321deb35e3ac
--- /dev/null
+++ b/client/vite.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3001',
+ changeOrigin: true,
+ },
+ '/socket.io': {
+ target: 'http://localhost:3001',
+ changeOrigin: true,
+ ws: true,
+ },
+ },
+ },
+})
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..dd55446d9408484d832bc00e7971596998d5639c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "chat-app",
+ "version": "1.0.0",
+ "description": "A modern chat application with Telegram-like features",
+ "main": "index.js",
+ "scripts": {
+ "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
+ "dev:server": "cd server && npm run dev",
+ "dev:client": "cd client && npm run dev",
+ "build": "npm run build:client && npm run build:server",
+ "build:client": "cd client && npm run build",
+ "build:server": "cd server && npm run build",
+ "start": "cd server && npm run start",
+ "test": "npm run test:client && npm run test:server",
+ "test:client": "cd client && npm run test",
+ "test:server": "cd server && npm run test",
+ "lint": "npm run lint:client && npm run lint:server",
+ "lint:client": "cd client && npm run lint",
+ "lint:server": "cd server && npm run lint",
+ "install:all": "npm install && cd client && npm install && cd ../server && npm install"
+ },
+ "keywords": [
+ "chat",
+ "messaging",
+ "real-time",
+ "socket.io",
+ "react",
+ "node.js",
+ "typescript"
+ ],
+ "author": "Your Name",
+ "license": "MIT",
+ "devDependencies": {
+ "concurrently": "^8.2.2"
+ },
+ "workspaces": [
+ "client",
+ "server",
+ "shared"
+ ]
+}
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..3967909e6c8d1faddad6bb0f2a3a4eb9e4656f22
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,50 @@
+# Server Configuration
+NODE_ENV=development
+PORT=3001
+HOST=localhost
+
+# Database Configuration (Supabase)
+DATABASE_URL=postgresql://username:password@localhost:5432/chatapp
+SUPABASE_URL=your_supabase_url
+SUPABASE_SERVICE_KEY=your_supabase_service_key
+
+# JWT Configuration
+JWT_SECRET=your_super_secret_jwt_key_here
+JWT_EXPIRES_IN=7d
+JWT_REFRESH_SECRET=your_refresh_token_secret
+JWT_REFRESH_EXPIRES_IN=30d
+
+# Redis Configuration (Optional - for caching and sessions)
+REDIS_URL=redis://localhost:6379
+REDIS_PASSWORD=
+
+# File Upload Configuration
+MAX_FILE_SIZE=10485760
+ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,audio/mp3,audio/wav,application/pdf,text/plain
+UPLOAD_PATH=uploads
+
+# Email Configuration (for notifications)
+SMTP_HOST=smtp.gmail.com
+SMTP_PORT=587
+SMTP_USER=your_email@gmail.com
+SMTP_PASS=your_app_password
+FROM_EMAIL=noreply@chatapp.com
+FROM_NAME=ChatApp
+
+# Rate Limiting
+RATE_LIMIT_WINDOW_MS=900000
+RATE_LIMIT_MAX_REQUESTS=100
+
+# CORS Configuration
+CORS_ORIGIN=http://localhost:5173
+
+# Admin Configuration
+ADMIN_EMAIL=admin@chatapp.com
+ADMIN_PASSWORD=admin123456
+
+# Logging
+LOG_LEVEL=info
+LOG_FILE=logs/app.log
+
+# WebSocket Configuration
+SOCKET_CORS_ORIGIN=http://localhost:5173
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..d8a541aad1d36af331efba3f03df578383bb437a
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,70 @@
+{
+ "name": "chat-app-server",
+ "version": "1.0.0",
+ "description": "Chat application backend server",
+ "main": "dist/index.js",
+ "scripts": {
+ "dev": "nodemon src/index.ts",
+ "build": "tsc",
+ "start": "node dist/index.js",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "lint": "eslint src --ext .ts",
+ "lint:fix": "eslint src --ext .ts --fix",
+ "migrate": "npx prisma migrate dev",
+ "db:generate": "npx prisma generate",
+ "db:push": "npx prisma db push",
+ "db:studio": "npx prisma studio"
+ },
+ "keywords": [
+ "chat",
+ "socket.io",
+ "express",
+ "typescript",
+ "node.js"
+ ],
+ "author": "Your Name",
+ "license": "MIT",
+ "dependencies": {
+ "express": "^4.18.2",
+ "socket.io": "^4.7.4",
+ "cors": "^2.8.5",
+ "helmet": "^7.1.0",
+ "bcryptjs": "^2.4.3",
+ "jsonwebtoken": "^9.0.2",
+ "multer": "^1.4.5-lts.1",
+ "sharp": "^0.32.6",
+ "joi": "^17.11.0",
+ "dotenv": "^16.3.1",
+ "@supabase/supabase-js": "^2.38.4",
+ "prisma": "^5.6.0",
+ "@prisma/client": "^5.6.0",
+ "redis": "^4.6.10",
+ "nodemailer": "^6.9.7",
+ "express-rate-limit": "^7.1.5",
+ "compression": "^1.7.4",
+ "morgan": "^1.10.0",
+ "uuid": "^9.0.1"
+ },
+ "devDependencies": {
+ "@types/express": "^4.17.21",
+ "@types/cors": "^2.8.17",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/jsonwebtoken": "^9.0.5",
+ "@types/multer": "^1.4.11",
+ "@types/node": "^20.9.0",
+ "@types/compression": "^1.7.5",
+ "@types/morgan": "^1.9.9",
+ "@types/uuid": "^9.0.7",
+ "@types/nodemailer": "^6.4.14",
+ "@typescript-eslint/eslint-plugin": "^6.10.0",
+ "@typescript-eslint/parser": "^6.10.0",
+ "eslint": "^8.53.0",
+ "jest": "^29.7.0",
+ "@types/jest": "^29.5.8",
+ "ts-jest": "^29.1.1",
+ "nodemon": "^3.0.1",
+ "ts-node": "^10.9.1",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma
new file mode 100644
index 0000000000000000000000000000000000000000..f1b4e66d3cc8abefb56af87c2140a29a0359ff02
--- /dev/null
+++ b/server/prisma/schema.prisma
@@ -0,0 +1,209 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id String @id @default(cuid())
+ email String @unique
+ username String @unique
+ displayName String
+ password String
+ avatar String?
+ bio String?
+ isOnline Boolean @default(false)
+ lastSeen DateTime @default(now())
+ isAdmin Boolean @default(false)
+ isVerified Boolean @default(false)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relations
+ sentMessages Message[] @relation("MessageSender")
+ chatParticipants ChatParticipant[]
+ messageReactions MessageReaction[]
+ ownedGroups Group[] @relation("GroupOwner")
+ notifications Notification[]
+ userSessions UserSession[]
+
+ @@map("users")
+}
+
+model Chat {
+ id String @id @default(cuid())
+ type ChatType @default(DIRECT)
+ name String?
+ description String?
+ avatar String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relations
+ participants ChatParticipant[]
+ messages Message[]
+ group Group?
+
+ @@map("chats")
+}
+
+model ChatParticipant {
+ id String @id @default(cuid())
+ chatId String
+ userId String
+ role ParticipantRole @default(MEMBER)
+ permissions Json?
+ joinedAt DateTime @default(now())
+ leftAt DateTime?
+
+ // Relations
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([chatId, userId])
+ @@map("chat_participants")
+}
+
+model Message {
+ id String @id @default(cuid())
+ chatId String
+ senderId String
+ content String
+ type MessageType @default(TEXT)
+ replyToId String?
+ isEdited Boolean @default(false)
+ isDeleted Boolean @default(false)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relations
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
+ sender User @relation("MessageSender", fields: [senderId], references: [id], onDelete: Cascade)
+ replyTo Message? @relation("MessageReply", fields: [replyToId], references: [id])
+ replies Message[] @relation("MessageReply")
+ attachments MessageAttachment[]
+ reactions MessageReaction[]
+
+ @@map("messages")
+}
+
+model MessageAttachment {
+ id String @id @default(cuid())
+ messageId String
+ type String
+ name String
+ url String
+ size Int
+ mimeType String
+ thumbnail String?
+
+ // Relations
+ message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
+
+ @@map("message_attachments")
+}
+
+model MessageReaction {
+ id String @id @default(cuid())
+ messageId String
+ userId String
+ emoji String
+ createdAt DateTime @default(now())
+
+ // Relations
+ message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([messageId, userId, emoji])
+ @@map("message_reactions")
+}
+
+model Group {
+ id String @id @default(cuid())
+ chatId String @unique
+ name String
+ description String?
+ avatar String?
+ type GroupType @default(PRIVATE)
+ maxMembers Int @default(100)
+ ownerId String
+ settings Json?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Relations
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
+ owner User @relation("GroupOwner", fields: [ownerId], references: [id])
+
+ @@map("groups")
+}
+
+model Notification {
+ id String @id @default(cuid())
+ userId String
+ type NotificationType
+ title String
+ content String
+ data Json?
+ isRead Boolean @default(false)
+ createdAt DateTime @default(now())
+
+ // Relations
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@map("notifications")
+}
+
+model UserSession {
+ id String @id @default(cuid())
+ userId String
+ token String @unique
+ userAgent String?
+ ipAddress String?
+ expiresAt DateTime
+ createdAt DateTime @default(now())
+
+ // Relations
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@map("user_sessions")
+}
+
+// Enums
+enum ChatType {
+ DIRECT
+ GROUP
+}
+
+enum ParticipantRole {
+ MEMBER
+ ADMIN
+ OWNER
+}
+
+enum MessageType {
+ TEXT
+ IMAGE
+ FILE
+ AUDIO
+ VIDEO
+ SYSTEM
+}
+
+enum GroupType {
+ PUBLIC
+ PRIVATE
+}
+
+enum NotificationType {
+ MESSAGE
+ MENTION
+ GROUP_INVITE
+ SYSTEM
+}
diff --git a/server/src/config/database.ts b/server/src/config/database.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a9ff50ef830c6cf9560d733255fbc62ed6daad52
--- /dev/null
+++ b/server/src/config/database.ts
@@ -0,0 +1,69 @@
+import { PrismaClient } from '@prisma/client'
+
+declare global {
+ var __prisma: PrismaClient | undefined
+}
+
+// Prevent multiple instances of Prisma Client in development
+const prisma = globalThis.__prisma || new PrismaClient({
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
+})
+
+if (process.env.NODE_ENV === 'development') {
+ globalThis.__prisma = prisma
+}
+
+export { prisma }
+
+export async function initializeDatabase() {
+ try {
+ // Test the connection
+ await prisma.$connect()
+ console.log('✅ Database connected successfully')
+
+ // Run any initialization logic here
+ await createDefaultAdmin()
+
+ } catch (error) {
+ console.error('❌ Database connection failed:', error)
+ throw error
+ }
+}
+
+async function createDefaultAdmin() {
+ try {
+ const adminEmail = process.env.ADMIN_EMAIL || 'admin@chatapp.com'
+ const adminPassword = process.env.ADMIN_PASSWORD || 'admin123456'
+
+ // Check if admin user already exists
+ const existingAdmin = await prisma.user.findUnique({
+ where: { email: adminEmail }
+ })
+
+ if (!existingAdmin) {
+ const bcrypt = await import('bcryptjs')
+ const hashedPassword = await bcrypt.hash(adminPassword, 12)
+
+ await prisma.user.create({
+ data: {
+ email: adminEmail,
+ username: 'admin',
+ displayName: 'Administrator',
+ password: hashedPassword,
+ isAdmin: true,
+ isVerified: true,
+ }
+ })
+
+ console.log('✅ Default admin user created')
+ console.log(`📧 Admin email: ${adminEmail}`)
+ console.log(`🔑 Admin password: ${adminPassword}`)
+ }
+ } catch (error) {
+ console.error('❌ Failed to create default admin:', error)
+ }
+}
+
+export async function disconnectDatabase() {
+ await prisma.$disconnect()
+}
diff --git a/server/src/index.ts b/server/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e2a78675dcba5472a57b3bd04e19784ef973a96d
--- /dev/null
+++ b/server/src/index.ts
@@ -0,0 +1,157 @@
+import express from 'express'
+import { createServer } from 'http'
+import { Server } from 'socket.io'
+import cors from 'cors'
+import helmet from 'helmet'
+import compression from 'compression'
+import morgan from 'morgan'
+import dotenv from 'dotenv'
+import path from 'path'
+
+// Load environment variables
+dotenv.config()
+
+// Import routes and middleware
+import authRoutes from './routes/auth'
+import userRoutes from './routes/users'
+import chatRoutes from './routes/chats'
+import messageRoutes from './routes/messages'
+import uploadRoutes from './routes/upload'
+import adminRoutes from './routes/admin'
+
+// Import middleware
+import { errorHandler } from './middleware/errorHandler'
+import { rateLimiter } from './middleware/rateLimiter'
+import { authMiddleware } from './middleware/auth'
+
+// Import socket handlers
+import { setupSocketHandlers } from './socket'
+
+// Import database
+import { initializeDatabase } from './config/database'
+
+const app = express()
+const server = createServer(app)
+
+// Initialize Socket.IO
+const io = new Server(server, {
+ cors: {
+ origin: process.env.CORS_ORIGIN || "http://localhost:5173",
+ methods: ["GET", "POST"],
+ credentials: true
+ },
+ transports: ['websocket', 'polling']
+})
+
+// Middleware
+app.use(helmet({
+ crossOriginEmbedderPolicy: false,
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'"],
+ scriptSrc: ["'self'"],
+ imgSrc: ["'self'", "data:", "https:"],
+ },
+ },
+}))
+
+app.use(cors({
+ origin: process.env.CORS_ORIGIN || "http://localhost:5173",
+ credentials: true
+}))
+
+app.use(compression())
+app.use(morgan('combined'))
+app.use(express.json({ limit: '10mb' }))
+app.use(express.urlencoded({ extended: true, limit: '10mb' }))
+
+// Rate limiting
+app.use(rateLimiter)
+
+// Static files
+app.use('/uploads', express.static(path.join(__dirname, '../uploads')))
+
+// Health check
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'OK',
+ timestamp: new Date().toISOString(),
+ uptime: process.uptime()
+ })
+})
+
+// API Routes
+app.use('/api/auth', authRoutes)
+app.use('/api/users', authMiddleware, userRoutes)
+app.use('/api/chats', authMiddleware, chatRoutes)
+app.use('/api/messages', authMiddleware, messageRoutes)
+app.use('/api/upload', authMiddleware, uploadRoutes)
+app.use('/api/admin', authMiddleware, adminRoutes)
+
+// 404 handler
+app.use('*', (req, res) => {
+ res.status(404).json({
+ success: false,
+ error: 'Route not found'
+ })
+})
+
+// Error handling middleware
+app.use(errorHandler)
+
+// Setup Socket.IO handlers
+setupSocketHandlers(io)
+
+// Initialize database and start server
+async function startServer() {
+ try {
+ // Initialize database connection
+ await initializeDatabase()
+ console.log('Database connected successfully')
+
+ const PORT = process.env.PORT || 3001
+ const HOST = process.env.HOST || 'localhost'
+
+ server.listen(PORT, () => {
+ console.log(`🚀 Server running on http://${HOST}:${PORT}`)
+ console.log(`📡 Socket.IO server ready`)
+ console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`)
+ })
+ } catch (error) {
+ console.error('Failed to start server:', error)
+ process.exit(1)
+ }
+}
+
+// Graceful shutdown
+process.on('SIGTERM', () => {
+ console.log('SIGTERM received, shutting down gracefully')
+ server.close(() => {
+ console.log('Server closed')
+ process.exit(0)
+ })
+})
+
+process.on('SIGINT', () => {
+ console.log('SIGINT received, shutting down gracefully')
+ server.close(() => {
+ console.log('Server closed')
+ process.exit(0)
+ })
+})
+
+// Handle uncaught exceptions
+process.on('uncaughtException', (error) => {
+ console.error('Uncaught Exception:', error)
+ process.exit(1)
+})
+
+process.on('unhandledRejection', (reason, promise) => {
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason)
+ process.exit(1)
+})
+
+startServer()
+
+export { app, server, io }
diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6d2ad7bc1446d0d381c777c133ddf48063a480af
--- /dev/null
+++ b/server/src/middleware/auth.ts
@@ -0,0 +1,158 @@
+import { Request, Response, NextFunction } from 'express'
+import jwt from 'jsonwebtoken'
+import { prisma } from '../config/database'
+
+export interface AuthRequest extends Request {
+ // Use a loose type here to avoid dependency on generated Prisma types
+ user?: any
+}
+
+export const authMiddleware = async (
+ req: AuthRequest,
+ res: Response,
+ next: NextFunction
+) => {
+ try {
+ const authHeader = req.headers.authorization
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({
+ success: false,
+ error: 'Access token required'
+ })
+ }
+
+ const token = authHeader.substring(7) // Remove 'Bearer ' prefix
+
+ if (!token) {
+ return res.status(401).json({
+ success: false,
+ error: 'Access token required'
+ })
+ }
+
+ // Verify JWT token
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }
+
+ // Get user from database
+ const user = await prisma.user.findUnique({
+ where: { id: decoded.userId },
+ select: {
+ id: true,
+ email: true,
+ username: true,
+ displayName: true,
+ avatar: true,
+ bio: true,
+ isOnline: true,
+ lastSeen: true,
+ isAdmin: true,
+ isVerified: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ })
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ error: 'Invalid token - user not found'
+ })
+ }
+
+ // Attach user to request
+ req.user = user
+ next()
+ } catch (error) {
+ if (error instanceof jwt.JsonWebTokenError) {
+ return res.status(401).json({
+ success: false,
+ error: 'Invalid token'
+ })
+ }
+
+ if (error instanceof jwt.TokenExpiredError) {
+ return res.status(401).json({
+ success: false,
+ error: 'Token expired'
+ })
+ }
+
+ console.error('Auth middleware error:', error)
+ return res.status(500).json({
+ success: false,
+ error: 'Authentication failed'
+ })
+ }
+}
+
+export const adminMiddleware = (
+ req: AuthRequest,
+ res: Response,
+ next: NextFunction
+) => {
+ if (!req.user) {
+ return res.status(401).json({
+ success: false,
+ error: 'Authentication required'
+ })
+ }
+
+ if (!req.user.isAdmin) {
+ return res.status(403).json({
+ success: false,
+ error: 'Admin access required'
+ })
+ }
+
+ next()
+}
+
+export const optionalAuthMiddleware = async (
+ req: AuthRequest,
+ res: Response,
+ next: NextFunction
+) => {
+ try {
+ const authHeader = req.headers.authorization
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return next()
+ }
+
+ const token = authHeader.substring(7)
+
+ if (!token) {
+ return next()
+ }
+
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }
+
+ const user = await prisma.user.findUnique({
+ where: { id: decoded.userId },
+ select: {
+ id: true,
+ email: true,
+ username: true,
+ displayName: true,
+ avatar: true,
+ bio: true,
+ isOnline: true,
+ lastSeen: true,
+ isAdmin: true,
+ isVerified: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ })
+
+ if (user) {
+ req.user = user
+ }
+
+ next()
+ } catch (error) {
+ // Ignore auth errors for optional auth
+ next()
+ }
+}
diff --git a/server/src/middleware/errorHandler.ts b/server/src/middleware/errorHandler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6a7931d88fa2f0ef4fc673eb13a9f8af09ca2ead
--- /dev/null
+++ b/server/src/middleware/errorHandler.ts
@@ -0,0 +1,104 @@
+import { Request, Response, NextFunction } from 'express'
+import { Prisma } from '@prisma/client'
+
+export interface AppError extends Error {
+ statusCode?: number
+ code?: string
+}
+
+export const errorHandler = (
+ error: AppError,
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ console.error('Error:', error)
+
+ // Default error
+ let statusCode = error.statusCode || 500
+ let message = error.message || 'Internal server error'
+ let code = error.code || 'INTERNAL_ERROR'
+
+ // Prisma errors
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ switch (error.code) {
+ case 'P2002':
+ statusCode = 409
+ message = 'Resource already exists'
+ code = 'DUPLICATE_ERROR'
+ break
+ case 'P2025':
+ statusCode = 404
+ message = 'Resource not found'
+ code = 'NOT_FOUND'
+ break
+ case 'P2003':
+ statusCode = 400
+ message = 'Invalid reference'
+ code = 'INVALID_REFERENCE'
+ break
+ default:
+ statusCode = 400
+ message = 'Database error'
+ code = 'DATABASE_ERROR'
+ }
+ }
+
+ // Validation errors
+ if (error.name === 'ValidationError') {
+ statusCode = 400
+ code = 'VALIDATION_ERROR'
+ }
+
+ // JWT errors
+ if (error.name === 'JsonWebTokenError') {
+ statusCode = 401
+ message = 'Invalid token'
+ code = 'INVALID_TOKEN'
+ }
+
+ if (error.name === 'TokenExpiredError') {
+ statusCode = 401
+ message = 'Token expired'
+ code = 'TOKEN_EXPIRED'
+ }
+
+ // Multer errors (file upload)
+ if (error.code === 'LIMIT_FILE_SIZE') {
+ statusCode = 413
+ message = 'File too large'
+ code = 'FILE_TOO_LARGE'
+ }
+
+ if (error.code === 'LIMIT_FILE_COUNT') {
+ statusCode = 413
+ message = 'Too many files'
+ code = 'TOO_MANY_FILES'
+ }
+
+ // Don't expose internal errors in production
+ if (process.env.NODE_ENV === 'production' && statusCode === 500) {
+ message = 'Internal server error'
+ }
+
+ res.status(statusCode).json({
+ success: false,
+ error: message,
+ code,
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack })
+ })
+}
+
+export const notFoundHandler = (req: Request, res: Response) => {
+ res.status(404).json({
+ success: false,
+ error: 'Route not found',
+ code: 'NOT_FOUND'
+ })
+}
+
+export const asyncHandler = (fn: Function) => {
+ return (req: Request, res: Response, next: NextFunction) => {
+ Promise.resolve(fn(req, res, next)).catch(next)
+ }
+}
diff --git a/server/src/middleware/rateLimiter.ts b/server/src/middleware/rateLimiter.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ef801277f12da82905019ee28cb3c2c9947d4785
--- /dev/null
+++ b/server/src/middleware/rateLimiter.ts
@@ -0,0 +1,53 @@
+import rateLimit from 'express-rate-limit'
+
+// General rate limiter
+export const rateLimiter = rateLimit({
+ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
+ max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'), // limit each IP to 100 requests per windowMs
+ message: {
+ success: false,
+ error: 'Too many requests, please try again later',
+ code: 'RATE_LIMIT_EXCEEDED'
+ },
+ standardHeaders: true,
+ legacyHeaders: false,
+})
+
+// Strict rate limiter for auth endpoints
+export const authRateLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 5, // limit each IP to 5 requests per windowMs
+ message: {
+ success: false,
+ error: 'Too many authentication attempts, please try again later',
+ code: 'AUTH_RATE_LIMIT_EXCEEDED'
+ },
+ standardHeaders: true,
+ legacyHeaders: false,
+})
+
+// Message rate limiter
+export const messageRateLimiter = rateLimit({
+ windowMs: 60 * 1000, // 1 minute
+ max: 30, // limit each IP to 30 messages per minute
+ message: {
+ success: false,
+ error: 'Too many messages, please slow down',
+ code: 'MESSAGE_RATE_LIMIT_EXCEEDED'
+ },
+ standardHeaders: true,
+ legacyHeaders: false,
+})
+
+// File upload rate limiter
+export const uploadRateLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 10, // limit each IP to 10 uploads per 15 minutes
+ message: {
+ success: false,
+ error: 'Too many file uploads, please try again later',
+ code: 'UPLOAD_RATE_LIMIT_EXCEEDED'
+ },
+ standardHeaders: true,
+ legacyHeaders: false,
+})
diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6b4fde17cca4110e68e6085b9be84c09321bccec
--- /dev/null
+++ b/server/src/routes/admin.ts
@@ -0,0 +1,207 @@
+import { Router } from 'express'
+import { Request, Response } from 'express'
+
+const router = Router()
+
+// Middleware to check admin permissions
+const requireAdmin = (req: Request, res: Response, next: any) => {
+ // TODO: Implement admin check
+ // For now, just pass through
+ next()
+}
+
+// Get system statistics
+router.get('/stats', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ // TODO: Implement real statistics
+ res.json({
+ success: true,
+ data: {
+ totalUsers: 1234,
+ activeUsers: 567,
+ totalChats: 890,
+ totalMessages: 12345,
+ storageUsed: 2.5, // GB
+ serverUptime: 99.9 // %
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get statistics'
+ })
+ }
+})
+
+// Get all users (admin only)
+router.get('/users', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { page = 1, limit = 20, search } = req.query
+ // TODO: Implement user listing with pagination and search
+ res.json({
+ success: true,
+ data: {
+ users: [],
+ pagination: {
+ page: Number(page),
+ limit: Number(limit),
+ total: 0,
+ totalPages: 0
+ }
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get users'
+ })
+ }
+})
+
+// Get user details (admin only)
+router.get('/users/:id', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement get user details
+ res.json({
+ success: true,
+ data: {
+ id,
+ email: 'user@example.com',
+ displayName: 'User',
+ isActive: true,
+ createdAt: new Date(),
+ lastLogin: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get user details'
+ })
+ }
+})
+
+// Update user (admin only)
+router.put('/users/:id', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement user update
+ res.json({
+ success: true,
+ data: {
+ id,
+ ...req.body,
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to update user'
+ })
+ }
+})
+
+// Deactivate user (admin only)
+router.post('/users/:id/deactivate', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement user deactivation
+ res.json({
+ success: true,
+ message: 'User deactivated successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to deactivate user'
+ })
+ }
+})
+
+// Activate user (admin only)
+router.post('/users/:id/activate', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement user activation
+ res.json({
+ success: true,
+ message: 'User activated successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to activate user'
+ })
+ }
+})
+
+// Get all chats (admin only)
+router.get('/chats', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { page = 1, limit = 20 } = req.query
+ // TODO: Implement chat listing
+ res.json({
+ success: true,
+ data: {
+ chats: [],
+ pagination: {
+ page: Number(page),
+ limit: Number(limit),
+ total: 0,
+ totalPages: 0
+ }
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get chats'
+ })
+ }
+})
+
+// Delete chat (admin only)
+router.delete('/chats/:id', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement chat deletion
+ res.json({
+ success: true,
+ message: 'Chat deleted successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to delete chat'
+ })
+ }
+})
+
+// Get system logs (admin only)
+router.get('/logs', requireAdmin, async (req: Request, res: Response) => {
+ try {
+ const { page = 1, limit = 50, level } = req.query
+ // TODO: Implement log retrieval
+ res.json({
+ success: true,
+ data: {
+ logs: [],
+ pagination: {
+ page: Number(page),
+ limit: Number(limit),
+ total: 0,
+ totalPages: 0
+ }
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get logs'
+ })
+ }
+})
+
+export default router
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..21ac748cddfe10d8d8ed7daa8c1774059f8b6cdf
--- /dev/null
+++ b/server/src/routes/auth.ts
@@ -0,0 +1,295 @@
+import { Router } from 'express'
+import bcrypt from 'bcryptjs'
+import jwt from 'jsonwebtoken'
+import Joi from 'joi'
+import { prisma } from '../config/database'
+import { authMiddleware, AuthRequest } from '../middleware/auth'
+import { authRateLimiter } from '../middleware/rateLimiter'
+import { asyncHandler } from '../middleware/errorHandler'
+
+const router = Router()
+
+// Validation schemas
+const registerSchema = Joi.object({
+ email: Joi.string().email().required(),
+ username: Joi.string().alphanum().min(3).max(20).required(),
+ password: Joi.string().min(6).required(),
+ displayName: Joi.string().min(1).max(50).required(),
+})
+
+const loginSchema = Joi.object({
+ email: Joi.string().email().required(),
+ password: Joi.string().required(),
+})
+
+const changePasswordSchema = Joi.object({
+ currentPassword: Joi.string().required(),
+ newPassword: Joi.string().min(6).required(),
+})
+
+// Helper function to generate JWT token
+const generateToken = (userId: string) => {
+ return jwt.sign(
+ { userId },
+ process.env.JWT_SECRET!,
+ { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
+ )
+}
+
+// Register
+router.post('/register', authRateLimiter, asyncHandler(async (req, res) => {
+ const { error, value } = registerSchema.validate(req.body)
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ error: error.details[0].message
+ })
+ }
+
+ const { email, username, password, displayName } = value
+
+ // Check if user already exists
+ const existingUser = await prisma.user.findFirst({
+ where: {
+ OR: [
+ { email },
+ { username }
+ ]
+ }
+ })
+
+ if (existingUser) {
+ return res.status(409).json({
+ success: false,
+ error: existingUser.email === email ? 'Email already registered' : 'Username already taken'
+ })
+ }
+
+ // Hash password
+ const hashedPassword = await bcrypt.hash(password, 12)
+
+ // Create user
+ const user = await prisma.user.create({
+ data: {
+ email,
+ username,
+ password: hashedPassword,
+ displayName,
+ },
+ select: {
+ id: true,
+ email: true,
+ username: true,
+ displayName: true,
+ avatar: true,
+ bio: true,
+ isOnline: true,
+ lastSeen: true,
+ isAdmin: true,
+ isVerified: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ })
+
+ // Generate token
+ const token = generateToken(user.id)
+
+ res.status(201).json({
+ success: true,
+ data: {
+ user,
+ token
+ }
+ })
+}))
+
+// Login
+router.post('/login', authRateLimiter, asyncHandler(async (req, res) => {
+ const { error, value } = loginSchema.validate(req.body)
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ error: error.details[0].message
+ })
+ }
+
+ const { email, password } = value
+
+ // Find user
+ const user = await prisma.user.findUnique({
+ where: { email }
+ })
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ error: 'Invalid credentials'
+ })
+ }
+
+ // Check password
+ const isValidPassword = await bcrypt.compare(password, user.password)
+ if (!isValidPassword) {
+ return res.status(401).json({
+ success: false,
+ error: 'Invalid credentials'
+ })
+ }
+
+ // Update user online status
+ await prisma.user.update({
+ where: { id: user.id },
+ data: {
+ isOnline: true,
+ lastSeen: new Date()
+ }
+ })
+
+ // Generate token
+ const token = generateToken(user.id)
+
+ // Return user without password
+ const { password: _, ...userWithoutPassword } = user
+
+ res.json({
+ success: true,
+ data: {
+ user: userWithoutPassword,
+ token
+ }
+ })
+}))
+
+// Logout
+router.post('/logout', authMiddleware, asyncHandler(async (req: AuthRequest, res) => {
+ // Update user offline status
+ await prisma.user.update({
+ where: { id: req.user!.id },
+ data: {
+ isOnline: false,
+ lastSeen: new Date()
+ }
+ })
+
+ res.json({
+ success: true,
+ message: 'Logged out successfully'
+ })
+}))
+
+// Get current user
+router.get('/me', authMiddleware, asyncHandler(async (req: AuthRequest, res) => {
+ res.json({
+ success: true,
+ data: req.user
+ })
+}))
+
+// Change password
+router.post('/change-password', authMiddleware, asyncHandler(async (req: AuthRequest, res) => {
+ const { error, value } = changePasswordSchema.validate(req.body)
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ error: error.details[0].message
+ })
+ }
+
+ const { currentPassword, newPassword } = value
+
+ // Get user with password
+ const user = await prisma.user.findUnique({
+ where: { id: req.user!.id }
+ })
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ error: 'User not found'
+ })
+ }
+
+ // Check current password
+ const isValidPassword = await bcrypt.compare(currentPassword, user.password)
+ if (!isValidPassword) {
+ return res.status(401).json({
+ success: false,
+ error: 'Current password is incorrect'
+ })
+ }
+
+ // Hash new password
+ const hashedNewPassword = await bcrypt.hash(newPassword, 12)
+
+ // Update password
+ await prisma.user.update({
+ where: { id: user.id },
+ data: {
+ password: hashedNewPassword
+ }
+ })
+
+ res.json({
+ success: true,
+ message: 'Password changed successfully'
+ })
+}))
+
+// Refresh token (optional - for token refresh functionality)
+router.post('/refresh', asyncHandler(async (req, res) => {
+ const { refreshToken } = req.body
+
+ if (!refreshToken) {
+ return res.status(401).json({
+ success: false,
+ error: 'Refresh token required'
+ })
+ }
+
+ try {
+ const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as { userId: string }
+
+ const user = await prisma.user.findUnique({
+ where: { id: decoded.userId },
+ select: {
+ id: true,
+ email: true,
+ username: true,
+ displayName: true,
+ avatar: true,
+ bio: true,
+ isOnline: true,
+ lastSeen: true,
+ isAdmin: true,
+ isVerified: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ })
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ error: 'Invalid refresh token'
+ })
+ }
+
+ const newToken = generateToken(user.id)
+
+ res.json({
+ success: true,
+ data: {
+ user,
+ token: newToken
+ }
+ })
+ } catch (error) {
+ return res.status(401).json({
+ success: false,
+ error: 'Invalid refresh token'
+ })
+ }
+}))
+
+export default router
diff --git a/server/src/routes/chats.ts b/server/src/routes/chats.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8bcd29f42a4c8590c9cbff115f60943940a51597
--- /dev/null
+++ b/server/src/routes/chats.ts
@@ -0,0 +1,196 @@
+import { Router } from 'express'
+import { Request, Response } from 'express'
+
+const router = Router()
+
+// Get all chats for current user
+router.get('/', async (req: Request, res: Response) => {
+ try {
+ // TODO: Implement get chats
+ res.json({
+ success: true,
+ data: []
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get chats'
+ })
+ }
+})
+
+// Create new chat
+router.post('/', async (req: Request, res: Response) => {
+ try {
+ const { type, name, participantIds } = req.body
+ // TODO: Implement create chat
+ res.status(201).json({
+ success: true,
+ data: {
+ id: 'chat-1',
+ type,
+ name,
+ participants: [],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to create chat'
+ })
+ }
+})
+
+// Get chat by ID
+router.get('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement get chat by ID
+ res.json({
+ success: true,
+ data: {
+ id,
+ type: 'direct',
+ participants: [],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get chat'
+ })
+ }
+})
+
+// Update chat
+router.put('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement update chat
+ res.json({
+ success: true,
+ data: {
+ id,
+ ...req.body,
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to update chat'
+ })
+ }
+})
+
+// Delete chat
+router.delete('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement delete chat
+ res.json({
+ success: true,
+ message: 'Chat deleted successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to delete chat'
+ })
+ }
+})
+
+// Get chat messages
+router.get('/:id/messages', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ const { page = 1, limit = 50 } = req.query
+ // TODO: Implement get chat messages
+ res.json({
+ success: true,
+ data: {
+ messages: [],
+ pagination: {
+ page: Number(page),
+ limit: Number(limit),
+ total: 0,
+ totalPages: 0
+ }
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get messages'
+ })
+ }
+})
+
+// Send message to chat
+router.post('/:id/messages', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ const { content, type = 'text', attachments, replyTo } = req.body
+ // TODO: Implement send message
+ res.status(201).json({
+ success: true,
+ data: {
+ id: 'message-1',
+ chatId: id,
+ senderId: 'user-1',
+ content,
+ type,
+ attachments: attachments || [],
+ replyTo,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to send message'
+ })
+ }
+})
+
+// Add member to chat
+router.post('/:id/members', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ const { userId } = req.body
+ // TODO: Implement add member
+ res.json({
+ success: true,
+ message: 'Member added successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to add member'
+ })
+ }
+})
+
+// Remove member from chat
+router.delete('/:id/members/:userId', async (req: Request, res: Response) => {
+ try {
+ const { id, userId } = req.params
+ // TODO: Implement remove member
+ res.json({
+ success: true,
+ message: 'Member removed successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to remove member'
+ })
+ }
+})
+
+export default router
diff --git a/server/src/routes/messages.ts b/server/src/routes/messages.ts
new file mode 100644
index 0000000000000000000000000000000000000000..35475141077a9f1a5aa6266ac23fe14866501ed8
--- /dev/null
+++ b/server/src/routes/messages.ts
@@ -0,0 +1,129 @@
+import { Router } from 'express'
+import { Request, Response } from 'express'
+
+const router = Router()
+
+// Get message by ID
+router.get('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement get message by ID
+ res.json({
+ success: true,
+ data: {
+ id,
+ chatId: 'chat-1',
+ senderId: 'user-1',
+ content: 'Sample message',
+ type: 'text',
+ attachments: [],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get message'
+ })
+ }
+})
+
+// Update message
+router.put('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ const { content } = req.body
+ // TODO: Implement update message
+ res.json({
+ success: true,
+ data: {
+ id,
+ content,
+ updatedAt: new Date(),
+ edited: true
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to update message'
+ })
+ }
+})
+
+// Delete message
+router.delete('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement delete message
+ res.json({
+ success: true,
+ message: 'Message deleted successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to delete message'
+ })
+ }
+})
+
+// Add reaction to message
+router.post('/:id/reactions', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ const { emoji } = req.body
+ // TODO: Implement add reaction
+ res.json({
+ success: true,
+ data: {
+ messageId: id,
+ emoji,
+ userId: 'user-1',
+ createdAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to add reaction'
+ })
+ }
+})
+
+// Remove reaction from message
+router.delete('/:id/reactions/:emoji', async (req: Request, res: Response) => {
+ try {
+ const { id, emoji } = req.params
+ // TODO: Implement remove reaction
+ res.json({
+ success: true,
+ message: 'Reaction removed successfully'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to remove reaction'
+ })
+ }
+})
+
+// Mark message as read
+router.post('/:id/read', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement mark as read
+ res.json({
+ success: true,
+ message: 'Message marked as read'
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to mark message as read'
+ })
+ }
+})
+
+export default router
diff --git a/server/src/routes/upload.ts b/server/src/routes/upload.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e706f118a3a326fac278d0e76dd55dd8a17b6e49
--- /dev/null
+++ b/server/src/routes/upload.ts
@@ -0,0 +1,170 @@
+import { Router } from 'express'
+import { Request, Response } from 'express'
+import multer from 'multer'
+import path from 'path'
+import fs from 'fs'
+
+const router = Router()
+
+// Configure multer for file uploads
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ const uploadDir = path.join(__dirname, '../../uploads')
+ if (!fs.existsSync(uploadDir)) {
+ fs.mkdirSync(uploadDir, { recursive: true })
+ }
+ cb(null, uploadDir)
+ },
+ filename: (req, file, cb) => {
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
+ cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname))
+ }
+})
+
+const upload = multer({
+ storage,
+ limits: {
+ fileSize: 10 * 1024 * 1024, // 10MB limit
+ },
+ fileFilter: (req, file, cb) => {
+ // Allow images, videos, audio, and documents
+ const allowedTypes = /jpeg|jpg|png|gif|webp|mp4|webm|mp3|wav|pdf|doc|docx|txt/
+ const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase())
+ const mimetype = allowedTypes.test(file.mimetype)
+
+ if (mimetype && extname) {
+ return cb(null, true)
+ } else {
+ cb(new Error('Invalid file type'))
+ }
+ }
+})
+
+// Upload single file
+router.post('/single', upload.single('file'), async (req: Request, res: Response) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({
+ success: false,
+ error: 'No file uploaded'
+ })
+ }
+
+ const fileUrl = `/uploads/${req.file.filename}`
+
+ res.json({
+ success: true,
+ data: {
+ id: req.file.filename,
+ filename: req.file.originalname,
+ url: fileUrl,
+ size: req.file.size,
+ mimetype: req.file.mimetype,
+ uploadedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to upload file'
+ })
+ }
+})
+
+// Upload multiple files
+router.post('/multiple', upload.array('files', 10), async (req: Request, res: Response) => {
+ try {
+ const files = req.files as Express.Multer.File[]
+
+ if (!files || files.length === 0) {
+ return res.status(400).json({
+ success: false,
+ error: 'No files uploaded'
+ })
+ }
+
+ const uploadedFiles = files.map(file => ({
+ id: file.filename,
+ filename: file.originalname,
+ url: `/uploads/${file.filename}`,
+ size: file.size,
+ mimetype: file.mimetype,
+ uploadedAt: new Date()
+ }))
+
+ res.json({
+ success: true,
+ data: uploadedFiles
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to upload files'
+ })
+ }
+})
+
+// Upload avatar
+router.post('/avatar', upload.single('avatar'), async (req: Request, res: Response) => {
+ try {
+ if (!req.file) {
+ return res.status(400).json({
+ success: false,
+ error: 'No avatar uploaded'
+ })
+ }
+
+ // Check if it's an image
+ if (!req.file.mimetype.startsWith('image/')) {
+ return res.status(400).json({
+ success: false,
+ error: 'Avatar must be an image'
+ })
+ }
+
+ const avatarUrl = `/uploads/${req.file.filename}`
+
+ // TODO: Update user avatar in database
+
+ res.json({
+ success: true,
+ data: {
+ avatarUrl,
+ uploadedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to upload avatar'
+ })
+ }
+})
+
+// Delete file
+router.delete('/:filename', async (req: Request, res: Response) => {
+ try {
+ const { filename } = req.params
+ const filePath = path.join(__dirname, '../../uploads', filename)
+
+ if (fs.existsSync(filePath)) {
+ fs.unlinkSync(filePath)
+ res.json({
+ success: true,
+ message: 'File deleted successfully'
+ })
+ } else {
+ res.status(404).json({
+ success: false,
+ error: 'File not found'
+ })
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to delete file'
+ })
+ }
+})
+
+export default router
diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e15c2e0c97f130a196bd3096b5abecf0edfdcb9e
--- /dev/null
+++ b/server/src/routes/users.ts
@@ -0,0 +1,93 @@
+import { Router } from 'express'
+import { Request, Response } from 'express'
+
+const router = Router()
+
+// Get current user profile
+router.get('/me', async (req: Request, res: Response) => {
+ try {
+ // TODO: Implement user profile retrieval
+ res.json({
+ success: true,
+ data: {
+ id: 'user-1',
+ email: 'user@example.com',
+ displayName: 'User',
+ avatar: null,
+ bio: null,
+ isOnline: true,
+ lastSeen: new Date(),
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get user profile'
+ })
+ }
+})
+
+// Update user profile
+router.put('/me', async (req: Request, res: Response) => {
+ try {
+ // TODO: Implement user profile update
+ res.json({
+ success: true,
+ data: {
+ id: 'user-1',
+ ...req.body,
+ updatedAt: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to update user profile'
+ })
+ }
+})
+
+// Search users
+router.get('/search', async (req: Request, res: Response) => {
+ try {
+ const { q } = req.query
+ // TODO: Implement user search
+ res.json({
+ success: true,
+ data: []
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to search users'
+ })
+ }
+})
+
+// Get user by ID
+router.get('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params
+ // TODO: Implement get user by ID
+ res.json({
+ success: true,
+ data: {
+ id,
+ displayName: 'User',
+ avatar: null,
+ bio: null,
+ isOnline: false,
+ lastSeen: new Date()
+ }
+ })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ error: 'Failed to get user'
+ })
+ }
+})
+
+export default router
diff --git a/server/src/socket/index.ts b/server/src/socket/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..96ee61603df7bdb45cd4f3d6f80711b4a4506d39
--- /dev/null
+++ b/server/src/socket/index.ts
@@ -0,0 +1,212 @@
+import { Server, Socket } from 'socket.io'
+import jwt from 'jsonwebtoken'
+
+interface AuthenticatedSocket extends Socket {
+ userId?: string
+ user?: any
+}
+
+// Store active connections
+const activeConnections = new Map()
+
+export function setupSocketHandlers(io: Server) {
+ // Authentication middleware
+ io.use(async (socket: AuthenticatedSocket, next) => {
+ try {
+ const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.replace('Bearer ', '')
+
+ if (!token) {
+ return next(new Error('Authentication error: No token provided'))
+ }
+
+ // TODO: Verify JWT token and get user
+ // For now, just mock the authentication
+ const decoded = { userId: 'user-1', email: 'user@example.com' }
+
+ socket.userId = decoded.userId
+ socket.user = decoded
+ next()
+ } catch (error) {
+ next(new Error('Authentication error: Invalid token'))
+ }
+ })
+
+ io.on('connection', (socket: AuthenticatedSocket) => {
+ console.log(`User ${socket.userId} connected`)
+
+ // Store the connection
+ if (socket.userId) {
+ activeConnections.set(socket.userId, socket)
+ }
+
+ // Join user to their personal room
+ if (socket.userId) {
+ socket.join(`user:${socket.userId}`)
+ }
+
+ // Handle joining chat rooms
+ socket.on('join_chat', (chatId: string) => {
+ socket.join(`chat:${chatId}`)
+ console.log(`User ${socket.userId} joined chat ${chatId}`)
+ })
+
+ // Handle leaving chat rooms
+ socket.on('leave_chat', (chatId: string) => {
+ socket.leave(`chat:${chatId}`)
+ console.log(`User ${socket.userId} left chat ${chatId}`)
+ })
+
+ // Handle sending messages
+ socket.on('send_message', async (data: {
+ chatId: string
+ content: string
+ type?: string
+ replyTo?: string
+ }) => {
+ try {
+ // TODO: Save message to database
+ const message = {
+ id: `msg-${Date.now()}`,
+ chatId: data.chatId,
+ senderId: socket.userId,
+ content: data.content,
+ type: data.type || 'text',
+ replyTo: data.replyTo,
+ createdAt: new Date(),
+ sender: socket.user
+ }
+
+ // Broadcast to all users in the chat
+ socket.to(`chat:${data.chatId}`).emit('new_message', message)
+
+ // Send confirmation back to sender
+ socket.emit('message_sent', { messageId: message.id, status: 'sent' })
+
+ console.log(`Message sent in chat ${data.chatId} by user ${socket.userId}`)
+ } catch (error) {
+ socket.emit('error', { message: 'Failed to send message' })
+ }
+ })
+
+ // Handle typing indicators
+ socket.on('typing_start', (chatId: string) => {
+ socket.to(`chat:${chatId}`).emit('user_typing', {
+ userId: socket.userId,
+ chatId,
+ isTyping: true
+ })
+ })
+
+ socket.on('typing_stop', (chatId: string) => {
+ socket.to(`chat:${chatId}`).emit('user_typing', {
+ userId: socket.userId,
+ chatId,
+ isTyping: false
+ })
+ })
+
+ // Handle message reactions
+ socket.on('add_reaction', async (data: {
+ messageId: string
+ emoji: string
+ }) => {
+ try {
+ // TODO: Save reaction to database
+ const reaction = {
+ messageId: data.messageId,
+ userId: socket.userId,
+ emoji: data.emoji,
+ createdAt: new Date()
+ }
+
+ // Broadcast to all connected users
+ io.emit('reaction_added', reaction)
+
+ console.log(`Reaction ${data.emoji} added to message ${data.messageId} by user ${socket.userId}`)
+ } catch (error) {
+ socket.emit('error', { message: 'Failed to add reaction' })
+ }
+ })
+
+ // Handle message status updates
+ socket.on('message_read', async (data: {
+ messageId: string
+ chatId: string
+ }) => {
+ try {
+ // TODO: Update message read status in database
+
+ // Notify sender that message was read
+ socket.to(`chat:${data.chatId}`).emit('message_status_updated', {
+ messageId: data.messageId,
+ status: 'read',
+ readBy: socket.userId,
+ readAt: new Date()
+ })
+
+ console.log(`Message ${data.messageId} marked as read by user ${socket.userId}`)
+ } catch (error) {
+ socket.emit('error', { message: 'Failed to update message status' })
+ }
+ })
+
+ // Handle user presence
+ socket.on('update_presence', (status: 'online' | 'away' | 'busy' | 'offline') => {
+ // TODO: Update user presence in database
+
+ // Broadcast presence update to all connected users
+ socket.broadcast.emit('user_presence_updated', {
+ userId: socket.userId,
+ status,
+ lastSeen: new Date()
+ })
+
+ console.log(`User ${socket.userId} presence updated to ${status}`)
+ })
+
+ // Handle disconnection
+ socket.on('disconnect', (reason) => {
+ console.log(`User ${socket.userId} disconnected: ${reason}`)
+
+ // Remove from active connections
+ if (socket.userId) {
+ activeConnections.delete(socket.userId)
+ }
+
+ // Update user presence to offline
+ socket.broadcast.emit('user_presence_updated', {
+ userId: socket.userId,
+ status: 'offline',
+ lastSeen: new Date()
+ })
+ })
+
+ // Handle errors
+ socket.on('error', (error) => {
+ console.error(`Socket error for user ${socket.userId}:`, error)
+ })
+ })
+
+ // Helper function to send message to specific user
+ const sendToUser = (userId: string, event: string, data: any) => {
+ const userSocket = activeConnections.get(userId)
+ if (userSocket) {
+ userSocket.emit(event, data)
+ return true
+ }
+ return false
+ }
+
+ // Helper function to send message to chat
+ const sendToChat = (chatId: string, event: string, data: any) => {
+ io.to(`chat:${chatId}`).emit(event, data)
+ }
+
+ // Export helper functions for use in other parts of the application
+ return {
+ sendToUser,
+ sendToChat,
+ getActiveConnections: () => activeConnections,
+ getConnectionCount: () => activeConnections.size
+ }
+}
diff --git a/server/tsconfig.json b/server/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..0d2a046eb5b4264c0ea1fb99cc523ecee5dda27d
--- /dev/null
+++ b/server/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020"],
+ "module": "CommonJS",
+ "moduleResolution": "node",
+ "outDir": "./dist",
+ "rootDir": "../",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "removeComments": true,
+ "noImplicitAny": false,
+ "noImplicitReturns": false,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@/shared/*": ["../shared/*"]
+ }
+ },
+ "include": [
+ "src/**/*",
+ "../shared/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/*.test.ts",
+ "**/*.spec.ts"
+ ]
+}
diff --git a/shared/types/index.ts b/shared/types/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..55764cd92a0f04beeef76a29a402862950818cfe
--- /dev/null
+++ b/shared/types/index.ts
@@ -0,0 +1,300 @@
+// User types
+export interface User {
+ id: string
+ email: string
+ username: string
+ displayName: string
+ avatar?: string
+ bio?: string
+ isOnline: boolean
+ lastSeen: Date
+ isAdmin: boolean
+ createdAt: Date
+ updatedAt: Date
+}
+
+export interface UserProfile {
+ id: string
+ userId: string
+ displayName: string
+ bio?: string
+ avatar?: string
+ theme: 'light' | 'dark' | 'system'
+ notifications: NotificationSettings
+ privacy: PrivacySettings
+}
+
+export interface NotificationSettings {
+ messages: boolean
+ mentions: boolean
+ groups: boolean
+ sounds: boolean
+ desktop: boolean
+}
+
+export interface PrivacySettings {
+ showOnlineStatus: boolean
+ showLastSeen: boolean
+ allowDirectMessages: boolean
+ allowGroupInvites: boolean
+}
+
+// Chat types
+export interface Chat {
+ id: string
+ type: 'direct' | 'group'
+ name?: string
+ description?: string
+ avatar?: string
+ participants: ChatParticipant[]
+ lastMessage?: Message
+ unreadCount: number
+ createdAt: Date
+ updatedAt: Date
+}
+
+export interface ChatParticipant {
+ id: string
+ chatId: string
+ userId: string
+ user: User
+ role: 'member' | 'admin' | 'owner'
+ joinedAt: Date
+ permissions: ChatPermissions
+}
+
+export interface ChatPermissions {
+ canSendMessages: boolean
+ canSendMedia: boolean
+ canAddMembers: boolean
+ canRemoveMembers: boolean
+ canEditChat: boolean
+ canDeleteMessages: boolean
+}
+
+// Message types
+export interface Message {
+ id: string
+ chatId: string
+ senderId: string
+ sender: User
+ content: string
+ type: MessageType
+ attachments: MessageAttachment[]
+ replyTo?: string
+ replyToMessage?: Message
+ reactions: MessageReaction[]
+ isEdited: boolean
+ isDeleted: boolean
+ createdAt: Date
+ updatedAt: Date
+}
+
+export type MessageType = 'text' | 'image' | 'file' | 'audio' | 'video' | 'system'
+
+export interface MessageAttachment {
+ id: string
+ messageId: string
+ type: 'image' | 'file' | 'audio' | 'video'
+ name: string
+ url: string
+ size: number
+ mimeType: string
+ thumbnail?: string
+}
+
+export interface MessageReaction {
+ id: string
+ messageId: string
+ userId: string
+ user: User
+ emoji: string
+ createdAt: Date
+}
+
+// Group types
+export interface Group {
+ id: string
+ name: string
+ description?: string
+ avatar?: string
+ type: 'public' | 'private'
+ memberCount: number
+ maxMembers: number
+ ownerId: string
+ owner: User
+ settings: GroupSettings
+ createdAt: Date
+ updatedAt: Date
+}
+
+export interface GroupSettings {
+ allowMemberInvites: boolean
+ requireApproval: boolean
+ allowFileSharing: boolean
+ allowVoiceMessages: boolean
+ messageHistory: 'visible' | 'hidden'
+ slowMode: number // seconds
+}
+
+// Socket events
+export interface SocketEvents {
+ // Connection
+ connect: () => void
+ disconnect: () => void
+
+ // Authentication
+ authenticate: (token: string) => void
+ authenticated: (user: User) => void
+
+ // Messages
+ 'message:send': (data: SendMessageData) => void
+ 'message:receive': (message: Message) => void
+ 'message:edit': (data: EditMessageData) => void
+ 'message:delete': (data: DeleteMessageData) => void
+ 'message:reaction': (data: MessageReactionData) => void
+
+ // Typing
+ 'typing:start': (data: TypingData) => void
+ 'typing:stop': (data: TypingData) => void
+ 'typing:update': (data: TypingData) => void
+
+ // User status
+ 'user:online': (userId: string) => void
+ 'user:offline': (userId: string) => void
+ 'user:status': (data: UserStatusData) => void
+
+ // Chat events
+ 'chat:join': (chatId: string) => void
+ 'chat:leave': (chatId: string) => void
+ 'chat:update': (chat: Chat) => void
+
+ // Group events
+ 'group:member:add': (data: GroupMemberData) => void
+ 'group:member:remove': (data: GroupMemberData) => void
+ 'group:update': (group: Group) => void
+}
+
+// Socket data types
+export interface SendMessageData {
+ chatId: string
+ content: string
+ type: MessageType
+ attachments?: File[]
+ replyTo?: string
+}
+
+export interface EditMessageData {
+ messageId: string
+ content: string
+}
+
+export interface DeleteMessageData {
+ messageId: string
+}
+
+export interface MessageReactionData {
+ messageId: string
+ emoji: string
+ action: 'add' | 'remove'
+}
+
+export interface TypingData {
+ chatId: string
+ userId: string
+}
+
+export interface UserStatusData {
+ userId: string
+ isOnline: boolean
+ lastSeen?: Date
+}
+
+export interface GroupMemberData {
+ groupId: string
+ userId: string
+ role?: 'member' | 'admin' | 'owner'
+}
+
+// API types
+export interface ApiResponse {
+ success: boolean
+ data?: T
+ error?: string
+ message?: string
+}
+
+export interface PaginatedResponse {
+ data: T[]
+ total: number
+ page: number
+ limit: number
+ hasMore: boolean
+}
+
+export interface LoginRequest {
+ email: string
+ password: string
+}
+
+export interface RegisterRequest {
+ email: string
+ username: string
+ password: string
+ displayName: string
+}
+
+export interface CreateChatRequest {
+ type: 'direct' | 'group'
+ name?: string
+ description?: string
+ participantIds: string[]
+}
+
+export interface UpdateProfileRequest {
+ displayName?: string
+ bio?: string
+ avatar?: File
+}
+
+// Admin types
+export interface AdminStats {
+ totalUsers: number
+ activeUsers: number
+ totalChats: number
+ totalMessages: number
+ storageUsed: number
+ serverUptime: number
+}
+
+export interface AdminUser extends User {
+ messageCount: number
+ chatCount: number
+ lastActivity: Date
+ status: 'active' | 'suspended' | 'banned'
+}
+
+export interface AdminChat extends Chat {
+ messageCount: number
+ activeMembers: number
+ reportCount: number
+ status: 'active' | 'archived' | 'suspended'
+}
+
+// Error types
+export interface AppError {
+ code: string
+ message: string
+ details?: any
+}
+
+export type ErrorCode =
+ | 'AUTH_REQUIRED'
+ | 'INVALID_CREDENTIALS'
+ | 'USER_NOT_FOUND'
+ | 'CHAT_NOT_FOUND'
+ | 'MESSAGE_NOT_FOUND'
+ | 'PERMISSION_DENIED'
+ | 'VALIDATION_ERROR'
+ | 'SERVER_ERROR'
+ | 'NETWORK_ERROR'