Fix inter-services communication

This commit is contained in:
2026-01-07 15:07:30 +03:00
Unverified
parent ef66025dba
commit 8ab5e4e071
17 changed files with 287 additions and 160 deletions
+102 -90
View File
@@ -1,103 +1,43 @@
import asyncio
import time
from fastapi import FastAPI, Request
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import subprocess
import sys
import os
from routes import account, messaging, profile, push, webrtc, devices, moderation
import httpx
import logging
from backend.shared.models import User
from backend.shared.constants import OWNER_USERNAME
# Gateway doesn't need direct model access - it's a stateless proxy
# Gateway doesn't need constants - it's a stateless proxy
from backend.shared.utils import get_client_ip
from backend.shared.db import POOL_CONFIG, SessionLocal
from logging_config import access_logger # noqa: F401 - ensure loggers configured
from security.audit import log_access
from security.rate_limit import limiter
# Gateway doesn't need database access - it's a stateless proxy
from backend.logging_config import access_logger # noqa: F401 - ensure loggers configured
from backend.security.audit import log_access
from backend.security.rate_limit import limiter
from slowapi.middleware import SlowAPIMiddleware
# Service URL mapping for routing
SERVICE_URLS = {
"account": os.getenv("ACCOUNT_SERVICE_URL", "http://account_service:8302"),
"profile": os.getenv("PROFILE_SERVICE_URL", "http://profile_service:8303"),
"devices": os.getenv("DEVICE_SERVICE_URL", "http://device_service:8304"),
"messaging": os.getenv("MESSAGING_SERVICE_URL", "http://messaging_service:8305"),
"push": os.getenv("PUSH_SERVICE_URL", "http://push_service:8306"),
"webrtc": os.getenv("WEBRTC_SERVICE_URL", "http://webrtc_service:8307"),
"moderation": os.getenv("MODERATION_SERVICE_URL", "http://moderation_service:8308"),
}
logger = logging.getLogger("uvicorn.error")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup - run migration in subprocess to avoid logging interference
try:
logger.info("Starting database migration check...")
# Run migration in a separate process
subprocess.run(
[
sys.executable,
"-c",
"import sys; sys.path.append('.'); from migration import run_migrations; run_migrations()"
],
cwd=os.path.dirname(os.path.abspath(__file__))
)
except Exception as e:
logger.error(f"Failed to run database migrations: {e}")
raise
try:
with SessionLocal() as db:
owner = db.query(User).filter(User.id == 1).first()
if owner and not owner.verified:
owner.verified = True
db.commit()
logger.info(f"Owner user '{OWNER_USERNAME}' has been verified")
elif owner and owner.verified:
logger.info(f"Owner user '{OWNER_USERNAME}' is already verified")
else:
logger.warning(f"Owner user '{OWNER_USERNAME}' not found")
except Exception as e:
logger.error(f"Failed to ensure owner verification: {e}")
logger.info(
"SQLAlchemy pool configured (size=%s, max_overflow=%s, timeout=%ss, recycle=%ss, pre_ping=%s)",
POOL_CONFIG["pool_size"],
POOL_CONFIG["max_overflow"],
POOL_CONFIG["pool_timeout"],
POOL_CONFIG["pool_recycle"],
POOL_CONFIG["pool_pre_ping"],
)
# Start the messaging cleanup task
try:
from routes.messaging import messagingManager
messagingManager.start_cleanup_task()
logger.info("Messaging cleanup task started")
except Exception as e:
logger.error(f"Failed to start messaging cleanup task: {e}")
# Reset all rate limits on startup to ensure clean state
# This prevents rate limits from persisting across restarts
try:
from security.rate_limit import reset_all_rate_limits
cleared = reset_all_rate_limits()
if cleared > 0:
logger.info(f"Cleared {cleared} rate limit entries on startup")
except Exception as e:
logger.warning(f"Failed to reset rate limits on startup: {e}")
# Start the rate limit cleanup task
try:
from security.rate_limit import start_rate_limit_cleanup_task
cleanup_task = asyncio.create_task(start_rate_limit_cleanup_task())
logger.info("Rate limit cleanup task started")
except Exception as e:
logger.error(f"Failed to start rate limit cleanup task: {e}")
cleanup_task = None
# Gateway is a stateless proxy - no database operations or background tasks needed
logger.info("Gateway proxy service initialized - routing to microservices")
yield
# Shutdown - cancel cleanup task if it exists
if cleanup_task:
cleanup_task.cancel()
try:
await cleanup_task
except asyncio.CancelledError:
pass
logger.info("Gateway proxy service shutting down.")
# Инициализация FastAPI
app = FastAPI(title="FromChat", lifespan=lifespan)
@@ -168,11 +108,83 @@ app.add_middleware(
allow_headers=["*"],
)
# Routes
app.include_router(account.router)
app.include_router(messaging.router)
app.include_router(profile.router)
app.include_router(push.router, prefix="/push")
app.include_router(webrtc.router, prefix="/webrtc")
app.include_router(devices.router, prefix="/devices")
app.include_router(moderation.router)
# Common API endpoints - route to appropriate services (defined first for priority)
@app.api_route("/login", methods=["POST"])
async def login(request: Request):
"""Login endpoint - routes to account service."""
return await _proxy_to_service("account", "login", request)
@app.api_route("/register", methods=["POST"])
async def register(request: Request):
"""Register endpoint - routes to account service."""
return await _proxy_to_service("account", "register", request)
@app.api_route("/chat/ws", methods=["GET"])
async def chat_websocket(request: Request):
"""Chat WebSocket endpoint - routes to messaging service."""
return await _proxy_to_service("messaging", "chat/ws", request)
# API routes - route to appropriate microservices
@app.api_route("/account/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_account(path: str, request: Request):
"""Proxy account service requests."""
return await _proxy_to_service("account", path, request)
@app.api_route("/profile/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_profile(path: str, request: Request):
"""Proxy profile service requests."""
return await _proxy_to_service("profile", path, request)
@app.api_route("/devices/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_devices(path: str, request: Request):
"""Proxy device service requests."""
return await _proxy_to_service("devices", path, request)
@app.api_route("/messaging/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_messaging(path: str, request: Request):
"""Proxy messaging service requests."""
return await _proxy_to_service("messaging", path, request)
@app.api_route("/push/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_push(path: str, request: Request):
"""Proxy push service requests."""
return await _proxy_to_service("push", path, request)
@app.api_route("/webrtc/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_webrtc(path: str, request: Request):
"""Proxy WebRTC service requests."""
return await _proxy_to_service("webrtc", path, request)
@app.api_route("/moderation/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_moderation(path: str, request: Request):
"""Proxy moderation service requests."""
return await _proxy_to_service("moderation", path, request)
async def _proxy_to_service(service: str, path: str, request: Request):
"""Helper function to proxy requests to microservices."""
service_url = SERVICE_URLS[service]
target_url = f"{service_url}/{service}/{path}"
# Get request body
body = await request.body()
# Prepare headers (remove host header)
headers = dict(request.headers)
headers.pop("host", None)
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.request(
method=request.method,
url=target_url,
headers=headers,
content=body,
params=request.query_params,
)
return response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text
except httpx.RequestError as exc:
logging.error(f"Error communicating with {service} service: {exc}")
raise HTTPException(status_code=503, detail=f"Service {service} unavailable")
# Routes are handled by the catch-all proxy above
+31 -3
View File
@@ -20,7 +20,8 @@ from backend.shared.dependencies import get_current_user, get_db
from backend.shared.utils import convert_user
from backend.shared.constants import OWNER_USERNAME
from backend.shared.models import Message, SendMessageRequest, EditMessageRequest, User, DMEnvelope, MessageFile, DMFile, Reaction, ReactionRequest, ReactionResponse, DMReaction, DMReactionRequest, DMReactionResponse, UpdateLog
import backend.push_service as push_service
import os
import httpx
from PIL import Image
import io
import json
@@ -29,7 +30,7 @@ from better_profanity import profanity as _bp
from backend.security.audit import log_access, log_dm, log_public_chat, log_security
from backend.security.profanity import contains_profanity
from backend.security.rate_limit import rate_limit_per_ip
from backend.websocket.utils import authenticate_user
from backend.services.messaging.files.websocket.utils import authenticate_user
from backend.shared.models import FcmToken
@@ -393,7 +394,16 @@ async def _send_message_internal(
# Send push notifications for public messages
try:
await push_service.send_public_message_notification(db, new_message, exclude_user_id=current_user.id)
push_service_url = os.getenv("PUSH_SERVICE_URL", "http://push_service:8306")
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{push_service_url}/push/send-public-notification",
json={
"message_id": new_message.id,
"exclude_user_id": current_user.id
}
)
response.raise_for_status()
except Exception as e:
logger.error(f"Failed to send push notification for message {new_message.id}: {e}")
@@ -1561,3 +1571,21 @@ async def get_file_encrypted(filename: str, current_user: User = Depends(get_cur
raise HTTPException(500)
return FileResponse(str(path))
class SendSuspensionRequest(BaseModel):
user_id: int
reason: str
@router.post("/send-suspension")
async def send_suspension_to_user(
request: SendSuspensionRequest,
db: Session = Depends(get_db)
):
"""Send suspension message to user via WebSocket (called by profile service)"""
try:
await messagingManager.send_suspension_to_user(request.user_id, request.reason)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+13 -3
View File
@@ -13,8 +13,9 @@ from backend.shared.dependencies import get_db, get_current_user
from backend.shared.models import User, UpdateBioRequest, UserProfileResponse
from pydantic import BaseModel
from backend.shared.validation import is_valid_username, is_valid_display_name
from backend.similarity import is_user_similar_to_verified
from .messaging import messagingManager
from backend.shared.similarity import is_user_similar_to_verified
import os
import httpx
from backend.security.audit import log_security
from backend.security.profanity import contains_profanity
from backend.security.rate_limit import rate_limit_per_ip
@@ -479,7 +480,16 @@ async def suspend_user(
# Send WebSocket suspension message
try:
await messagingManager.send_suspension_to_user(user_id, request.reason)
messaging_service_url = os.getenv("MESSAGING_SERVICE_URL", "http://messaging_service:8305")
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{messaging_service_url}/messaging/send-suspension",
json={
"user_id": user_id,
"reason": request.reason
}
)
response.raise_for_status()
except Exception as e:
# Log error but don't fail the request
pass
+24 -1
View File
@@ -1,11 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from backend.shared.dependencies import get_current_user, get_db
from backend.shared.models import User, PushSubscriptionRequest
import backend.push_service as push_service
from backend.services.push.files import push_service
router = APIRouter()
class SendPublicNotificationRequest(BaseModel):
message_id: int
exclude_user_id: int
@router.post("/subscribe")
async def subscribe_to_push_notifications(
request: PushSubscriptionRequest,
@@ -44,3 +49,21 @@ async def unsubscribe_from_push_notifications(
raise HTTPException(status_code=500, detail="Failed to disable push notifications")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/send-public-notification")
async def send_public_message_notification(
request: SendPublicNotificationRequest,
db: Session = Depends(get_db)
):
"""Send push notification for public message (called by messaging service)"""
try:
# Get the message from database
from backend.shared.models import Message
message = db.query(Message).filter(Message.id == request.message_id).first()
if not message:
raise HTTPException(status_code=404, detail="Message not found")
await push_service.send_public_message_notification(db, message, exclude_user_id=request.exclude_user_id)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+7 -7
View File
@@ -10,13 +10,13 @@ import uvicorn
import os
# Import service routers
from routes.account import router as account_router
from routes.profile import router as profile_router
from routes.devices import router as device_router
from routes.messaging import router as messaging_router
from routes.push import router as push_router
from routes.webrtc import router as webrtc_router
from routes.moderation import router as moderation_router
from backend.routes.account import router as account_router
from backend.routes.profile import router as profile_router
from backend.routes.devices import router as device_router
from backend.routes.messaging import router as messaging_router
from backend.routes.push import router as push_router
from backend.routes.webrtc import router as webrtc_router
from backend.routes.moderation import router as moderation_router
# Import security modules
from security.audit import log_access
+3 -7
View File
@@ -1,10 +1,6 @@
from fastapi import FastAPI
# Gateway service - handles complex operations that Caddy cannot
# This will be expanded later with routing logic to other services
# Gateway service - runs the main gateway app from backend/app.py
if __name__ == "__main__":
app = FastAPI(title="Gateway Service")
from backend.app import app
import os, uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 8301)))
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 8300)))
+46 -29
View File
@@ -9,6 +9,7 @@ Base = declarative_base()
class User(Base):
__tablename__ = "users"
__table_args__ = {"schema": "account_schema"}
id = Column(BigInteger, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
@@ -42,9 +43,10 @@ class User(Base):
class Message(Base):
__tablename__ = "messages"
__table_args__ = {"schema": "messaging_schema"}
id = Column(BigInteger, primary_key=True, index=True)
sender_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
sender_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
content = Column(Text, nullable=False)
content_type = Column(String(50), default="text")
encrypted_content = Column(Text, nullable=True)
@@ -53,8 +55,8 @@ class Message(Base):
edited_at = Column(DateTime, nullable=True)
edited = Column(Boolean, default=False)
deleted = Column(Boolean, default=False)
reply_to_id = Column(BigInteger, ForeignKey("messages.id"), nullable=True)
thread_id = Column(BigInteger, ForeignKey("messages.id"), nullable=True)
reply_to_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=True)
thread_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=True)
is_public = Column(Boolean, default=False)
# Relationships
@@ -63,13 +65,15 @@ class Message(Base):
reply_to = relationship("Message", remote_side=[id], foreign_keys=[reply_to_id])
thread = relationship("Message", remote_side=[id], foreign_keys=[thread_id])
reactions = relationship("MessageReaction", back_populates="message", cascade="all, delete-orphan")
files = relationship("MessageFile", back_populates="message", cascade="all, delete-orphan")
class MessageRecipient(Base):
__tablename__ = "message_recipients"
__table_args__ = {"schema": "messaging_schema"}
id = Column(BigInteger, primary_key=True, index=True)
message_id = Column(BigInteger, ForeignKey("messages.id"), nullable=False, index=True)
recipient_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
message_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=False, index=True)
recipient_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
read_at = Column(DateTime, nullable=True)
delivered_at = Column(DateTime, nullable=True)
encrypted_key = Column(Text, nullable=True)
@@ -80,10 +84,11 @@ class MessageRecipient(Base):
class MessageReaction(Base):
__tablename__ = "message_reactions"
__table_args__ = {"schema": "messaging_schema"}
id = Column(BigInteger, primary_key=True, index=True)
message_id = Column(BigInteger, ForeignKey("messages.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
message_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
reaction = Column(String(50), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
@@ -92,9 +97,10 @@ class MessageReaction(Base):
class Device(Base):
__tablename__ = "devices"
__table_args__ = {"schema": "device_schema"}
id = Column(BigInteger, primary_key=True, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
device_id = Column(String(255), unique=True, nullable=False, index=True)
device_name = Column(String(255), nullable=True)
device_type = Column(String(50), nullable=True)
@@ -110,10 +116,11 @@ class Device(Base):
class PushSubscription(Base):
__tablename__ = "push_subscriptions"
__table_args__ = {"schema": "push_schema"}
id = Column(BigInteger, primary_key=True, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
device_id = Column(BigInteger, ForeignKey("devices.id"), nullable=True, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
device_id = Column(BigInteger, ForeignKey("device_schema.devices.id"), nullable=True, index=True)
endpoint = Column(String(500), nullable=False)
p256dh = Column(String(255), nullable=False)
auth = Column(String(255), nullable=False)
@@ -126,10 +133,11 @@ class PushSubscription(Base):
class WebRTCSession(Base):
__tablename__ = "webrtc_sessions"
__table_args__ = {"schema": "webrtc_schema"}
id = Column(BigInteger, primary_key=True, index=True)
session_id = Column(String(255), unique=True, nullable=False, index=True)
initiator_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
initiator_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
participant_ids = Column(JSON, nullable=False)
offer = Column(JSON, nullable=True)
answer = Column(JSON, nullable=True)
@@ -140,11 +148,12 @@ class WebRTCSession(Base):
class ModerationAction(Base):
__tablename__ = "moderation_actions"
__table_args__ = {"schema": "moderation_schema"}
id = Column(BigInteger, primary_key=True, index=True)
moderator_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
target_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True)
target_message_id = Column(BigInteger, ForeignKey("messages.id"), nullable=True)
moderator_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
target_user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=True)
target_message_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=True)
action_type = Column(String(50), nullable=False)
reason = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
@@ -153,9 +162,10 @@ class ModerationAction(Base):
class MessageFile(Base):
__tablename__ = "message_file"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True)
message_id = Column(BigInteger, ForeignKey("messages.id"), nullable=False, index=True)
message_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=False, index=True)
path = Column(Text, nullable=False)
name = Column(Text, nullable=False)
@@ -166,7 +176,7 @@ class CryptoPublicKey(Base):
__tablename__ = "crypto_public_key"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, unique=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, unique=True)
public_key_b64 = Column(Text, nullable=False)
@@ -174,16 +184,17 @@ class CryptoBackup(Base):
__tablename__ = "crypto_backup"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, unique=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, unique=True)
blob_json = Column(Text, nullable=False)
class DMEnvelope(Base):
__tablename__ = "dm_envelope"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True)
sender_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
recipient_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
sender_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
recipient_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
iv_b64 = Column(Text, nullable=False)
ciphertext_b64 = Column(Text, nullable=False)
salt_b64 = Column(Text, nullable=False)
@@ -197,11 +208,12 @@ class DMEnvelope(Base):
class DMFile(Base):
__tablename__ = "dm_file"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True)
message_id = Column(Integer, ForeignKey("dm_envelope.id"), nullable=False, index=True)
sender_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
recipient_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
message_id = Column(Integer, ForeignKey("messaging_schema.dm_envelope.id"), nullable=False, index=True)
sender_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
recipient_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
name = Column(Text, nullable=False)
path = Column(Text, nullable=False)
@@ -210,9 +222,10 @@ class DMFile(Base):
class FcmToken(Base):
__tablename__ = "fcm_token"
__table_args__ = {"schema": "push_schema"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
token = Column(Text, nullable=False, unique=True)
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
@@ -220,10 +233,11 @@ class FcmToken(Base):
class Reaction(Base):
__tablename__ = "reaction"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True)
message_id = Column(BigInteger, ForeignKey("messages.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
message_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
emoji = Column(String(10), nullable=False) # Store emoji as string
timestamp = Column(DateTime, default=datetime.now)
@@ -236,10 +250,11 @@ class Reaction(Base):
class DMReaction(Base):
__tablename__ = "dm_reaction"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True)
dm_envelope_id = Column(Integer, ForeignKey("dm_envelope.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False)
dm_envelope_id = Column(Integer, ForeignKey("messaging_schema.dm_envelope.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
emoji = Column(String(10), nullable=False) # Store emoji as string
timestamp = Column(DateTime, default=datetime.now)
@@ -254,9 +269,10 @@ class DMReaction(Base):
# Tracks authenticated device sessions per user
class DeviceSession(Base):
__tablename__ = "device_session"
__table_args__ = {"schema": "device_schema"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
# Raw User-Agent for reference/debugging
raw_user_agent = Column(Text, nullable=True)
@@ -392,9 +408,10 @@ class DMReactionResponse(BaseModel):
class UpdateLog(Base):
"""Stores update sequence numbers and updates for gap detection"""
__tablename__ = "update_log"
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
sequence = Column(Integer, nullable=False, index=True)
updates = Column(Text, nullable=False) # JSON array of updates
timestamp = Column(DateTime, default=datetime.now, index=True)
+9 -7
View File
@@ -2,13 +2,15 @@
-- This script creates dedicated users with limited privileges for each service
-- Create service-specific database roles with limited privileges
CREATE ROLE account_service_user LOGIN PASSWORD 'account_service_password';
CREATE ROLE profile_service_user LOGIN PASSWORD 'profile_service_password';
CREATE ROLE device_service_user LOGIN PASSWORD 'device_service_password';
CREATE ROLE messaging_service_user LOGIN PASSWORD 'messaging_service_password';
CREATE ROLE push_service_user LOGIN PASSWORD 'push_service_user_password';
CREATE ROLE webrtc_service_user LOGIN PASSWORD 'webrtc_service_password';
CREATE ROLE moderation_service_user LOGIN PASSWORD 'moderation_service_password';
-- All services use the same password from DB_PASSWORD environment variable
CREATE ROLE account_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE profile_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE device_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE messaging_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE push_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE webrtc_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE moderation_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE gateway_user LOGIN PASSWORD '${DB_PASSWORD}';
-- Create dedicated schemas for each service
CREATE SCHEMA IF NOT EXISTS account_schema AUTHORIZATION account_service_user;
+34
View File
@@ -56,8 +56,17 @@ services:
context: ..
dockerfile: docker/Dockerfile.multi
target: gateway
ports: ["8300:8300"]
environment:
DATABASE_URL: postgresql://gateway_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
PORT: 8300
ACCOUNT_SERVICE_URL: http://account_service:8302
PROFILE_SERVICE_URL: http://profile_service:8303
DEVICE_SERVICE_URL: http://device_service:8304
MESSAGING_SERVICE_URL: http://messaging_service:8305
PUSH_SERVICE_URL: http://push_service:8306
WEBRTC_SERVICE_URL: http://webrtc_service:8307
MODERATION_SERVICE_URL: http://moderation_service:8308
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -95,6 +104,7 @@ services:
environment:
DATABASE_URL: postgresql://account_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
JWT_SECRET: ${JWT_SECRET:-changeme}
PORT: 8302
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -125,6 +135,8 @@ services:
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com}
MESSAGING_SERVICE_URL: http://messaging_service:8305
PORT: 8303
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -151,6 +163,7 @@ services:
target: device_service
environment:
DATABASE_URL: postgresql://device_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
PORT: 8304
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -175,12 +188,16 @@ services:
context: ..
dockerfile: docker/Dockerfile.multi
target: messaging_service
ports:
- "8305:8305"
environment:
DATABASE_URL: postgresql://messaging_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
FIREBASE_CERT: ${FIREBASE_CERT:-}
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com}
PUSH_SERVICE_URL: http://push_service:8306
PORT: 8305
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -214,6 +231,7 @@ services:
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com}
PORT: 8306
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -243,6 +261,7 @@ services:
target: webrtc_service
environment:
DATABASE_URL: postgresql://webrtc_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
PORT: 8307
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -269,6 +288,7 @@ services:
target: moderation_service
environment:
DATABASE_URL: postgresql://moderation_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
PORT: 8308
depends_on:
migration_runner:
condition: service_completed_successfully
@@ -315,6 +335,20 @@ services:
- gateway
profiles: ["prod"]
# Frontend service - serves the React app
frontend:
build:
context: ..
dockerfile: deployment/frontend/Dockerfile
ports:
- "8301:8301"
environment:
- PORT=8301
- BACKEND_HOST=http://gateway:8300
restart: unless-stopped
networks:
- fromchat_external
volumes:
database:
name: fromchat-database
+8 -4
View File
@@ -1,9 +1,10 @@
import express from 'express';
import type { Request, Response } from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { resolve } from 'path';
const app = express();
const port = process.env.PORT || 3000;
const port = Number(process.env.PORT) || 8301;
const backendHost = process.env.BACKEND_HOST || "http://localhost:8300";
const filePath = process.env.STATIC_FILE_PATH || ".";
@@ -15,14 +16,17 @@ app.use('/api', createProxyMiddleware({
ws: true
}));
// WebSockets are handled by the general API proxy above
// Serve static files
app.use(express.static(resolve(filePath)));
// SPA routing - catch all handler for client-side routing
app.use((_req, res) => {
app.use((_req: Request, res: Response) => {
res.sendFile(resolve(filePath, 'index.html'));
});
app.listen(port, () => {
console.log(`Server launched on http://localhost:${port}`);
app.listen(port, '0.0.0.0', () => {
console.log(`Backend host: ${backendHost}`);
console.log(`Server launched on http://0.0.0.0:${port}`);
});
+8 -7
View File
@@ -52,11 +52,11 @@ ENV SERVICE_NAME=account
FROM base AS profile_service
COPY backend/routes/profile.py /app/backend/routes/profile.py
COPY backend/routes/messaging.py /app/backend/routes/messaging.py
COPY backend/push_service.py /app/backend/push_service.py
COPY backend/services/push/files/push_service.py /app/backend/services/push/files/push_service.py
COPY backend/security /app/backend/security/
COPY backend/logging_config.py /app/backend/logging_config.py
COPY backend/websocket /app/backend/websocket/
COPY backend/similarity.py /app/backend/similarity.py
COPY backend/services/messaging/files/websocket /app/backend/services/messaging/files/websocket/
COPY backend/shared/similarity.py /app/backend/shared/similarity.py
COPY backend/services/profile/main.py /app/backend/services/profile/main.py
ENV SERVICE_NAME=profile
@@ -69,17 +69,17 @@ ENV SERVICE_NAME=device
# Messaging service - minimal files only
FROM base AS messaging_service
COPY backend/routes/messaging.py /app/backend/routes/messaging.py
COPY backend/push_service.py /app/backend/push_service.py
COPY backend/services/push/files/push_service.py /app/backend/services/push/files/push_service.py
COPY backend/security /app/backend/security/
COPY backend/logging_config.py /app/backend/logging_config.py
COPY backend/websocket /app/backend/websocket/
COPY backend/services/messaging/files/websocket /app/backend/services/messaging/files/websocket/
COPY backend/services/messaging/main.py /app/backend/services/messaging/main.py
ENV SERVICE_NAME=messaging
# Push service - minimal files only
FROM base AS push_service
COPY backend/routes/push.py /app/backend/routes/push.py
COPY backend/push_service.py /app/backend/push_service.py
COPY backend/services/push/files/push_service.py /app/backend/services/push/files/push_service.py
COPY backend/services/push/main.py /app/backend/services/push/main.py
ENV SERVICE_NAME=push
@@ -93,7 +93,7 @@ ENV SERVICE_NAME=webrtc
FROM base AS moderation_service
COPY backend/routes/moderation.py /app/backend/routes/moderation.py
COPY backend/security /app/backend/security/
COPY backend/similarity.py /app/backend/similarity.py
COPY backend/shared/similarity.py /app/backend/shared/similarity.py
COPY backend/logging_config.py /app/backend/logging_config.py
COPY backend/services/moderation/main.py /app/backend/services/moderation/main.py
ENV SERVICE_NAME=moderation
@@ -103,6 +103,7 @@ FROM base AS gateway
COPY backend/app.py /app/backend/app.py
COPY backend/main.py /app/backend/main.py
COPY backend/dependencies.py /app/backend/dependencies.py
COPY backend/logging_config.py /app/backend/logging_config.py
COPY backend/security /app/backend/security/
COPY backend/services/gateway/main.py /app/backend/services/gateway/main.py
ENV SERVICE_NAME=gateway