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 asyncio
import time import time
from fastapi import FastAPI, Request from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import subprocess import subprocess
import sys import sys
import os import os
from routes import account, messaging, profile, push, webrtc, devices, moderation import httpx
import logging import logging
from backend.shared.models import User # Gateway doesn't need direct model access - it's a stateless proxy
from backend.shared.constants import OWNER_USERNAME # Gateway doesn't need constants - it's a stateless proxy
from backend.shared.utils import get_client_ip from backend.shared.utils import get_client_ip
from backend.shared.db import POOL_CONFIG, SessionLocal # Gateway doesn't need database access - it's a stateless proxy
from logging_config import access_logger # noqa: F401 - ensure loggers configured from backend.logging_config import access_logger # noqa: F401 - ensure loggers configured
from security.audit import log_access from backend.security.audit import log_access
from security.rate_limit import limiter from backend.security.rate_limit import limiter
from slowapi.middleware import SlowAPIMiddleware 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") logger = logging.getLogger("uvicorn.error")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# Startup - run migration in subprocess to avoid logging interference # Gateway is a stateless proxy - no database operations or background tasks needed
try: logger.info("Gateway proxy service initialized - routing to microservices")
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
yield yield
logger.info("Gateway proxy service shutting down.")
# Shutdown - cancel cleanup task if it exists
if cleanup_task:
cleanup_task.cancel()
try:
await cleanup_task
except asyncio.CancelledError:
pass
# Инициализация FastAPI # Инициализация FastAPI
app = FastAPI(title="FromChat", lifespan=lifespan) app = FastAPI(title="FromChat", lifespan=lifespan)
@@ -168,11 +108,83 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Routes # Common API endpoints - route to appropriate services (defined first for priority)
app.include_router(account.router) @app.api_route("/login", methods=["POST"])
app.include_router(messaging.router) async def login(request: Request):
app.include_router(profile.router) """Login endpoint - routes to account service."""
app.include_router(push.router, prefix="/push") return await _proxy_to_service("account", "login", request)
app.include_router(webrtc.router, prefix="/webrtc")
app.include_router(devices.router, prefix="/devices") @app.api_route("/register", methods=["POST"])
app.include_router(moderation.router) 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.utils import convert_user
from backend.shared.constants import OWNER_USERNAME 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 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 from PIL import Image
import io import io
import json 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.audit import log_access, log_dm, log_public_chat, log_security
from backend.security.profanity import contains_profanity from backend.security.profanity import contains_profanity
from backend.security.rate_limit import rate_limit_per_ip 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 from backend.shared.models import FcmToken
@@ -393,7 +394,16 @@ async def _send_message_internal(
# Send push notifications for public messages # Send push notifications for public messages
try: 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: except Exception as e:
logger.error(f"Failed to send push notification for message {new_message.id}: {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) raise HTTPException(500)
return FileResponse(str(path)) 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 backend.shared.models import User, UpdateBioRequest, UserProfileResponse
from pydantic import BaseModel from pydantic import BaseModel
from backend.shared.validation import is_valid_username, is_valid_display_name from backend.shared.validation import is_valid_username, is_valid_display_name
from backend.similarity import is_user_similar_to_verified from backend.shared.similarity import is_user_similar_to_verified
from .messaging import messagingManager import os
import httpx
from backend.security.audit import log_security from backend.security.audit import log_security
from backend.security.profanity import contains_profanity from backend.security.profanity import contains_profanity
from backend.security.rate_limit import rate_limit_per_ip from backend.security.rate_limit import rate_limit_per_ip
@@ -479,7 +480,16 @@ async def suspend_user(
# Send WebSocket suspension message # Send WebSocket suspension message
try: 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: except Exception as e:
# Log error but don't fail the request # Log error but don't fail the request
pass pass
+24 -1
View File
@@ -1,11 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pydantic import BaseModel
from backend.shared.dependencies import get_current_user, get_db from backend.shared.dependencies import get_current_user, get_db
from backend.shared.models import User, PushSubscriptionRequest from backend.shared.models import User, PushSubscriptionRequest
import backend.push_service as push_service from backend.services.push.files import push_service
router = APIRouter() router = APIRouter()
class SendPublicNotificationRequest(BaseModel):
message_id: int
exclude_user_id: int
@router.post("/subscribe") @router.post("/subscribe")
async def subscribe_to_push_notifications( async def subscribe_to_push_notifications(
request: PushSubscriptionRequest, request: PushSubscriptionRequest,
@@ -44,3 +49,21 @@ async def unsubscribe_from_push_notifications(
raise HTTPException(status_code=500, detail="Failed to disable push notifications") raise HTTPException(status_code=500, detail="Failed to disable push notifications")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(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 os
# Import service routers # Import service routers
from routes.account import router as account_router from backend.routes.account import router as account_router
from routes.profile import router as profile_router from backend.routes.profile import router as profile_router
from routes.devices import router as device_router from backend.routes.devices import router as device_router
from routes.messaging import router as messaging_router from backend.routes.messaging import router as messaging_router
from routes.push import router as push_router from backend.routes.push import router as push_router
from routes.webrtc import router as webrtc_router from backend.routes.webrtc import router as webrtc_router
from routes.moderation import router as moderation_router from backend.routes.moderation import router as moderation_router
# Import security modules # Import security modules
from security.audit import log_access from security.audit import log_access
+3 -7
View File
@@ -1,10 +1,6 @@
from fastapi import FastAPI # Gateway service - runs the main gateway app from backend/app.py
# Gateway service - handles complex operations that Caddy cannot
# This will be expanded later with routing logic to other services
if __name__ == "__main__": if __name__ == "__main__":
app = FastAPI(title="Gateway Service") from backend.app import app
import os, uvicorn 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): class User(Base):
__tablename__ = "users" __tablename__ = "users"
__table_args__ = {"schema": "account_schema"}
id = Column(BigInteger, primary_key=True, index=True) id = Column(BigInteger, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False) username = Column(String(50), unique=True, index=True, nullable=False)
@@ -42,9 +43,10 @@ class User(Base):
class Message(Base): class Message(Base):
__tablename__ = "messages" __tablename__ = "messages"
__table_args__ = {"schema": "messaging_schema"}
id = Column(BigInteger, primary_key=True, index=True) 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 = Column(Text, nullable=False)
content_type = Column(String(50), default="text") content_type = Column(String(50), default="text")
encrypted_content = Column(Text, nullable=True) encrypted_content = Column(Text, nullable=True)
@@ -53,8 +55,8 @@ class Message(Base):
edited_at = Column(DateTime, nullable=True) edited_at = Column(DateTime, nullable=True)
edited = Column(Boolean, default=False) edited = Column(Boolean, default=False)
deleted = Column(Boolean, default=False) deleted = Column(Boolean, default=False)
reply_to_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("messages.id"), nullable=True) thread_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=True)
is_public = Column(Boolean, default=False) is_public = Column(Boolean, default=False)
# Relationships # Relationships
@@ -63,13 +65,15 @@ class Message(Base):
reply_to = relationship("Message", remote_side=[id], foreign_keys=[reply_to_id]) reply_to = relationship("Message", remote_side=[id], foreign_keys=[reply_to_id])
thread = relationship("Message", remote_side=[id], foreign_keys=[thread_id]) thread = relationship("Message", remote_side=[id], foreign_keys=[thread_id])
reactions = relationship("MessageReaction", back_populates="message", cascade="all, delete-orphan") reactions = relationship("MessageReaction", back_populates="message", cascade="all, delete-orphan")
files = relationship("MessageFile", back_populates="message", cascade="all, delete-orphan")
class MessageRecipient(Base): class MessageRecipient(Base):
__tablename__ = "message_recipients" __tablename__ = "message_recipients"
__table_args__ = {"schema": "messaging_schema"}
id = Column(BigInteger, primary_key=True, index=True) id = Column(BigInteger, 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)
recipient_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True) recipient_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
read_at = Column(DateTime, nullable=True) read_at = Column(DateTime, nullable=True)
delivered_at = Column(DateTime, nullable=True) delivered_at = Column(DateTime, nullable=True)
encrypted_key = Column(Text, nullable=True) encrypted_key = Column(Text, nullable=True)
@@ -80,10 +84,11 @@ class MessageRecipient(Base):
class MessageReaction(Base): class MessageReaction(Base):
__tablename__ = "message_reactions" __tablename__ = "message_reactions"
__table_args__ = {"schema": "messaging_schema"}
id = Column(BigInteger, primary_key=True, index=True) id = Column(BigInteger, 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)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False, index=True) user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False, index=True)
reaction = Column(String(50), nullable=False) reaction = Column(String(50), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
@@ -92,9 +97,10 @@ class MessageReaction(Base):
class Device(Base): class Device(Base):
__tablename__ = "devices" __tablename__ = "devices"
__table_args__ = {"schema": "device_schema"}
id = Column(BigInteger, primary_key=True, index=True) 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_id = Column(String(255), unique=True, nullable=False, index=True)
device_name = Column(String(255), nullable=True) device_name = Column(String(255), nullable=True)
device_type = Column(String(50), nullable=True) device_type = Column(String(50), nullable=True)
@@ -110,10 +116,11 @@ class Device(Base):
class PushSubscription(Base): class PushSubscription(Base):
__tablename__ = "push_subscriptions" __tablename__ = "push_subscriptions"
__table_args__ = {"schema": "push_schema"}
id = Column(BigInteger, primary_key=True, index=True) 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(BigInteger, ForeignKey("devices.id"), nullable=True, index=True) device_id = Column(BigInteger, ForeignKey("device_schema.devices.id"), nullable=True, index=True)
endpoint = Column(String(500), nullable=False) endpoint = Column(String(500), nullable=False)
p256dh = Column(String(255), nullable=False) p256dh = Column(String(255), nullable=False)
auth = Column(String(255), nullable=False) auth = Column(String(255), nullable=False)
@@ -126,10 +133,11 @@ class PushSubscription(Base):
class WebRTCSession(Base): class WebRTCSession(Base):
__tablename__ = "webrtc_sessions" __tablename__ = "webrtc_sessions"
__table_args__ = {"schema": "webrtc_schema"}
id = Column(BigInteger, primary_key=True, index=True) id = Column(BigInteger, primary_key=True, index=True)
session_id = Column(String(255), unique=True, nullable=False, 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) participant_ids = Column(JSON, nullable=False)
offer = Column(JSON, nullable=True) offer = Column(JSON, nullable=True)
answer = Column(JSON, nullable=True) answer = Column(JSON, nullable=True)
@@ -140,11 +148,12 @@ class WebRTCSession(Base):
class ModerationAction(Base): class ModerationAction(Base):
__tablename__ = "moderation_actions" __tablename__ = "moderation_actions"
__table_args__ = {"schema": "moderation_schema"}
id = Column(BigInteger, primary_key=True, index=True) id = Column(BigInteger, primary_key=True, index=True)
moderator_id = Column(BigInteger, ForeignKey("users.id"), nullable=False) moderator_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
target_user_id = Column(BigInteger, ForeignKey("users.id"), nullable=True) target_user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=True)
target_message_id = Column(BigInteger, ForeignKey("messages.id"), nullable=True) target_message_id = Column(BigInteger, ForeignKey("messaging_schema.messages.id"), nullable=True)
action_type = Column(String(50), nullable=False) action_type = Column(String(50), nullable=False)
reason = Column(Text, nullable=True) reason = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
@@ -153,9 +162,10 @@ class ModerationAction(Base):
class MessageFile(Base): class MessageFile(Base):
__tablename__ = "message_file" __tablename__ = "message_file"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True) 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) path = Column(Text, nullable=False)
name = Column(Text, nullable=False) name = Column(Text, nullable=False)
@@ -166,7 +176,7 @@ class CryptoPublicKey(Base):
__tablename__ = "crypto_public_key" __tablename__ = "crypto_public_key"
id = Column(Integer, primary_key=True, index=True) 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) public_key_b64 = Column(Text, nullable=False)
@@ -174,16 +184,17 @@ class CryptoBackup(Base):
__tablename__ = "crypto_backup" __tablename__ = "crypto_backup"
id = Column(Integer, primary_key=True, index=True) 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) blob_json = Column(Text, nullable=False)
class DMEnvelope(Base): class DMEnvelope(Base):
__tablename__ = "dm_envelope" __tablename__ = "dm_envelope"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
sender_id = Column(BigInteger, ForeignKey("users.id"), nullable=False) sender_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
recipient_id = Column(BigInteger, ForeignKey("users.id"), nullable=False) recipient_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
iv_b64 = Column(Text, nullable=False) iv_b64 = Column(Text, nullable=False)
ciphertext_b64 = Column(Text, nullable=False) ciphertext_b64 = Column(Text, nullable=False)
salt_b64 = Column(Text, nullable=False) salt_b64 = Column(Text, nullable=False)
@@ -197,11 +208,12 @@ class DMEnvelope(Base):
class DMFile(Base): class DMFile(Base):
__tablename__ = "dm_file" __tablename__ = "dm_file"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
message_id = Column(Integer, ForeignKey("dm_envelope.id"), nullable=False, index=True) message_id = Column(Integer, ForeignKey("messaging_schema.dm_envelope.id"), nullable=False, index=True)
sender_id = Column(BigInteger, ForeignKey("users.id"), nullable=False) sender_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
recipient_id = Column(BigInteger, ForeignKey("users.id"), nullable=False) recipient_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
name = Column(Text, nullable=False) name = Column(Text, nullable=False)
path = Column(Text, nullable=False) path = Column(Text, nullable=False)
@@ -210,9 +222,10 @@ class DMFile(Base):
class FcmToken(Base): class FcmToken(Base):
__tablename__ = "fcm_token" __tablename__ = "fcm_token"
__table_args__ = {"schema": "push_schema"}
id = Column(Integer, primary_key=True, index=True) 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) token = Column(Text, nullable=False, unique=True)
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
@@ -220,10 +233,11 @@ class FcmToken(Base):
class Reaction(Base): class Reaction(Base):
__tablename__ = "reaction" __tablename__ = "reaction"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True) 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)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False) user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
emoji = Column(String(10), nullable=False) # Store emoji as string emoji = Column(String(10), nullable=False) # Store emoji as string
timestamp = Column(DateTime, default=datetime.now) timestamp = Column(DateTime, default=datetime.now)
@@ -236,10 +250,11 @@ class Reaction(Base):
class DMReaction(Base): class DMReaction(Base):
__tablename__ = "dm_reaction" __tablename__ = "dm_reaction"
__table_args__ = {"schema": "messaging_schema"}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
dm_envelope_id = Column(Integer, ForeignKey("dm_envelope.id"), nullable=False, index=True) dm_envelope_id = Column(Integer, ForeignKey("messaging_schema.dm_envelope.id"), nullable=False, index=True)
user_id = Column(BigInteger, ForeignKey("users.id"), nullable=False) user_id = Column(BigInteger, ForeignKey("account_schema.users.id"), nullable=False)
emoji = Column(String(10), nullable=False) # Store emoji as string emoji = Column(String(10), nullable=False) # Store emoji as string
timestamp = Column(DateTime, default=datetime.now) timestamp = Column(DateTime, default=datetime.now)
@@ -254,9 +269,10 @@ class DMReaction(Base):
# Tracks authenticated device sessions per user # Tracks authenticated device sessions per user
class DeviceSession(Base): class DeviceSession(Base):
__tablename__ = "device_session" __tablename__ = "device_session"
__table_args__ = {"schema": "device_schema"}
id = Column(Integer, primary_key=True, index=True) 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 for reference/debugging
raw_user_agent = Column(Text, nullable=True) raw_user_agent = Column(Text, nullable=True)
@@ -392,9 +408,10 @@ class DMReactionResponse(BaseModel):
class UpdateLog(Base): class UpdateLog(Base):
"""Stores update sequence numbers and updates for gap detection""" """Stores update sequence numbers and updates for gap detection"""
__tablename__ = "update_log" __tablename__ = "update_log"
__table_args__ = {"schema": "public"}
id = Column(Integer, primary_key=True, index=True) 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) sequence = Column(Integer, nullable=False, index=True)
updates = Column(Text, nullable=False) # JSON array of updates updates = Column(Text, nullable=False) # JSON array of updates
timestamp = Column(DateTime, default=datetime.now, index=True) 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 -- This script creates dedicated users with limited privileges for each service
-- Create service-specific database roles with limited privileges -- Create service-specific database roles with limited privileges
CREATE ROLE account_service_user LOGIN PASSWORD 'account_service_password'; -- All services use the same password from DB_PASSWORD environment variable
CREATE ROLE profile_service_user LOGIN PASSWORD 'profile_service_password'; CREATE ROLE account_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE device_service_user LOGIN PASSWORD 'device_service_password'; CREATE ROLE profile_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE messaging_service_user LOGIN PASSWORD 'messaging_service_password'; CREATE ROLE device_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE push_service_user LOGIN PASSWORD 'push_service_user_password'; CREATE ROLE messaging_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE webrtc_service_user LOGIN PASSWORD 'webrtc_service_password'; CREATE ROLE push_service_user LOGIN PASSWORD '${DB_PASSWORD}';
CREATE ROLE moderation_service_user LOGIN PASSWORD 'moderation_service_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 dedicated schemas for each service
CREATE SCHEMA IF NOT EXISTS account_schema AUTHORIZATION account_service_user; CREATE SCHEMA IF NOT EXISTS account_schema AUTHORIZATION account_service_user;
+34
View File
@@ -56,8 +56,17 @@ services:
context: .. context: ..
dockerfile: docker/Dockerfile.multi dockerfile: docker/Dockerfile.multi
target: gateway target: gateway
ports: ["8300:8300"]
environment: environment:
DATABASE_URL: postgresql://gateway_user:${DB_PASSWORD:-changeme}@database:5432/fromchat 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: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -95,6 +104,7 @@ services:
environment: environment:
DATABASE_URL: postgresql://account_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat DATABASE_URL: postgresql://account_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
JWT_SECRET: ${JWT_SECRET:-changeme} JWT_SECRET: ${JWT_SECRET:-changeme}
PORT: 8302
depends_on: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -125,6 +135,8 @@ services:
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-} VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com} VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com}
MESSAGING_SERVICE_URL: http://messaging_service:8305
PORT: 8303
depends_on: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -151,6 +163,7 @@ services:
target: device_service target: device_service
environment: environment:
DATABASE_URL: postgresql://device_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat DATABASE_URL: postgresql://device_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
PORT: 8304
depends_on: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -175,12 +188,16 @@ services:
context: .. context: ..
dockerfile: docker/Dockerfile.multi dockerfile: docker/Dockerfile.multi
target: messaging_service target: messaging_service
ports:
- "8305:8305"
environment: environment:
DATABASE_URL: postgresql://messaging_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat DATABASE_URL: postgresql://messaging_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
FIREBASE_CERT: ${FIREBASE_CERT:-} FIREBASE_CERT: ${FIREBASE_CERT:-}
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-} VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com} VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com}
PUSH_SERVICE_URL: http://push_service:8306
PORT: 8305
depends_on: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -214,6 +231,7 @@ services:
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-} VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com} VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@example.com}
PORT: 8306
depends_on: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -243,6 +261,7 @@ services:
target: webrtc_service target: webrtc_service
environment: environment:
DATABASE_URL: postgresql://webrtc_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat DATABASE_URL: postgresql://webrtc_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
PORT: 8307
depends_on: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -269,6 +288,7 @@ services:
target: moderation_service target: moderation_service
environment: environment:
DATABASE_URL: postgresql://moderation_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat DATABASE_URL: postgresql://moderation_service_user:${DB_PASSWORD:-changeme}@database:5432/fromchat
PORT: 8308
depends_on: depends_on:
migration_runner: migration_runner:
condition: service_completed_successfully condition: service_completed_successfully
@@ -315,6 +335,20 @@ services:
- gateway - gateway
profiles: ["prod"] 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: volumes:
database: database:
name: fromchat-database name: fromchat-database
+8 -4
View File
@@ -1,9 +1,10 @@
import express from 'express'; import express from 'express';
import type { Request, Response } from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { resolve } from 'path'; import { resolve } from 'path';
const app = express(); 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 backendHost = process.env.BACKEND_HOST || "http://localhost:8300";
const filePath = process.env.STATIC_FILE_PATH || "."; const filePath = process.env.STATIC_FILE_PATH || ".";
@@ -15,14 +16,17 @@ app.use('/api', createProxyMiddleware({
ws: true ws: true
})); }));
// WebSockets are handled by the general API proxy above
// Serve static files // Serve static files
app.use(express.static(resolve(filePath))); app.use(express.static(resolve(filePath)));
// SPA routing - catch all handler for client-side routing // 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')); res.sendFile(resolve(filePath, 'index.html'));
}); });
app.listen(port, () => { app.listen(port, '0.0.0.0', () => {
console.log(`Server launched on http://localhost:${port}`); 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 FROM base AS profile_service
COPY backend/routes/profile.py /app/backend/routes/profile.py COPY backend/routes/profile.py /app/backend/routes/profile.py
COPY backend/routes/messaging.py /app/backend/routes/messaging.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/security /app/backend/security/
COPY backend/logging_config.py /app/backend/logging_config.py 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/similarity.py /app/backend/similarity.py COPY backend/shared/similarity.py /app/backend/shared/similarity.py
COPY backend/services/profile/main.py /app/backend/services/profile/main.py COPY backend/services/profile/main.py /app/backend/services/profile/main.py
ENV SERVICE_NAME=profile ENV SERVICE_NAME=profile
@@ -69,17 +69,17 @@ ENV SERVICE_NAME=device
# Messaging service - minimal files only # Messaging service - minimal files only
FROM base AS messaging_service FROM base AS messaging_service
COPY backend/routes/messaging.py /app/backend/routes/messaging.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/security /app/backend/security/
COPY backend/logging_config.py /app/backend/logging_config.py 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 COPY backend/services/messaging/main.py /app/backend/services/messaging/main.py
ENV SERVICE_NAME=messaging ENV SERVICE_NAME=messaging
# Push service - minimal files only # Push service - minimal files only
FROM base AS push_service FROM base AS push_service
COPY backend/routes/push.py /app/backend/routes/push.py 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 COPY backend/services/push/main.py /app/backend/services/push/main.py
ENV SERVICE_NAME=push ENV SERVICE_NAME=push
@@ -93,7 +93,7 @@ ENV SERVICE_NAME=webrtc
FROM base AS moderation_service FROM base AS moderation_service
COPY backend/routes/moderation.py /app/backend/routes/moderation.py COPY backend/routes/moderation.py /app/backend/routes/moderation.py
COPY backend/security /app/backend/security/ 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/logging_config.py /app/backend/logging_config.py
COPY backend/services/moderation/main.py /app/backend/services/moderation/main.py COPY backend/services/moderation/main.py /app/backend/services/moderation/main.py
ENV SERVICE_NAME=moderation ENV SERVICE_NAME=moderation
@@ -103,6 +103,7 @@ FROM base AS gateway
COPY backend/app.py /app/backend/app.py COPY backend/app.py /app/backend/app.py
COPY backend/main.py /app/backend/main.py COPY backend/main.py /app/backend/main.py
COPY backend/dependencies.py /app/backend/dependencies.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/security /app/backend/security/
COPY backend/services/gateway/main.py /app/backend/services/gateway/main.py COPY backend/services/gateway/main.py /app/backend/services/gateway/main.py
ENV SERVICE_NAME=gateway ENV SERVICE_NAME=gateway