From 6f3fa4b022c58211df870000be7ea43a20d5e5c0 Mon Sep 17 00:00:00 2001 From: Ishan Raj Singh Date: Thu, 8 Jan 2026 21:12:01 +0530 Subject: [PATCH 1/2] Implement auth --- client/chat.go | 202 ++++++++++++++++++------- server/go.mod | 3 + server/models/User.js | 4 + server/package-lock.json | 43 +++++- server/package.json | 6 +- server/server.js | 316 +++++++++++++++++++++++++++++---------- 6 files changed, 432 insertions(+), 142 deletions(-) create mode 100644 server/go.mod diff --git a/client/chat.go b/client/chat.go index 7ac9c0d..dedd30c 100644 --- a/client/chat.go +++ b/client/chat.go @@ -2,8 +2,8 @@ package main import ( "bufio" + "encoding/json" "fmt" - "log" "net/url" "os" "strings" @@ -12,92 +12,186 @@ import ( "github.com/gorilla/websocket" ) +// AuthCredentials represents user login information +type AuthCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + func connectToEchoServer(serverURL string, username string, password string) error { - u := url.URL{Scheme: "ws", Host: serverURL, Path: "/"} - fmt.Printf("Connecting to %s\n", u.String()) - c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + // Build WebSocket URL + wsURL := url.URL{Scheme: "ws", Host: serverURL, Path: "/"} + fmt.Printf("Connecting to %s\n", wsURL.String()) + + // Establish connection + conn, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil) if err != nil { - return fmt.Errorf("[%s] ✗ Failed to connect to server: %v", getTimestamp(), err) + return fmt.Errorf("[%s] ✗ Connection failed: %v", getTimestamp(), err) + } + defer conn.Close() + + fmt.Printf("[%s] ✓ Connected successfully\n", getTimestamp()) + // Prepare authentication payload - FIXED VERSION + authData := map[string]string{ + "username": username, + "password": password, } - defer c.Close() + authJSON, err := json.Marshal(authData) + if err != nil { + return fmt.Errorf("Failed to serialize authentication data: %v", err) + } - fmt.Printf("[%s] ✓ Connected to server\n", getTimestamp()) - err = c.WriteMessage(websocket.TextMessage, []byte(username)) + // Debug: Show what we're sending + fmt.Printf("Sending auth data: %s\n", string(authJSON)) + + // Send authentication credentials + err = conn.WriteMessage(websocket.TextMessage, authJSON) if err != nil { - return fmt.Errorf("Failed to write to server: %v", err) + return fmt.Errorf("Failed to send credentials: %v", err) + } + fmt.Printf("[%s] ✓ Credentials sent for user: %s\n", getTimestamp(), username) + fmt.Println("-------------------------------------------") + fmt.Println("Awaiting server response...") + + // Wait for authentication response + _, responseData, err := conn.ReadMessage() + if err != nil { + return fmt.Errorf("[%s] ✗ Server response error: %v", getTimestamp(), err) + } + + serverResponse := string(responseData) + + // Check if authentication failed + if strings.HasPrefix(serverResponse, "ERROR:") { + fmt.Printf("\r%s%s\n", "\033[K", serverResponse) + return fmt.Errorf("Authentication rejected by server") } - fmt.Printf("[%s] ✓ Username sent: %s\n", getTimestamp(), username) + + // Authentication successful + fmt.Printf("\r%s%s\n", "\033[K", serverResponse) + fmt.Printf("[%s] ✓ Authentication successful!\n", getTimestamp()) fmt.Println("-------------------------------------------") - fmt.Println("Listening for messages from server...") - - // Run a goroutine so incoming messages are received in background while user types - go func() { - for { - messageType, data, err := c.ReadMessage() - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("error: %v", err) - log.Println(err) - return - } + fmt.Println("You can now send messages. Type /quit to exit.") + fmt.Println() + + // Start background goroutine for receiving messages + go receiveMessagesInBackground(conn) + + // Main loop for sending messages + sendMessagesFromTerminal(conn) + + return nil +} + + +// receiveMessagesInBackground listens for incoming messages +func receiveMessagesInBackground(conn *websocket.Conn) { + for { + msgType, data, err := conn.ReadMessage() + + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + fmt.Printf("\r%sServer closed the connection.\n", "\033[K") + os.Exit(1) + return + } + + if err != nil { + fmt.Printf("\r%sConnection error: %v\n", "\033[K", err) + os.Exit(1) + return + } + + if msgType == websocket.TextMessage { + message := string(data) + // Clear current line and print message + fmt.Printf("\r%s%s\nEnter Message: ", "\033[K", message) + } + } +} + +// sendMessagesFromTerminal handles user input +func sendMessagesFromTerminal(conn *websocket.Conn) { + scanner := bufio.NewScanner(os.Stdin) + + for { + fmt.Print("Enter Message: ") + if !scanner.Scan() { + break + } + + userInput := strings.TrimSpace(scanner.Text()) + + // Check for quit command + if userInput == "/quit" { + fmt.Println("Disconnecting...") + break + } + + // Send non-empty messages + if userInput != "" { + err := conn.WriteMessage(websocket.TextMessage, []byte(userInput)) if err != nil { - fmt.Printf("error: %v\n", err) + fmt.Printf("Failed to send message: %v\n", err) break } - if messageType == websocket.TextMessage { - message := string(data) - // We print the message directly to avoid double timestamps/prefixes - fmt.Printf("\r%s%s\nEnter Message : ", "\033[K", message) - } } - }() + } +} - // Read for terminal input +// getUsername prompts for and validates username +func getUsername() string { reader := bufio.NewReader(os.Stdin) - // Infinite loop so that user can keep sending messages for { - fmt.Print("Enter Message : ") - text, _ := reader.ReadString('\n') - text = strings.TrimSpace(text) - c.WriteMessage(websocket.TextMessage, []byte(text)) - } - return nil + fmt.Print("Enter your username: ") + username, _ := reader.ReadString('\n') + username = strings.TrimSpace(username) -} + if username == "" { + fmt.Println("⚠ Username cannot be empty. Please try again.") + continue + } -func getUsername() string { - reader := bufio.NewReader(os.Stdin) - fmt.Print("Enter your username: ") - username, _ := reader.ReadString('\n') - if len(username) > 0 && username[len(username)-1] == '\n' { - username = username[:len(username)-1] + return username } - return username } -func getTimestamp() string { - return time.Now().Format("02/01/2006 03:04:05 PM") +// getPassword prompts for and validates password +func getPassword() string { + reader := bufio.NewReader(os.Stdin) + + for { + fmt.Print("Enter your password: ") + password, _ := reader.ReadString('\n') + password = strings.TrimSpace(password) + + if password == "" { + fmt.Println("⚠ Password cannot be empty. Please try again.") + continue + } + + return password + } } +// getServerAddress prompts for server address with default func getServerAddress() string { reader := bufio.NewReader(os.Stdin) fmt.Print("Enter Server Address (default: localhost:8080): ") address, _ := reader.ReadString('\n') address = strings.TrimSpace(address) - - // If the user just hits enter, default to localhost:8080 + if address == "" { return "localhost:8080" } + return address } -func getPassword() string { - reader := bufio.NewReader(os.Stdin) - fmt.Print("Enter your password: ") - password, _ := reader.ReadString('\n') - return strings.TrimSpace(password) -} \ No newline at end of file +// getTimestamp returns formatted current time +func getTimestamp() string { + return time.Now().Format("02/01/2006 03:04:05 PM") +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..522b839 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,3 @@ +module github.com/opencodeiiita/Echo + +go 1.25.5 diff --git a/server/models/User.js b/server/models/User.js index b19a827..10d2db8 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -7,6 +7,10 @@ const userSchema = new mongoose.Schema({ unique: true, trim: true, }, + password: { + type: String, + required: true, + }, connectedAt: { type: Date, default: Date.now, diff --git a/server/package-lock.json b/server/package-lock.json index ef93615..7bb5222 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,10 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "mongoose": "^9.1.2", "nodemon": "^3.1.11", - "ws": "^8.18.3" + "ws": "^8.19.0" } }, "node_modules/@mongodb-js/saslprep": { @@ -58,6 +59,20 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -386,6 +401,26 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemon": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", @@ -575,9 +610,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/server/package.json b/server/package.json index 09744cc..a4adc88 100644 --- a/server/package.json +++ b/server/package.json @@ -11,9 +11,11 @@ "author": "", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "mongoose": "^9.1.2", "nodemon": "^3.1.11", - "ws": "^8.18.3" - } + "ws": "^8.19.0" + }, + "type": "commonjs" } diff --git a/server/server.js b/server/server.js index c9732e5..1a7cba7 100644 --- a/server/server.js +++ b/server/server.js @@ -1,124 +1,276 @@ require("dotenv").config(); const WebSocket = require("ws"); const mongoose = require("mongoose"); +const bcrypt = require("bcrypt"); const User = require("./models/User"); const Message = require("./models/Message"); -const PORT = process.env.PORT || 8080; -const MONGODB_URI = process.env.MONGODB_URI; +// Configuration constants +const SERVER_CONFIG = { + port: process.env.PORT || 8080, + mongoUri: process.env.MONGODB_URI, + bcryptSaltRounds: 10, +}; -const clients = new Map(); +// Active WebSocket connections registry +const activeConnections = new Map(); -function getTimestamp() { - return new Date().toLocaleString(); -} +// Utility: Generate formatted timestamp +const formatTimestamp = () => { + const now = new Date(); + return now.toLocaleString("en-IN", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: true, + }); +}; -async function connectDB() { +// Database connection handler +// Database connection handler +async function initializeMongoDB() { try { - await mongoose.connect(MONGODB_URI); - console.log(`[${getTimestamp()}] Connected to MongoDB`); - } catch (error) { - console.error(`[${getTimestamp()}] MongoDB connection error:`, error.message); + console.log(`[${formatTimestamp()}] Connecting to MongoDB Atlas...`); + await mongoose.connect(SERVER_CONFIG.mongoUri); // ✅ No options needed in Mongoose 6+ + console.log(`[${formatTimestamp()}] ✓ MongoDB connection established`); + } catch (err) { + console.error(`[${formatTimestamp()}] ✗ MongoDB connection failed:`, err.message); + console.error("Full error:", err); process.exit(1); } } -async function logUserConnection(username) { - try { - await User.findOneAndUpdate( + +// Authentication module +const AuthService = { + // Generate secure password hash + async generateHash(plainPassword) { + return await bcrypt.hash(plainPassword, SERVER_CONFIG.bcryptSaltRounds); + }, + + // Validate password against stored hash + async validatePassword(plainPassword, storedHash) { + return await bcrypt.compare(plainPassword, storedHash); + }, + + // Retrieve user from database + async fetchUserByUsername(username) { + return await User.findOne({ username }).lean(); + }, + + // Register new user in database + async registerNewUser(username, hashedPassword) { + const newUser = await User.create({ + username, + password: hashedPassword, + connectedAt: new Date(), + isOnline: true, + }); + console.log(`[${formatTimestamp()}] ✓ New user registered: "${username}"`); + console.log(`[${formatTimestamp()}] Password hash: ${hashedPassword.substring(0, 25)}...`); + return newUser; + }, + + // Update existing user's online status + async updateUserOnlineStatus(username, isOnline) { + return await User.findOneAndUpdate( { username }, - { username, connectedAt: new Date(), isOnline: true }, - { upsert: true, new: true } + { + isOnline, + connectedAt: isOnline ? new Date() : undefined + }, + { new: true } ); - console.log(`[${getTimestamp()}] User "${username}" logged to database`); - } catch (error) { - console.error(`[${getTimestamp()}] Error logging user:`, error.message); - } -} + }, -async function isUsernameTaken(username) { - const user = await User.findOne({ username, isOnline: true }); - return !!user; -} + // Main authentication logic + async authenticateUser(username, password) { + const user = await this.fetchUserByUsername(username); -async function markUserOffline(username) { - try { - await User.findOneAndUpdate({ username }, { isOnline: false }); - } catch (error) { - console.error(`[${getTimestamp()}] Error marking user offline:`, error.message); - } + if (!user) { + // New user scenario - create account + const hashedPassword = await this.generateHash(password); + await this.registerNewUser(username, hashedPassword); + return { + success: true, + isNewUser: true, + message: `Welcome ${username}! Your account has been created.` + }; + } + + // Existing user scenario + if (user.isOnline) { + return { + success: false, + message: `ERROR: User "${username}" is already connected from another session` + }; + } + + // Verify password + const isPasswordCorrect = await this.validatePassword(password, user.password); + + if (!isPasswordCorrect) { + console.log(`[${formatTimestamp()}] ✗ Failed login attempt for "${username}" - Wrong password`); + return { + success: false, + message: "ERROR: Wrong password" + }; + } + + // Update online status + await this.updateUserOnlineStatus(username, true); + console.log(`[${formatTimestamp()}] ✓ User "${username}" authenticated successfully`); + + return { + success: true, + isNewUser: false, + message: `Welcome back, ${username}!` + }; + }, +}; + +// Message broadcasting utility +function broadcastToAllClients(wsServer, message, excludeClient = null) { + wsServer.clients.forEach((client) => { + if (client !== excludeClient && client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); } -async function logMessage(sender, content) { +// Store message in database +async function persistMessage(senderUsername, messageContent) { try { - await Message.create({ sender, content, timestamp: new Date() }); - } catch (error) { - console.error(`[${getTimestamp()}] Error logging message:`, error.message); + await Message.create({ + sender: senderUsername, + content: messageContent, + timestamp: new Date(), + }); + } catch (err) { + console.error(`[${formatTimestamp()}] ✗ Failed to save message:`, err.message); } } -async function startServer() { - await connectDB(); +// WebSocket connection handler +function handleWebSocketConnection(ws, wsServer) { + let authenticatedUsername = null; + let hasCompletedAuth = false; - const wss = new WebSocket.Server({ port: PORT }); + // Listen for initial authentication message + ws.once("message", async (rawData) => { + try { + // Debug: Show what we received + console.log(`[${formatTimestamp()}] 📥 Received raw data:`, rawData); + console.log(`[${formatTimestamp()}] 📥 Data type:`, typeof rawData); + console.log(`[${formatTimestamp()}] 📥 Data as string:`, rawData.toString()); + + const dataString = rawData.toString().trim(); + console.log(`[${formatTimestamp()}] 📥 Trimmed string:`, dataString); + + const authPayload = JSON.parse(dataString); + console.log(`[${formatTimestamp()}] 📥 Parsed JSON:`, authPayload); + + const { username, password } = authPayload; - wss.on("connection", (ws) => { - ws.once("message", async (message) => { - const username = message.toString().trim(); + // Validate credentials presence + if (!username?.trim() || !password?.trim()) { + ws.send("ERROR: Both username and password are required"); + ws.close(); + return; + } - const taken = await isUsernameTaken(username); - if (taken) { - ws.send(`ERROR: Username "${username}" is already taken. Please reconnect with a different username.`); + // Attempt authentication + const authResult = await AuthService.authenticateUser( + username.trim(), + password.trim() + ); + + if (!authResult.success) { + ws.send(authResult.message); ws.close(); - console.log(`[${getTimestamp()}] Rejected connection: username "${username}" is taken`); return; } - await logUserConnection(username); + // Authentication successful + hasCompletedAuth = true; + authenticatedUsername = username.trim(); + activeConnections.set(ws, authenticatedUsername); - clients.set(ws, username); - console.log(`[${getTimestamp()}] ${username} joined`); + // Send success message to client + ws.send(authResult.message); - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(`${username} has joined`); - } - }); + // Notify all clients about new user + console.log(`[${formatTimestamp()}] ✓ "${authenticatedUsername}" joined the chat`); + broadcastToAllClients(wsServer, `${authenticatedUsername} has joined the chat`, ws); - ws.on("message", async (message) => { - const text = message.toString().trim(); - const username = clients.get(ws); - const time = getTimestamp(); + // Set up message handler for authenticated user + ws.on("message", async (msgData) => { + if (!hasCompletedAuth) return; - await logMessage(username, text); + const messageText = msgData.toString().trim(); + if (!messageText) return; - const finalMessage = `${time}: ${username} said: ${text}`; - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(finalMessage); - } - }); + const username = activeConnections.get(ws); + const timestamp = formatTimestamp(); + + // Save to database + await persistMessage(username, messageText); + + // Broadcast to all clients + const formattedMessage = `[${timestamp}] ${username}: ${messageText}`; + broadcastToAllClients(wsServer, formattedMessage); }); - }); - ws.on("close", async () => { - const username = clients.get(ws); - if (username) { - console.log(`[${getTimestamp()}] ${username} disconnected`); - - await markUserOffline(username); - - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(`${username} has left`); - } - }); - clients.delete(ws); - } - }); + } catch (parseError) { + console.error(`[${formatTimestamp()}] ✗ Parse error:`, parseError.message); + console.error(`[${formatTimestamp()}] ✗ Received data:`, rawData.toString()); + console.error(`[${formatTimestamp()}] ✗ Full error:`, parseError); + ws.send("ERROR: Invalid authentication format. Send JSON with username and password."); + ws.close(); + } + }); + + // Handle client disconnect + ws.on("close", async () => { + if (authenticatedUsername) { + console.log(`[${formatTimestamp()}] ✗ "${authenticatedUsername}" disconnected`); + + // Update database + await AuthService.updateUserOnlineStatus(authenticatedUsername, false); + + // Notify other clients + broadcastToAllClients(wsServer, `${authenticatedUsername} has left the chat`); + + // Clean up + activeConnections.delete(ws); + } + }); + + // Handle errors + ws.on("error", (err) => { + console.error(`[${formatTimestamp()}] ✗ WebSocket error:`, err.message); + }); +} + + +// Initialize and start server +async function startEchoServer() { + await initializeMongoDB(); + + const wsServer = new WebSocket.Server({ port: SERVER_CONFIG.port }); + + wsServer.on("connection", (ws) => { + handleWebSocketConnection(ws, wsServer); }); - console.log(`[${getTimestamp()}] WebSocket server running on port ${PORT}`); + console.log(`[${formatTimestamp()}] ✓ Echo WebSocket server listening on port ${SERVER_CONFIG.port}`); } -startServer(); +// Launch server +startEchoServer().catch((err) => { + console.error(`[${formatTimestamp()}] ✗ Server startup failed:`, err); + process.exit(1); +}); From 0afb786a81b6c79a64cd799f474e9482fd7457a3 Mon Sep 17 00:00:00 2001 From: Ishan Raj Singh Date: Thu, 8 Jan 2026 21:20:11 +0530 Subject: [PATCH 2/2] Update documentation --- client/chat.go | 2 +- documentation.md | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/client/chat.go b/client/chat.go index dedd30c..aa630fe 100644 --- a/client/chat.go +++ b/client/chat.go @@ -32,7 +32,7 @@ func connectToEchoServer(serverURL string, username string, password string) err fmt.Printf("[%s] ✓ Connected successfully\n", getTimestamp()) - // Prepare authentication payload - FIXED VERSION + // Prepare authentication payload authData := map[string]string{ "username": username, "password": password, diff --git a/documentation.md b/documentation.md index 422e11c..b3668ef 100644 --- a/documentation.md +++ b/documentation.md @@ -88,3 +88,43 @@ getTimestamp() // returns the current timestamp as a string in the format of "02 - Added call to getPassword() after username prompt. - Passed password to connectToEchoServer(). - Tested locally: Verified password prompt appears and chat messages display cleanly. + +### {ishanrajsingh} {#111 Into Fire (Server ver.)} + +**Server Implementation (server/server.js):** +- Implemented complete authentication system with bcrypt password hashing using 10 salt rounds +- Added AuthService module containing generateHash(), validatePassword(), fetchUserByUsername(), registerNewUser(), and updateUserOnlineStatus() methods +- Created authenticateUser() function that handles both new user registration and existing user login verification +- Implemented handleWebSocketConnection() to manage WebSocket lifecycle with authentication as first message requirement +- Added JSON parsing for authentication payload containing username and password fields +- Implemented wrong password rejection logic that sends "ERROR: Wrong password" message and immediately closes connection +- Added broadcastToAllClients() utility function for message distribution excluding sender +- Integrated MongoDB Atlas connection using Mongoose with proper error handling and connection status logging +- Added formatTimestamp() utility for consistent Indian Standard Time formatted logs +- Implemented persistMessage() function to store all chat messages in MongoDB messages collection +- Added comprehensive error logging for authentication failures, connection issues, and database errors +- Created session management using Map data structure to track active connections with username associations + +**Database Models:** +- Created models/User.js schema with username (unique indexed), password (bcrypt hash storage), isOnline boolean flag, and connectedAt timestamp +- Added schema validation for username minimum 3 characters and maximum 30 characters +- Implemented automatic timestamps with createdAt and updatedAt fields +- Created models/Message.js schema with sender username, content text, and timestamp fields + +**Client Implementation (client/chat.go):** +- Refactored connectToEchoServer() to accept username and password parameters +- Implemented AuthCredentials struct with JSON tags for proper marshaling +- Added JSON payload creation using json.Marshal() for authentication data transmission +- Modified authentication flow to send credentials as first WebSocket message +- Added server response parsing to handle success messages and ERROR-prefixed rejection messages +- Implemented receiveMessagesInBackground() goroutine for concurrent message reception +- Added sendMessagesFromTerminal() function with /quit command support for graceful disconnection +- Enhanced error handling with specific messages for authentication failures and connection issues +- Added input validation loops in getUsername() and getPassword() to prevent empty credentials +- Implemented proper cleanup on authentication failure with connection closure + +**Client Entry Point (client/main.go):** +- Updated main() flow to collect username and password before connection attempt +- Added getPassword() function call after getUsername() in credential gathering sequence +- Modified connectToEchoServer() invocation to pass password parameter +- Maintained getServerAddress() functionality with localhost:8080 as default \ No newline at end of file