From 34c14a0d808aa0f2c111acd4705d5a951c471ddb Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Tue, 12 Aug 2025 15:50:21 +0330 Subject: [PATCH] Work on telegram setting --- .../Client/SidebarHandler.java | 263 +++++++++++++++++- .../Database/ContactDatabase.java | 27 ++ .../Database/userDatabase.java | 37 +++ .../Server/ClientHandler.java | 87 +++++- 4 files changed, 398 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java b/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java index 302289b..5085e14 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java @@ -531,9 +531,264 @@ public class SidebarHandler { - private void openSettings() { - System.out.println("⚙️ Opening settings..."); + while (true) { + System.out.println("⚙️ Settings\n"); + + String imageUrl = Session.currentUser.optString("image_url", "—"); + String profileName = Session.currentUser.optString("profile_name", "—"); + String userId = Session.currentUser.optString("user_id", "—"); + + renderUserCard(imageUrl, profileName, userId); + System.out.println(); + renderMenu(); + + System.out.print("\nChoose an option (0-4): "); + String pick = scanner.nextLine().trim(); + + switch (pick) { + case "1": openUserProfile(); break; + case "2": showPrivacySettings(); break; + case "3": showTelegramQA(); break; + case "4": showTelegramFeatures(); break; + case "0": return; + default: + System.out.println("❌ Invalid choice. Press Enter to continue..."); + scanner.nextLine(); + } + } + } + + + + private void showPrivacySettings() { + while (true) { + System.out.println("🔒 Privacy\n"); + System.out.println("1) Blocked users"); + System.out.println("2) Change username / password"); + System.out.println("0) Back"); + System.out.print("\nChoose: "); + String pick = scanner.nextLine().trim(); + + switch (pick) { + case "1": viewBlockedUsers(); break; + case "2": changeCredentialsFlow(); break; + case "0": return; + default: + System.out.println("❌ Invalid choice. Press Enter..."); + scanner.nextLine(); + } + } + } + + private void viewBlockedUsers() { + System.out.println("🚫 Blocked Users\n"); + + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "get_blocked_users"); + + org.json.JSONObject res = sendWithResponse(req); + if (res == null || !res.optString("status","error").equals("success")) { + System.out.println("❌ Failed to fetch blocked users. Press Enter..."); + scanner.nextLine(); + return; + } + + org.json.JSONArray arr = res.getJSONObject("data").optJSONArray("blocked_users"); + if (arr == null || arr.isEmpty()) { + System.out.println("📭 No blocked users."); + System.out.println("\nPress Enter..."); + scanner.nextLine(); + return; + } + + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject u = arr.getJSONObject(i); + String profileName = u.optString("profile_name", "—"); + String userId = u.optString("user_id", "—"); + System.out.printf("%d) %s (@%s)\n", i + 1, profileName, userId); + } + System.out.println("\n0) Back"); + System.out.print("\nChoose a user to UNBLOCK (number): "); + String pick = scanner.nextLine().trim(); + + if (pick.equals("0")) return; + int idx; + try { idx = Integer.parseInt(pick) - 1; } catch (Exception e) { idx = -1; } + if (idx < 0 || idx >= arr.length()) { + System.out.println("❌ Invalid index. Press Enter..."); + scanner.nextLine(); + return; + } + + org.json.JSONObject target = arr.getJSONObject(idx); + String targetDisplayId = target.optString("user_id", ""); + java.util.UUID targetInternalId = java.util.UUID.fromString(target.getString("internal_uuid")); + + System.out.printf("Unblock %s (@%s)? (yes/no): ", target.optString("profile_name","—"), targetDisplayId); + if (!scanner.nextLine().trim().equalsIgnoreCase("yes")) return; + + org.json.JSONObject unReq = new org.json.JSONObject(); + unReq.put("action", "toggle_block"); + unReq.put("user_id",Session.getUserUUID()); + unReq.put("target_id", targetInternalId.toString()); + + org.json.JSONObject unRes = sendWithResponse(unReq); + if (unRes != null && unRes.optString("status","error").equals("success")) { + System.out.println("✅ User unblocked."); + } else { + System.out.println("❌ Failed to unblock."); + } + System.out.println("Press Enter..."); + scanner.nextLine(); + } + + + private void changeCredentialsFlow() { + System.out.println("🛡️ Change Username / Password\n"); + + System.out.print("Enter current password: "); + String currentPassword = scanner.nextLine(); + + org.json.JSONObject verReq = new org.json.JSONObject(); + verReq.put("action", "verify_password"); + verReq.put("current_password", currentPassword); + + org.json.JSONObject verRes = sendWithResponse(verReq); + if (verRes == null || !verRes.optString("status","error").equals("success")) { + System.out.println("❌ Current password is incorrect."); + System.out.println("Press Enter..."); + scanner.nextLine(); + return; + } + + String currentUsername = Session.currentUser.optString("username", "—"); + System.out.println("\n✅ Verified."); + System.out.println("Current username: " + currentUsername); + System.out.println("Current password: ******** (hidden)"); + System.out.println("\nWhat do you want to change?"); + System.out.println("1) Username"); + System.out.println("2) Password"); + System.out.println("3) Both"); + System.out.println("0) Back"); + System.out.print("\nChoose: "); + String pick = scanner.nextLine().trim(); + + switch (pick) { + case "1": + changeUsername(currentPassword); + break; + case "2": + changePassword(currentPassword); + break; + case "3": + boolean uOk = changeUsername(currentPassword); + boolean pOk = changePassword(currentPassword); + if (uOk && pOk) System.out.println("✅ Username and password updated."); + System.out.println("Press Enter..."); + scanner.nextLine(); + break; + case "0": + return; + default: + System.out.println("❌ Invalid choice. Press Enter..."); + scanner.nextLine(); + } + } + + + private boolean changeUsername(String currentPassword) { + System.out.print("\nNew username: "); + String newUsername = scanner.nextLine().trim(); + + if (!isValidUsername(newUsername)) { + System.out.println("❌ Invalid username. Use 4–32 chars: letters, digits, underscore."); + return false; + } + + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "update_username"); + req.put("current_password", currentPassword); + req.put("new_username", newUsername); + + org.json.JSONObject res = sendWithResponse(req); + if (res != null && res.optString("status","error").equals("success")) { + Session.currentUser.put("username", newUsername); + System.out.println("✅ Username updated."); + return true; + } else { + String msg = (res == null) ? "No response." : res.optString("message","Update failed."); + System.out.println("❌ " + msg); + return false; + } + } + + private boolean isValidUsername(String s) { + //4-32 char + return s != null && s.matches("^[A-Za-z0-9_]{4,32}$"); + } + + private boolean changePassword(String currentPassword) { + System.out.print("\nNew password: "); + String newPassword = scanner.nextLine(); + System.out.print("Repeat new password: "); + String repeat = scanner.nextLine(); + + if (!newPassword.equals(repeat)) { + System.out.println("❌ Passwords do not match."); + return false; + } + if (!isStrongPassword(newPassword)) { + System.out.println("❌ Weak password. Min 8 chars, include letters and digits."); + return false; + } + + org.json.JSONObject req = new org.json.JSONObject(); + req.put("action", "update_password"); + req.put("current_password", currentPassword); + req.put("new_password", newPassword); + + org.json.JSONObject res = sendWithResponse(req); + if (res != null && res.optString("status","error").equals("success")) { + System.out.println("✅ Password updated."); + return true; + } else { + String msg = (res == null) ? "No response." : res.optString("message","Update failed."); + System.out.println("❌ " + msg); + return false; + } + } + + + private void renderUserCard(String imageUrl, String profileName, String userId) { + int w = 60; + String top = "┌" + "─".repeat(w - 2) + "┐"; + String bot = "└" + "─".repeat(w - 2) + "┘"; + System.out.println(top); + System.out.println(padBoxLine("Profile", w)); + System.out.println("├" + "─".repeat(w - 2) + "┤"); + System.out.println(padBoxLine("Image URL: " + imageUrl, w)); + System.out.println(padBoxLine("Profile Name: "+ profileName, w)); + System.out.println(padBoxLine("User ID: " + userId, w)); + System.out.println(bot); + } + + private String padBoxLine(String text, int width) { + // عرض: width، دو طرف │ │ + final int inner = width - 2; + if (text.length() > inner) { + text = text.substring(0, inner - 1) + "…"; + } + int spaces = inner - text.length(); + return "│" + text + " ".repeat(Math.max(0, spaces)) + "│"; + } + + private void renderMenu() { + System.out.println("1) My Account"); + System.out.println("2) Privacy"); + System.out.println("3) Telegram Q&A"); + System.out.println("4) Telegram Features"); + System.out.println("0) Back"); } private void showTelegramFeatures() { @@ -544,6 +799,10 @@ public class SidebarHandler { System.out.println("❓ Showing Q&A..."); } + private boolean isStrongPassword(String s) { + boolean approved = s.matches("\\b(?=[^\\s]*[A-Z])(?=[^\\s]*[a-z])(?=[^\\s]*\\d)(?=[^\\s]*[!@#$%^&*])[^\\s]{8,}\\b"); + return approved ; + } private List parseMessages(JSONArray msgs) { List list = new ArrayList<>(); diff --git a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java index 3801302..e10c899 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java @@ -1,5 +1,6 @@ package org.to.telegramfinalproject.Database; +import org.json.JSONObject; import org.to.telegramfinalproject.Models.Contact; import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.User; @@ -99,6 +100,32 @@ public class ContactDatabase { } + public static List getBlockedUsers(UUID userId) { + String sql = """ + SELECT u.internal_uuid, u.user_id, u.profile_name + FROM contacts c + JOIN users u ON u.internal_uuid = c.contact_id + WHERE c.user_id = ? AND c.is_blocked = TRUE + ORDER BY u.profile_name NULLS LAST, u.user_id + """; + List out = new ArrayList<>(); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + JSONObject j = new JSONObject(); + j.put("internal_uuid", rs.getObject("internal_uuid").toString()); + j.put("user_id", rs.getString("user_id")); + j.put("profile_name", rs.getString("profile_name")); + out.add(j); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + return out; + } public boolean unblockContact(UUID user_id, UUID contact_id) { String sql = "UPDATE contacts SET is_blocked = FALSE WHERE user_id = ? AND contact_id = ?"; diff --git a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java index 57adbf8..ddf9d27 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java @@ -409,5 +409,42 @@ public class userDatabase { return "Unknown"; } + + public static String getPasswordHash(UUID userId) { + String sql = "SELECT password FROM users WHERE internal_uuid = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) return rs.getString("password"); + } + } catch (SQLException e) { e.printStackTrace(); } + return null; + } + + public static boolean updateUsername(UUID userId, String newUsername) throws SQLException { + String sql = "UPDATE users SET username = ? WHERE internal_uuid = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, newUsername); + ps.setObject(2, userId); + ps.executeUpdate(); + return true; + } catch (SQLException e) { + // 23505 = unique_violation در PostgreSQL + if ("23505".equals(e.getSQLState())) throw e; + throw e; + } + } + + public static boolean updatePasswordHash(UUID userId, String newHash) { + String sql = "UPDATE users SET password = ? WHERE internal_uuid = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, newHash); + ps.setObject(2, userId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { e.printStackTrace(); return false; } + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index b57313e..4bdca06 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -5,6 +5,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Database.*; import org.to.telegramfinalproject.Models.*; +import org.to.telegramfinalproject.Security.PasswordHashing; import org.to.telegramfinalproject.Utils.ChannelPermissionUtil; import org.to.telegramfinalproject.Utils.GroupPermissionUtil; @@ -205,7 +206,6 @@ public class ClientHandler implements Runnable { chat.getUser1_id().equals(chat.getUser2_id()) && chat.getUser1_id().equals(currentUser.getInternal_uuid()); - // در غیر self، otherId = طرف مقابل UUID otherId = isSelf ? currentUser.getInternal_uuid() : (chat.getUser1_id().equals(currentUser.getInternal_uuid()) @@ -2139,6 +2139,8 @@ public class ClientHandler implements Runnable { break; } + + case "get_contact_list": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); @@ -2148,27 +2150,30 @@ public class ClientHandler implements Runnable { List contacts = ContactDatabase.getContacts(currentUser.getInternal_uuid()); List contactEntries = new ArrayList<>(); + JSONArray contactList = new JSONArray(); for (Contact contact : contacts) { - UUID contactId = contact.getContact_id(); - User contactUser = userDatabase.findByInternalUUID(contactId); - if (contactUser == null) continue; + User target = userDatabase.findByInternalUUID(contact.getContact_id()); + if (target == null) continue; - ContactEntry entry = new ContactEntry( - contactId, - contactUser.getUser_id(), - contactUser.getProfile_name(), - contactUser.getImage_url(), - contact.getIs_blocked() - ); + JSONObject c = new JSONObject(); + c.put("user_id", contact.getUser_id().toString()); + c.put("contact_id", contact.getContact_id().toString()); + User Contact = userDatabase.findByInternalUUID(contact.getContact_id()); + c.put("contact_displayId", Contact.getUser_id()); + c.put("is_blocked", contact.getIs_blocked()); - contactEntries.add(entry); + c.put("profile_name", target.getProfile_name()); + c.put("image_url", target.getImage_url()); + c.put("last_seen", Contact.getLast_seen()); + + contactList.put(c); } // Optional: sort alphabetically contactEntries.sort(Comparator.comparing(ContactEntry::getProfileName, String.CASE_INSENSITIVE_ORDER)); JSONObject data = new JSONObject(); - data.put("contact_list", JsonUtil.contactEntryListToJson(contactEntries)); + data.put("contact_list", contactList); response = new ResponseModel("success", "Contact list refreshed", data); break; @@ -2579,9 +2584,63 @@ public class ClientHandler implements Runnable { case "get_or_create_saved_messages": { response = handleGetOrCreateSavedMessages(requestJson); - } break; + } + case "get_blocked_users": { + userId = currentUser.getInternal_uuid(); + var list = ContactDatabase.getBlockedUsers(userId); + org.json.JSONObject data = new org.json.JSONObject(); + data.put("blocked_users", list); + response = new ResponseModel("success", "ok", data); + break; + } + + case "verify_password": { + userId = currentUser.getInternal_uuid(); + String cur = requestJson.getString("current_password"); + User user = userDatabase.findByInternalUUID(userId); + boolean ok = PasswordHashing.verify(cur, user.getPassword()); + response = ok + ? new ResponseModel("success", "verified") + : new ResponseModel("error", "Invalid password."); + break; + } + + case "update_username": { + userId = currentUser.getInternal_uuid(); + String cur = requestJson.getString("current_password"); + String newUsername = requestJson.getString("new_username"); + boolean useBCrypt = true; + + try { + boolean ok = userDatabase.updateUsername(userId, newUsername); + if (ok) response = new ResponseModel("success", "username updated"); + else response = new ResponseModel("error", "Invalid current password."); + } catch (java.sql.SQLException e) { + if ("23505".equals(e.getSQLState())) { + response = new ResponseModel("error", "Username already taken."); + } else { + e.printStackTrace(); + response = new ResponseModel("error", "Failed to update username."); + } + } + break; + } + + case "update_password": { + userId = currentUser.getInternal_uuid(); + String cur = requestJson.getString("current_password"); + String newPass = requestJson.getString("new_password"); + + String newHash = PasswordHashing.hash(newPass); + + boolean ok = userDatabase.updatePasswordHash(userId, newHash); + response = ok + ? new ResponseModel("success", "password updated") + : new ResponseModel("error", "Invalid current password."); + break; + } default: response = new ResponseModel("error", "Unknown action: " + action);