mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Add member count API for public chat
This commit is contained in:
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user