diff --git a/client/chat.go b/client/chat.go index 7ac9c0d..76a1382 100644 --- a/client/chat.go +++ b/client/chat.go @@ -30,10 +30,18 @@ func connectToEchoServer(serverURL string, username string, password string) err } fmt.Printf("[%s] ✓ Username sent: %s\n", getTimestamp(), username) + // Send password immediately after username for authentication + if err := c.WriteMessage(websocket.TextMessage, []byte(password)); err != nil { + return fmt.Errorf("Failed to send password to server: %v", err) + } + + readyCh := make(chan struct{}) + errCh := make(chan error, 1) + fmt.Println("-------------------------------------------") - fmt.Println("Listening for messages from server...") + fmt.Println("Waiting for authentication...") - // Run a goroutine so incoming messages are received in background while user types + // Goroutine to read incoming messages go func() { for { messageType, data, err := c.ReadMessage() @@ -43,17 +51,44 @@ func connectToEchoServer(serverURL string, username string, password string) err return } if err != nil { - fmt.Printf("error: %v\n", err) - break + errCh <- err + return } 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) + if message == "[[AUTH_OK]]" { + // Signal that we can start sending + select { + case <-readyCh: + // already closed + default: + close(readyCh) + } + fmt.Println("Authenticated. Listening for messages...") + continue + } + if strings.HasPrefix(message, "ERROR:") { + fmt.Println(message) + errCh <- fmt.Errorf(message) + return + } + // Print normal chat messages + fmt.Printf("\r%s%s\nEnter Message : ", "\u001b[K", message) } } }() + // Wait until authenticated or error + select { + case <-readyCh: + // proceed + case err := <-errCh: + return err + } + + fmt.Println("-------------------------------------------") + fmt.Println("You can chat now.") + // Read for terminal input reader := bufio.NewReader(os.Stdin) @@ -62,7 +97,12 @@ func connectToEchoServer(serverURL string, username string, password string) err fmt.Print("Enter Message : ") text, _ := reader.ReadString('\n') text = strings.TrimSpace(text) - c.WriteMessage(websocket.TextMessage, []byte(text)) + if text == "" { + continue + } + if err := c.WriteMessage(websocket.TextMessage, []byte(text)); err != nil { + return fmt.Errorf("failed to send message: %v", err) + } } return nil diff --git a/documentation.md b/documentation.md index 422e11c..0efd659 100644 --- a/documentation.md +++ b/documentation.md @@ -88,3 +88,14 @@ 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. + +### {IIT2023139} {#111 Into Fire (Server ver.)} + +- Added password authentication on server join in server/server.js: + - First message is treated as `username`, second as `password`. + - If user does not exist: server creates a new user with `bcryptjs` hashed password and marks them online. + - If user exists: server verifies the provided password against the stored hash; on mismatch, sends an error and disconnects. + - Enforces single-session: rejects connections if the same username is already online. +- Updated server/models/User.js to include required `passwordHash` field; no plaintext passwords stored. +- Updated client/chat.go to send the password right after the username to match the two-step handshake. +- Notes for testing: run the server, connect once with a new username to create and store a hash; reconnect with correct and incorrect passwords to observe accept/reject behavior. diff --git a/server/models/User.js b/server/models/User.js index b19a827..1e7fef4 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -7,6 +7,10 @@ const userSchema = new mongoose.Schema({ unique: true, trim: true, }, + passwordHash: { + type: String, + required: true, + }, connectedAt: { type: Date, default: Date.now, diff --git a/server/package-lock.json b/server/package-lock.json index ef93615..ee6955d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcryptjs": "^2.4.3", "dotenv": "^17.2.3", "mongoose": "^9.1.2", "nodemon": "^3.1.11", @@ -58,6 +59,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", diff --git a/server/package.json b/server/package.json index 09744cc..9bc8427 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,7 @@ "author": "", "license": "ISC", "dependencies": { + "bcryptjs": "^2.4.3", "dotenv": "^17.2.3", "mongoose": "^9.1.2", "nodemon": "^3.1.11", diff --git a/server/server.js b/server/server.js index c9732e5..a68ab15 100644 --- a/server/server.js +++ b/server/server.js @@ -3,6 +3,7 @@ const WebSocket = require("ws"); const mongoose = require("mongoose"); const User = require("./models/User"); const Message = require("./models/Message"); +const bcrypt = require("bcryptjs"); const PORT = process.env.PORT || 8080; const MONGODB_URI = process.env.MONGODB_URI; @@ -66,39 +67,96 @@ async function startServer() { ws.once("message", async (message) => { const username = message.toString().trim(); - const taken = await isUsernameTaken(username); - if (taken) { - ws.send(`ERROR: Username "${username}" is already taken. Please reconnect with a different username.`); - ws.close(); - console.log(`[${getTimestamp()}] Rejected connection: username "${username}" is taken`); - return; - } + // Immediately listen for password to avoid race conditions + const waitPassword = new Promise((resolve) => { + ws.once("message", (pwdMsg) => resolve(pwdMsg.toString().trim())); + }); - await logUserConnection(username); + try { + const password = await waitPassword; - clients.set(ws, username); - console.log(`[${getTimestamp()}] ${username} joined`); + // Reject if user already online + const taken = await isUsernameTaken(username); + if (taken) { + ws.send(`ERROR: Username "${username}" is already taken. Please reconnect with a different username.`); + ws.close(); + console.log(`[${getTimestamp()}] Rejected connection: username "${username}" is taken`); + return; + } - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(`${username} has joined`); + let user = await User.findOne({ username }); + + if (!user) { + // New user: create with hashed password + const saltRounds = 10; + const passwordHash = bcrypt.hashSync(password, saltRounds); + user = await User.create({ + username, + passwordHash, + connectedAt: new Date(), + isOnline: true, + }); + console.log(`[${getTimestamp()}] Created new user "${username}"`); + } else { + // Existing user: verify password + if (!user.passwordHash) { + // Migration path for legacy users without passwordHash + const saltRounds = 10; + const newHash = bcrypt.hashSync(password, saltRounds); + user.passwordHash = newHash; + user.connectedAt = new Date(); + user.isOnline = true; + await user.save(); + console.log(`[${getTimestamp()}] Set password for legacy user "${username}"`); + } + const ok = bcrypt.compareSync(password, user.passwordHash); + if (!ok) { + ws.send("ERROR: Wrong password. Disconnecting."); + ws.close(); + console.log(`[${getTimestamp()}] Wrong password for user "${username}"`); + return; + } + + // Mark as online and update connection time + await logUserConnection(username); } - }); - ws.on("message", async (message) => { - const text = message.toString().trim(); - const username = clients.get(ws); - const time = getTimestamp(); + // Finish login: track client + clients.set(ws, username); - await logMessage(username, text); + // Start handling chat messages BEFORE signaling ready to avoid race + ws.on("message", async (message) => { + const text = message.toString().trim(); + const uname = clients.get(ws); + const time = getTimestamp(); - const finalMessage = `${time}: ${username} said: ${text}`; + await logMessage(uname, text); + + const finalMessage = `${time}: ${uname} said: ${text}`; + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(finalMessage); + } + }); + }); + + // Signal to the client that auth is complete and it can send messages + if (ws.readyState === WebSocket.OPEN) { + ws.send("[[AUTH_OK]]"); + } + + // Announce join after signaling auth OK + console.log(`[${getTimestamp()}] ${username} joined`); wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { - client.send(finalMessage); + client.send(`${username} has joined`); } }); - }); + } catch (err) { + console.error(`[${getTimestamp()}] Auth error:`, err.message); + if (ws.readyState === WebSocket.OPEN) ws.send("ERROR: Authentication failed."); + try { ws.close(); } catch {} + } }); ws.on("close", async () => {