diff --git a/backend/dependencies.py b/backend/dependencies.py
index 5bdfcab..c19adee 100644
--- a/backend/dependencies.py
+++ b/backend/dependencies.py
@@ -1,8 +1,9 @@
+from datetime import datetime
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
-from utils import *
-from models import *
+from utils import verify_token
+from models import User, DeviceSession
from db import SessionLocal
security = HTTPBearer()
diff --git a/backend/push_service.py b/backend/push_service.py
index ee48ded..a06c36d 100644
--- a/backend/push_service.py
+++ b/backend/push_service.py
@@ -107,7 +107,7 @@ class PushNotificationService:
payload = {
"title": title,
"body": body,
- "icon": icon or "/logo.png",
+ "icon": icon or "about:blank",
"tag": f"message_{user_id}",
"data": data
}
diff --git a/backend/routes/account.py b/backend/routes/account.py
index eb90569..445930e 100644
--- a/backend/routes/account.py
+++ b/backend/routes/account.py
@@ -313,6 +313,8 @@ def set_public_key(payload: dict, current_user: User = Depends(get_current_user)
pk = payload.get("publicKey")
if not pk:
raise HTTPException(status_code=400, detail="publicKey required")
+ if not isinstance(pk, str) or len(pk) > 10000 or len(pk) < 10:
+ raise HTTPException(status_code=400, detail="Invalid publicKey format")
row = db.query(CryptoPublicKey).filter(CryptoPublicKey.user_id == current_user.id).first()
if row:
row.public_key_b64 = pk
@@ -334,6 +336,8 @@ def set_backup(payload: dict, current_user: User = Depends(get_current_user), db
blob = payload.get("blob")
if not blob:
raise HTTPException(status_code=400, detail="blob required")
+ if not isinstance(blob, str) or len(blob) > 1000000: # 1MB limit
+ raise HTTPException(status_code=400, detail="Invalid blob format or size exceeds 1MB")
row = db.query(CryptoBackup).filter(CryptoBackup.user_id == current_user.id).first()
if row:
row.blob_json = blob
diff --git a/backend/routes/devices.py b/backend/routes/devices.py
index 550c5f9..7cf41b9 100644
--- a/backend/routes/devices.py
+++ b/backend/routes/devices.py
@@ -60,6 +60,9 @@ def revoke_device(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
+ if not session_id or len(session_id) > 64 or len(session_id) < 1:
+ raise HTTPException(status_code=400, detail="Invalid session ID")
+
s = (
db.query(DeviceSession)
.filter(DeviceSession.user_id == current_user.id, DeviceSession.session_id == session_id)
diff --git a/backend/routes/messaging.py b/backend/routes/messaging.py
index 7f321f2..bd75081 100644
--- a/backend/routes/messaging.py
+++ b/backend/routes/messaging.py
@@ -198,7 +198,7 @@ def convert_message(msg: Message) -> dict:
}
-def convert_dm_envelope(envelope: DMEnvelope) -> dict:
+def convert_dm_envelope(db: Session, envelope: DMEnvelope) -> dict:
# Group reactions by emoji
reactions_dict = {}
if envelope.reactions:
@@ -217,9 +217,6 @@ def convert_dm_envelope(envelope: DMEnvelope) -> dict:
})
# Get sender info for verified status
- from models import User
- from dependencies import get_db
- db = next(get_db())
sender = db.query(User).filter(User.id == envelope.sender_id).first()
# Handle deleted or suspended users
@@ -454,9 +451,25 @@ async def dm_send(
if key not in payload:
raise HTTPException(status_code=400, detail=f"Missing {key}")
+ try:
+ recipient_id = int(payload["recipientId"])
+ except (ValueError, TypeError):
+ raise HTTPException(status_code=400, detail="Invalid recipientId")
+
+ if recipient_id <= 0:
+ raise HTTPException(status_code=400, detail="Invalid recipientId")
+
+ if recipient_id == current_user.id:
+ raise HTTPException(status_code=400, detail="Cannot send DM to yourself")
+
+ # Verify recipient exists
+ recipient = db.query(User).filter(User.id == recipient_id).first()
+ if not recipient or recipient.deleted or recipient.suspended:
+ raise HTTPException(status_code=404, detail="Recipient not found")
+
env = DMEnvelope(
sender_id=current_user.id,
- recipient_id=int(payload["recipientId"]),
+ recipient_id=recipient_id,
iv_b64=payload["iv"],
ciphertext_b64=payload["ciphertext"],
salt_b64=payload["salt"],
@@ -590,6 +603,17 @@ async def dm_fetch(request: Request, since: int | None = None, current_user: Use
@router.get("/dm/history/{other_user_id}")
@rate_limit_per_ip("60/minute") # Per-IP limit to prevent abuse
async def dm_history(request: Request, other_user_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
+ if other_user_id <= 0:
+ raise HTTPException(status_code=400, detail="Invalid user ID")
+
+ if other_user_id == current_user.id:
+ raise HTTPException(status_code=400, detail="Cannot get history with yourself")
+
+ # Verify other user exists
+ other_user = db.query(User).filter(User.id == other_user_id).first()
+ if not other_user or other_user.deleted or other_user.suspended:
+ raise HTTPException(status_code=404, detail="User not found")
+
return convert_envelopes(
db.query(DMEnvelope)
.filter(
@@ -631,7 +655,7 @@ async def get_dm_conversations(request: Request, current_user: User = Depends(ge
result.append({
"user": convert_user(other_user),
- "lastMessage": convert_dm_envelope(latest_message),
+ "lastMessage": convert_dm_envelope(db, latest_message),
"unreadCount": unread_count
})
@@ -833,7 +857,7 @@ async def add_dm_reaction(
# Refresh envelope to get updated reactions
db.refresh(envelope)
- envelope_data = convert_dm_envelope(envelope)
+ envelope_data = convert_dm_envelope(db, envelope)
# Broadcast reaction update to both participants
try:
diff --git a/backend/routes/profile.py b/backend/routes/profile.py
index 04a38ea..31d1794 100644
--- a/backend/routes/profile.py
+++ b/backend/routes/profile.py
@@ -275,6 +275,9 @@ async def get_user_by_username(
"""
Get user profile by username
"""
+ if not username or not is_valid_username(username):
+ raise HTTPException(status_code=400, detail="Invalid username format")
+
user = db.query(User).filter(User.username == username).first()
if not user:
@@ -322,6 +325,9 @@ async def get_user_by_id(
"""
Get user profile by user ID
"""
+ if user_id <= 0:
+ raise HTTPException(status_code=400, detail="Invalid user ID")
+
user = db.query(User).filter(User.id == user_id).first()
if not user:
diff --git a/backend/utils.py b/backend/utils.py
index 2d294dd..660b475 100644
--- a/backend/utils.py
+++ b/backend/utils.py
@@ -4,7 +4,7 @@ import jwt
from typing import Optional, Any
import bcrypt
-from constants import *
+from constants import ACCESS_TOKEN_EXPIRE_HOURS, JWT_SECRET_KEY, JWT_ALGORITHM
# JWT Helper Functions
def create_token(user_id: int, username: str, session_id: str) -> str:
diff --git a/frontend/index.html b/frontend/index.html
index 9038e1e..e0e69ad 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
Loading...
-
+
diff --git a/frontend/src/core/push-notifications/push-notifications.ts b/frontend/src/core/push-notifications/push-notifications.ts
index bef62c9..a0760dd 100644
--- a/frontend/src/core/push-notifications/push-notifications.ts
+++ b/frontend/src/core/push-notifications/push-notifications.ts
@@ -3,6 +3,7 @@ import { isElectron } from "@/core/electron/electron";
import { websocket } from "@/core/websocket";
import type { NewMessageWebSocketMessage, WebSocketMessage } from "@/core/types";
import serviceWorker from "./service-worker?worker&url";
+import logo from "@/images/logo.svg";
export interface PushSubscriptionData {
endpoint: string;
@@ -111,7 +112,7 @@ async function showMessageNotification(message: any): Promise {
body: message.content.length > 100
? message.content.substring(0, 100) + "..."
: message.content,
- icon: message.profile_picture || "/logo.png",
+ icon: message.profile_picture || logo,
tag: `message_${message.id}`,
data: {
type: "public_message",
diff --git a/frontend/src/core/push-notifications/service-worker.ts b/frontend/src/core/push-notifications/service-worker.ts
index ff40735..b111c16 100644
--- a/frontend/src/core/push-notifications/service-worker.ts
+++ b/frontend/src/core/push-notifications/service-worker.ts
@@ -1,5 +1,7 @@
///
+import logo from "@/images/logo.svg";
+
declare const self: ServiceWorkerGlobalScope;
interface NotificationPayload {
@@ -36,8 +38,8 @@ self.addEventListener("push", function(event: ExtendableEvent) {
const options: NotificationOptions = {
body: data.body,
- icon: data.icon || "/logo.png",
- badge: "/logo.png",
+ icon: data.icon || logo,
+ badge: logo,
image: data.image,
tag: data.tag || "message",
data: data.data,
diff --git a/frontend/src/core/websocket.ts b/frontend/src/core/websocket.ts
index 435a113..d4400c1 100644
--- a/frontend/src/core/websocket.ts
+++ b/frontend/src/core/websocket.ts
@@ -44,6 +44,18 @@ let globalMessageHandler: ((response: WebSocketMessage) => void) | null = n
*/
let callSignalingHandler: CallSignalingHandler | null = null;
+/**
+ * Reconnection state
+ */
+let reconnectAttempts = 0;
+const MAX_RECONNECT_DELAY = 30000; // 30 seconds max delay
+const INITIAL_RECONNECT_DELAY = 1000; // Start with 1 second
+let isReconnecting = false;
+let messageHandler: ((e: MessageEvent) => void) | null = null;
+let errorHandler: ((e: Event) => void) | null = null;
+let closeHandler: ((e: CloseEvent) => void) | null = null;
+let openHandler: ((e: Event) => void) | null = null;
+
/**
* Set the global WebSocket message handler
* @param handler - Function to handle WebSocket messages
@@ -60,97 +72,208 @@ export function setCallSignalingHandler(handler: CallSignalingHandler | null): v
callSignalingHandler = handler;
}
-export function request(payload: WebSocketMessage): Promise> {
- console.log("WebSocket request:", payload);
- return new Promise((resolve, reject) => {
- function requestInner() {
- let listener: ((e: MessageEvent) => void) | null = null;
- listener = (e) => {
- resolve(JSON.parse(e.data));
- websocket.removeEventListener("message", listener!);
+/**
+ * Clean up all event listeners from the current WebSocket instance
+ * @private
+ */
+function cleanupWebSocket(): void {
+ if (websocket) {
+ if (messageHandler) {
+ websocket.removeEventListener("message", messageHandler);
+ }
+ if (errorHandler) {
+ websocket.removeEventListener("error", errorHandler);
+ }
+ if (closeHandler) {
+ websocket.removeEventListener("close", closeHandler);
+ }
+ if (openHandler) {
+ websocket.removeEventListener("open", openHandler);
+ }
+
+ // Close if still connected
+ if (websocket.readyState === WebSocket.OPEN || websocket.readyState === WebSocket.CONNECTING) {
+ try {
+ websocket.close();
+ } catch (e) {
+ // Ignore errors during cleanup
}
- websocket.addEventListener("message", listener);
- websocket.send(JSON.stringify(payload))
-
- setTimeout(() => reject("Request timed out"), 10000);
}
-
- if (websocket.readyState == 0) {
- websocket.addEventListener("open", requestInner);
- setTimeout(() => reject("Request timed out"), 10000);
- } else {
- requestInner();
- }
- })
+ }
}
/**
- * This function will wait 3 seconds and them attempts to reconnect the WebSocket.
- * If it fails, tries again in an endless loop until the connection is established
- * again.
- *
+ * Calculate exponential backoff delay
+ * @param attempt - Current reconnection attempt number
+ * @returns Delay in milliseconds
* @private
*/
-async function onError() {
- console.warn("WebSocket disconnected, retrying in 3 seconds...");
- await delay(3000);
- websocket = create();
+function getReconnectDelay(attempt: number): number {
+ const delay = INITIAL_RECONNECT_DELAY * Math.pow(2, attempt);
+ return Math.min(delay, MAX_RECONNECT_DELAY);
+}
- let listener: () => void | null;
- listener = () => {
- console.log("WebSocket successfully reconnected!");
- websocket.removeEventListener("open", listener);
+/**
+ * Handle WebSocket reconnection with exponential backoff
+ * @private
+ */
+async function reconnect(): Promise {
+ if (isReconnecting) {
+ return;
}
- websocket.addEventListener("open", listener);
- websocket.addEventListener("error", onError);
+ isReconnecting = true;
+
+ // Clean up old connection
+ cleanupWebSocket();
+
+ const delayMs = getReconnectDelay(reconnectAttempts);
+ reconnectAttempts++;
+
+ await delay(delayMs);
+
+ try {
+ websocket = create();
+ setupEventHandlers();
+ } catch (error) {
+ // If creation fails, try again
+ isReconnecting = false;
+ reconnect();
+ }
+}
+
+/**
+ * Setup event handlers for the WebSocket connection
+ * @private
+ */
+function setupEventHandlers(): void {
+ // Message handler
+ messageHandler = (e: MessageEvent) => {
+ try {
+ const response: WebSocketMessage = JSON.parse(e.data);
+
+ // Handle call signaling messages
+ if (callSignalingHandler && response.type === "call_signaling" && response.data) {
+ callSignalingHandler.handleWebSocketMessage(response.data);
+ }
+
+ // Handle status and typing messages
+ if (response.type === "statusUpdate") {
+ onlineStatusManager.handleStatusUpdate(response as any);
+ } else if (response.type === "typing") {
+ typingManager.handleTyping(response as any);
+ } else if (response.type === "stopTyping") {
+ typingManager.handleStopTyping(response as any);
+ } else if (response.type === "dmTyping") {
+ typingManager.handleDmTyping(response as any);
+ } else if (response.type === "stopDmTyping") {
+ typingManager.handleStopDmTyping(response as any);
+ } else if (response.type === "suspended") {
+ // Handle account suspension
+ const { setSuspended } = useAppState.getState();
+ const reason = response.data?.reason || "No reason provided";
+ setSuspended(reason);
+ // Close WebSocket connection
+ websocket.close();
+ } else if (response.type === "account_deleted") {
+ // Handle account deletion - silent logout
+ const { logout } = useAppState.getState();
+ logout();
+ // Close WebSocket connection
+ websocket.close();
+ }
+
+ // Route message to global handler if set
+ if (globalMessageHandler) {
+ globalMessageHandler(response);
+ }
+ } catch (error) {
+ console.error("Error parsing WebSocket message:", error);
+ }
+ };
+ websocket.addEventListener("message", messageHandler);
+
+ // Open handler
+ openHandler = () => {
+ reconnectAttempts = 0; // Reset on successful connection
+ isReconnecting = false;
+ };
+ websocket.addEventListener("open", openHandler);
+
+ // Error handler
+ errorHandler = () => {
+ // Don't reconnect immediately on error - let close handler handle it
+ // This prevents double reconnection attempts
+ };
+ websocket.addEventListener("error", errorHandler);
+
+ // Close handler
+ closeHandler = (e: CloseEvent) => {
+ // Don't reconnect if it was a clean close (e.g., logout, suspension)
+ if (e.code === 1000 || e.code === 1001) {
+ return;
+ }
+
+ // Reconnect for unexpected closes
+ if (!isReconnecting) {
+ reconnect();
+ }
+ };
+ websocket.addEventListener("close", closeHandler);
+}
+
+export function request(payload: WebSocketMessage): Promise> {
+ console.log("WebSocket request:", payload);
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ reject(new Error("Request timed out"));
+ }, 10000);
+
+ function requestInner() {
+ if (websocket.readyState !== WebSocket.OPEN) {
+ clearTimeout(timeoutId);
+ reject(new Error("WebSocket is not open"));
+ return;
+ }
+
+ const listener = (e: MessageEvent) => {
+ clearTimeout(timeoutId);
+ try {
+ resolve(JSON.parse(e.data));
+ } catch (error) {
+ reject(error);
+ }
+ websocket.removeEventListener("message", listener);
+ };
+
+ websocket.addEventListener("message", listener);
+
+ try {
+ websocket.send(JSON.stringify(payload));
+ } catch (error) {
+ clearTimeout(timeoutId);
+ websocket.removeEventListener("message", listener);
+ reject(error);
+ }
+ }
+
+ if (websocket.readyState === WebSocket.CONNECTING) {
+ const openListener = () => {
+ websocket.removeEventListener("open", openListener);
+ requestInner();
+ };
+ websocket.addEventListener("open", openListener);
+ } else if (websocket.readyState === WebSocket.OPEN) {
+ requestInner();
+ } else {
+ clearTimeout(timeoutId);
+ reject(new Error("WebSocket is closed"));
+ }
+ });
}
// --------------
// Initialization
// --------------
-websocket.addEventListener("message", (e) => {
- try {
- const response: WebSocketMessage = JSON.parse(e.data);
-
- // Handle call signaling messages
- if (callSignalingHandler && response.type === "call_signaling" && response.data) {
- callSignalingHandler.handleWebSocketMessage(response.data);
- }
-
- // Handle status and typing messages
- if (response.type === "statusUpdate") {
- onlineStatusManager.handleStatusUpdate(response as any);
- } else if (response.type === "typing") {
- typingManager.handleTyping(response as any);
- } else if (response.type === "stopTyping") {
- typingManager.handleStopTyping(response as any);
- } else if (response.type === "dmTyping") {
- typingManager.handleDmTyping(response as any);
- } else if (response.type === "stopDmTyping") {
- typingManager.handleStopDmTyping(response as any);
- } else if (response.type === "suspended") {
- // Handle account suspension
- const { setSuspended } = useAppState.getState();
- const reason = response.data?.reason || "No reason provided";
- setSuspended(reason);
- // Close WebSocket connection
- websocket.close();
- } else if (response.type === "account_deleted") {
- // Handle account deletion - silent logout
- const { logout } = useAppState.getState();
- logout();
- // Close WebSocket connection
- websocket.close();
- }
-
- // Route message to global handler if set
- if (globalMessageHandler) {
- globalMessageHandler(response);
- }
- } catch (error) {
- console.error("Error parsing WebSocket message:", error);
- }
-});
-websocket.addEventListener("error", onError);
\ No newline at end of file
+setupEventHandlers();
\ No newline at end of file
diff --git a/frontend/src/css/_components.scss b/frontend/src/css/_components.scss
index 454d843..0f066b9 100644
--- a/frontend/src/css/_components.scss
+++ b/frontend/src/css/_components.scss
@@ -53,7 +53,7 @@ button, input {
}
&.warning {
- color: #ff9800; // Orange color for warnings
+ color: $color-dark-tertiary; // Purple-themed warning color
}
&.small mdui-icon {
diff --git a/frontend/src/css/_material.scss b/frontend/src/css/_material.scss
index 281337f..b9c53c2 100644
--- a/frontend/src/css/_material.scss
+++ b/frontend/src/css/_material.scss
@@ -54,11 +54,6 @@ $color-dark-surface-container-highest: rgb(55 51 57);
$color-dark-surface-primary-container-lightened: color.adjust($color-dark-primary-container, $lightness: 5%);
$color-dark-surface-container-lightened: color.adjust($color-dark-surface-container, $lightness: 5%);
-// custom colors
-$color-1: rgb(82, 109, 246);
-$color-2: rgb(65, 11, 113);
-$color-4: rgb(95, 26, 198);
-$color-3: rgb(49, 71, 179);
// Light
$color-light-primary: rgb(31 101 134);
$color-light-surface-tint: rgb(31 101 134);
diff --git a/frontend/src/pages/chat/css/left-panel.module.scss b/frontend/src/pages/chat/css/left-panel.module.scss
index 375f340..932a64f 100644
--- a/frontend/src/pages/chat/css/left-panel.module.scss
+++ b/frontend/src/pages/chat/css/left-panel.module.scss
@@ -25,6 +25,7 @@
justify-content: center;
align-items: center;
padding: 16px;
+ user-select: none;
.logo {
$size: 35px;