From 3b45cfda5cb1f658c03539dc5d6f9c6e0fae5e66 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 3 Apr 2026 19:20:21 +0300 Subject: [PATCH] Add member count API for public chat --- .../no-placeholder-string-sanitization.mdc | 10 +++++++ backend/services/main/routes/account.py | 30 +++++++++++++++++-- backend/services/main/routes/messaging.py | 13 ++++++++ backend/services/main/routes/profile.py | 10 +++++++ 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 .cursor/rules/no-placeholder-string-sanitization.mdc diff --git a/.cursor/rules/no-placeholder-string-sanitization.mdc b/.cursor/rules/no-placeholder-string-sanitization.mdc new file mode 100644 index 0000000..478c092 --- /dev/null +++ b/.cursor/rules/no-placeholder-string-sanitization.mdc @@ -0,0 +1,10 @@ +--- +description: Do not filter cache, API, or DB fields by matching fixed UI/placeholder English strings +alwaysApply: true +--- + +# No magic-string “sanitization” of user or message data + +- **Never** strip, null out, or rewrite stored or displayed values by comparing them to hard-coded UI strings (e.g. `"Direct messages"`, `"Direct message"`, `"User 123"`, etc.). Those strings can be legitimate **usernames, display names, or message text**. +- **Prefer**: fix the source (don’t persist placeholders; use `null`/absent fields; fix the writer). If legacy bad rows exist, use an explicit **schema/version/migration** or a **documented sentinel** agreed with the backend—not substring or equality checks on natural language. +- Applies especially to: local storage/cache layers, list previews, and any code that “cleans” strings before show or read. diff --git a/backend/services/main/routes/account.py b/backend/services/main/routes/account.py index c8e7523..16b53a1 100644 --- a/backend/services/main/routes/account.py +++ b/backend/services/main/routes/account.py @@ -1,7 +1,7 @@ from datetime import datetime from collections import defaultdict, deque import time -from fastapi import APIRouter, Depends, HTTPException, status, Request +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from sqlalchemy import inspect, text import uuid @@ -25,6 +25,17 @@ _FAILED_ATTEMPT_THRESHOLD = 5 _failed_login_attempts: dict[str, deque[float]] = defaultdict(deque) +async def _broadcast_registered_user_count_task(): + from ..db import SessionLocal + from .messaging import messagingManager + + db = SessionLocal() + try: + await messagingManager.broadcast_registered_user_count(db) + finally: + db.close() + + def _record_failed_login(identifier: str) -> bool: now = time.time() attempts = _failed_login_attempts[identifier] @@ -173,7 +184,12 @@ def login(request: Request, login_request: LoginRequest, db: Session = Depends(g @router.post("/register") @rate_limit_per_ip("3/hour") -def register(request: Request, register_request: RegisterRequest, db: Session = Depends(get_db)): +def register( + request: Request, + register_request: RegisterRequest, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), +): username = register_request.username.strip() display_name = register_request.display_name.strip() password = register_request.password.strip() @@ -288,6 +304,8 @@ def register(request: Request, register_request: RegisterRequest, db: Session = owner=is_owner, ) + background_tasks.add_task(_broadcast_registered_user_count_task) + return { "status": "success", "message": "Регистрация прошла успешно", @@ -550,6 +568,12 @@ async def _delete_user_data(user: User, db: Session): # Log error but don't fail the request pass + try: + from .messaging import messagingManager + await messagingManager.broadcast_registered_user_count(db) + except Exception: + pass + @router.post("/delete") async def delete_account( @@ -564,7 +588,7 @@ async def delete_account( raise HTTPException(status_code=400, detail="Cannot delete admin/owner account") await _delete_user_data(current_user, db) - + log_security( "self_delete_account", severity="warning", diff --git a/backend/services/main/routes/messaging.py b/backend/services/main/routes/messaging.py index a57a0d3..04b24f8 100644 --- a/backend/services/main/routes/messaging.py +++ b/backend/services/main/routes/messaging.py @@ -1045,6 +1045,8 @@ class MessaggingSocketManager: elif update_type == "statusUpdate": # Deduplicate by user ID sig_data = {"type": update_type, "userId": data.get("userId")} + elif update_type == "registeredUserCount": + sig_data = {"type": update_type, "count": data.get("count")} else: # For unknown types, use full data (less efficient but safe) sig_data = {"type": update_type, "data": data} @@ -1297,6 +1299,17 @@ class MessaggingSocketManager: if websocket in self.user_by_ws: await self._send_update(websocket, message_type, update_data, db) + async def broadcast_registered_user_count(self, db: Session): + """Notify all clients of the current non-deleted user count (public chat member count).""" + try: + n = db.query(User).filter(User.deleted.is_(False)).count() + except Exception: + return + try: + await self.broadcast({"type": "registeredUserCount", "data": {"count": n}}, db) + except Exception: + pass + async def send_update_to_user(self, user_id: int, update_type: str, update_data: dict, db: Session | None = None): """Send an update to a specific user (batched)""" for websocket in self.connections: diff --git a/backend/services/main/routes/profile.py b/backend/services/main/routes/profile.py index b0d28de..9015583 100644 --- a/backend/services/main/routes/profile.py +++ b/backend/services/main/routes/profile.py @@ -276,6 +276,16 @@ async def update_user_bio( } +@router.get("/user/stats/registered-count") +def get_registered_user_count( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + """Number of registered accounts (non-deleted users).""" + n = db.query(User).filter(User.deleted.is_(False)).count() + return {"count": n} + + @router.get("/user/{username}") async def get_user_by_username( username: str,