diff --git a/client/chat.go b/client/chat.go index 7ac9c0d..678a71f 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" @@ -24,13 +24,43 @@ func connectToEchoServer(serverURL string, username string, password string) err defer c.Close() fmt.Printf("[%s] ✓ Connected to server\n", getTimestamp()) - err = c.WriteMessage(websocket.TextMessage, []byte(username)) + + // Create authentication JSON + authData := map[string]string{ + "username": username, + "password": password, + } + authJSON, err := json.Marshal(authData) + if err != nil { + return fmt.Errorf("Failed to create authentication data: %v", err) + } + + err = c.WriteMessage(websocket.TextMessage, authJSON) if err != nil { return fmt.Errorf("Failed to write to server: %v", err) } - fmt.Printf("[%s] ✓ Username sent: %s\n", getTimestamp(), username) + fmt.Printf("[%s] ✓ Authentication data sent for user: %s\n", getTimestamp(), username) fmt.Println("-------------------------------------------") + fmt.Println("Waiting for authentication response...") + + // Wait for authentication response + messageType, data, err := c.ReadMessage() + if err != nil { + return fmt.Errorf("[%s] ✗ Connection error: %v", getTimestamp(), err) + } + + if messageType == websocket.TextMessage { + message := string(data) + if strings.HasPrefix(message, "ERROR:") { + fmt.Printf("\r%s%s\n", "\033[K", message) + return fmt.Errorf("Authentication failed: %s", message) + } else { + fmt.Printf("\r%s%s\n", "\033[K", message) + fmt.Printf("[%s] ✓ Authentication successful!\n", getTimestamp()) + } + } + fmt.Println("Listening for messages from server...") // Run a goroutine so incoming messages are received in background while user types @@ -38,12 +68,13 @@ func connectToEchoServer(serverURL string, username string, password string) err for { messageType, data, err := c.ReadMessage() if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("error: %v", err) - log.Println(err) + fmt.Printf("\r%sConnection closed by server.\n", "\033[K") + os.Exit(1) return } if err != nil { - fmt.Printf("error: %v\n", err) + fmt.Printf("\r%sConnection error: %v\n", "\033[K", err) + os.Exit(1) break } if messageType == websocket.TextMessage { @@ -70,12 +101,18 @@ func connectToEchoServer(serverURL string, username string, password string) err 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] + for { + 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 + } + + return username } - return username } func getTimestamp() string { @@ -97,7 +134,16 @@ func getServerAddress() string { func getPassword() string { reader := bufio.NewReader(os.Stdin) - fmt.Print("Enter your password: ") - password, _ := reader.ReadString('\n') - return strings.TrimSpace(password) + 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 + } } \ No newline at end of file diff --git a/documentation.md b/documentation.md index 422e11c..3958436 100644 --- a/documentation.md +++ b/documentation.md @@ -71,7 +71,6 @@ getTimestamp() // returns the current timestamp as a string in the format of "02 - Updated client/chat.go: - Added getServerAddress() function to capture server URL from user input via stdin. - Implemented default fallback to "localhost:8080" if input is empty. - - Updated client/main.go: - Removed dependency on command-line flags for server address. - Reordered logic to prompt for Server Address first, then Username. @@ -80,6 +79,7 @@ getTimestamp() // returns the current timestamp as a string in the format of "02 ### {Krishna200608} {#108 Into Fire (Client ver.)} - Updated client/chat.go: + - Added getPassword() function to capture user password from stdin. - Updated connectToEchoServer() signature to accept password parameter. - Modified message listener loop to remove "Server -> Client" prefix and redundant client-side timestamp, aligning output with the required spec. @@ -88,3 +88,30 @@ 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. + +### {dwivediprashant} {#111 Into Fire (Server ver.)} + +- Updated server/server.js: + + - Added bcrypt dependency for password hashing. + - Implemented authentication functions: hashPassword(), verifyPassword(), findUser(), createUser(). + - Modified connection logic to handle JSON authentication data. + - Added proper error handling for wrong passwords and duplicate online users. + +- Updated server/models/User.js: + + - Added password field to user schema for storing hashed passwords. + +- Updated server/package.json: + + - Added bcrypt dependency for secure password hashing. + +- Updated client/chat.go: + + - Added JSON encoding for authentication data. + - Modified connectToEchoServer() to send {"username": "...", "password": "..."}. + - Added input validation to prevent empty usernames and passwords. + - Implemented proper authentication error handling with clean exit on failure. + - Added success message display for correct authentication. + +- Tested locally: Verified new user registration, existing user login, wrong password rejection, and proper error handling. diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..d681f7c --- /dev/null +++ b/server/.env.example @@ -0,0 +1 @@ +MONGODB_URI=your_mongodb_uri \ No newline at end of file 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..3d2f36d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "mongoose": "^9.1.2", "nodemon": "^3.1.11", @@ -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", diff --git a/server/package.json b/server/package.json index 09744cc..b2107e0 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,7 @@ "author": "", "license": "ISC", "dependencies": { + "bcrypt": "^6.0.0", "dotenv": "^17.2.3", "mongoose": "^9.1.2", "nodemon": "^3.1.11", diff --git a/server/server.js b/server/server.js index c9732e5..81ab3f4 100644 --- a/server/server.js +++ b/server/server.js @@ -1,6 +1,7 @@ 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"); @@ -18,7 +19,10 @@ async function connectDB() { await mongoose.connect(MONGODB_URI); console.log(`[${getTimestamp()}] Connected to MongoDB`); } catch (error) { - console.error(`[${getTimestamp()}] MongoDB connection error:`, error.message); + console.error( + `[${getTimestamp()}] MongoDB connection error:`, + error.message + ); process.exit(1); } } @@ -27,7 +31,7 @@ async function logUserConnection(username) { try { await User.findOneAndUpdate( { username }, - { username, connectedAt: new Date(), isOnline: true }, + { connectedAt: new Date(), isOnline: true }, { upsert: true, new: true } ); console.log(`[${getTimestamp()}] User "${username}" logged to database`); @@ -36,6 +40,28 @@ async function logUserConnection(username) { } } +async function hashPassword(password) { + const saltRounds = 10; + return await bcrypt.hash(password, saltRounds); +} + +async function verifyPassword(password, hashedPassword) { + return await bcrypt.compare(password, hashedPassword); +} + +async function findUser(username) { + return await User.findOne({ username }); +} + +async function createUser(username, hashedPassword) { + return await User.create({ + username, + password: hashedPassword, + connectedAt: new Date(), + isOnline: true, + }); +} + async function isUsernameTaken(username) { const user = await User.findOne({ username, isOnline: true }); return !!user; @@ -45,7 +71,10 @@ async function markUserOffline(username) { try { await User.findOneAndUpdate({ username }, { isOnline: false }); } catch (error) { - console.error(`[${getTimestamp()}] Error marking user offline:`, error.message); + console.error( + `[${getTimestamp()}] Error marking user offline:`, + error.message + ); } } @@ -63,49 +92,99 @@ async function startServer() { const wss = new WebSocket.Server({ port: PORT }); wss.on("connection", (ws) => { + let isAuthenticated = false; + let currentUsername = null; + ws.once("message", async (message) => { - const username = message.toString().trim(); + try { + const data = JSON.parse(message.toString().trim()); + const { username, password } = data; + + if (!username || !password) { + ws.send("ERROR: 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.`); - ws.close(); - console.log(`[${getTimestamp()}] Rejected connection: username "${username}" is taken`); - return; - } + const existingUser = await findUser(username); - await logUserConnection(username); + if (existingUser) { + if (existingUser.isOnline) { + ws.send(`ERROR: User "${username}" is already online`); + ws.close(); + console.log( + `[${getTimestamp()}] Rejected connection: user "${username}" is already online` + ); + return; + } - clients.set(ws, username); - console.log(`[${getTimestamp()}] ${username} joined`); + const passwordMatch = await verifyPassword( + password, + existingUser.password + ); + if (!passwordMatch) { + ws.send("ERROR: Wrong password"); + ws.close(); + console.log( + `[${getTimestamp()}] Rejected connection: wrong password for "${username}"` + ); + return; + } - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(`${username} has joined`); + await User.findOneAndUpdate( + { username }, + { connectedAt: new Date(), isOnline: true } + ); + } else { + const hashedPassword = await hashPassword(password); + await createUser(username, hashedPassword); + console.log( + `[${getTimestamp()}] New user "${username}" created and logged in` + ); } - }); - ws.on("message", async (message) => { - const text = message.toString().trim(); - const username = clients.get(ws); - const time = getTimestamp(); + isAuthenticated = true; + currentUsername = username; + clients.set(ws, username); + console.log(`[${getTimestamp()}] ${username} joined`); - await logMessage(username, text); - - const finalMessage = `${time}: ${username} said: ${text}`; wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { - client.send(finalMessage); + client.send(`${username} has joined`); } }); - }); + + ws.on("message", async (message) => { + if (!isAuthenticated) return; + + const text = message.toString().trim(); + const username = clients.get(ws); + const time = getTimestamp(); + + await logMessage(username, text); + + const finalMessage = `${time}: ${username} said: ${text}`; + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(finalMessage); + } + }); + }); + } catch (error) { + console.error( + `[${getTimestamp()}] Error parsing authentication data:`, + error.message + ); + ws.send("ERROR: Invalid authentication data format"); + ws.close(); + } }); ws.on("close", async () => { const username = clients.get(ws); if (username) { console.log(`[${getTimestamp()}] ${username} disconnected`); - + await markUserOffline(username); wss.clients.forEach((client) => {