forked from ComputerTech/aprhodite
Fix #1+#8: Extract shared config module, unify JWT secret
- Create config.py with shared constants, AES-GCM helpers, and JWT helpers - app.py and routes.py now import from the single source of truth - Eliminates JWT secret mismatch (routes.py had hardcoded default) - Removes all duplicate _issue_jwt, _verify_jwt, _aesgcm_encrypt, _aesgcm_decrypt definitions - start.py also uses shared config loader
This commit is contained in:
parent
1c17a9bcf0
commit
99859f009f
107
app.py
107
app.py
|
|
@ -39,16 +39,11 @@ Socket events (server → client)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import uuid
|
|
||||||
import base64
|
|
||||||
import functools
|
import functools
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import jwt as pyjwt
|
|
||||||
import eventlet # noqa – monkey-patched in start.py before any other import
|
import eventlet # noqa – monkey-patched in start.py before any other import
|
||||||
from eventlet.queue import Queue as EvQueue
|
from eventlet.queue import Queue as EvQueue
|
||||||
|
|
||||||
|
|
@ -57,52 +52,15 @@ from flask_socketio import SocketIO, emit, join_room, disconnect
|
||||||
|
|
||||||
from database import db, init_db
|
from database import db, init_db
|
||||||
from models import User, Message, UserIgnore
|
from models import User, Message, UserIgnore
|
||||||
|
from config import (
|
||||||
# ---------------------------------------------------------------------------
|
SECRET_KEY, ADMIN_PASSWORD, DATABASE_URL,
|
||||||
# Configuration Loader
|
MAX_MSG_LEN, LOBBY, AI_FREE_LIMIT, AI_BOT_NAME,
|
||||||
# ---------------------------------------------------------------------------
|
OLLAMA_URL, VIOLET_MODEL, VIOLET_SYSTEM,
|
||||||
|
aesgcm_encrypt, aesgcm_decrypt, issue_jwt, verify_jwt,
|
||||||
def load_config():
|
|
||||||
conf = {}
|
|
||||||
config_path = os.path.join(os.path.dirname(__file__), "config.json")
|
|
||||||
if os.path.exists(config_path):
|
|
||||||
try:
|
|
||||||
with open(config_path, "r") as f:
|
|
||||||
conf = json.load(f)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Warning: Failed to load config.json: {e}")
|
|
||||||
return conf
|
|
||||||
|
|
||||||
_CONFIG = load_config()
|
|
||||||
|
|
||||||
def _get_conf(key, default=None):
|
|
||||||
# Order: Env Var > Config File > Default
|
|
||||||
return os.environ.get(key, _CONFIG.get(key, default))
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Configuration
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
SECRET_KEY = _get_conf("SECRET_KEY", uuid.uuid4().hex)
|
|
||||||
JWT_SECRET = _get_conf("JWT_SECRET", uuid.uuid4().hex)
|
|
||||||
ADMIN_PASSWORD = _get_conf("ADMIN_PASSWORD", "admin1234")
|
|
||||||
MAX_MSG_LEN = 500
|
|
||||||
LOBBY = "lobby"
|
|
||||||
AI_FREE_LIMIT = int(_get_conf("AI_FREE_LIMIT", 3))
|
|
||||||
AI_BOT_NAME = "Violet"
|
|
||||||
|
|
||||||
# Ollama
|
|
||||||
OLLAMA_URL = _get_conf("OLLAMA_URL", "http://localhost:11434")
|
|
||||||
VIOLET_MODEL = _get_conf("VIOLET_MODEL", "sam860/dolphin3-llama3.2:3b")
|
|
||||||
VIOLET_SYSTEM = (
|
|
||||||
"You are Violet, a flirtatious and sophisticated nightclub hostess at "
|
|
||||||
"an exclusive, dimly-lit members-only club. You are charming, witty, "
|
|
||||||
"and seductive — never crude or offensive. You speak with elegance, "
|
|
||||||
"mystery, and a hint of playful danger. Keep every reply to 1–3 "
|
|
||||||
"sentences maximum. You are in a private conversation with a special "
|
|
||||||
"guest who has caught your eye."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# In-process state
|
# In-process state
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -123,26 +81,6 @@ RATE_WINDOW = 5
|
||||||
ai_queue: EvQueue = EvQueue()
|
ai_queue: EvQueue = EvQueue()
|
||||||
_app_ref = None # set in create_app() for greenlet app-context access
|
_app_ref = None # set in create_app() for greenlet app-context access
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# AES-GCM helpers (server-side, transit only)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _aesgcm_encrypt(key_b64: str, plaintext: str) -> tuple:
|
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
||||||
key = base64.b64decode(key_b64)
|
|
||||||
nonce = os.urandom(12)
|
|
||||||
ct = AESGCM(key).encrypt(nonce, plaintext.encode("utf-8"), None)
|
|
||||||
return base64.b64encode(ct).decode(), base64.b64encode(nonce).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _aesgcm_decrypt(key_b64: str, ciphertext_b64: str, nonce_b64: str) -> str:
|
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
||||||
key = base64.b64decode(key_b64)
|
|
||||||
ct = base64.b64decode(ciphertext_b64)
|
|
||||||
nonce = base64.b64decode(nonce_b64)
|
|
||||||
return AESGCM(key).decrypt(nonce, ct, None).decode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Ollama integration
|
# Ollama integration
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -191,7 +129,7 @@ def _ai_worker() -> None:
|
||||||
|
|
||||||
# ── Decrypt user message (transit; key never stored) ──────────────────
|
# ── Decrypt user message (transit; key never stored) ──────────────────
|
||||||
try:
|
try:
|
||||||
plaintext = _aesgcm_decrypt(
|
plaintext = aesgcm_decrypt(
|
||||||
task["transit_key"], task["ciphertext"], task["nonce_val"]
|
task["transit_key"], task["ciphertext"], task["nonce_val"]
|
||||||
)
|
)
|
||||||
ai_text = call_ollama(plaintext)
|
ai_text = call_ollama(plaintext)
|
||||||
|
|
@ -200,7 +138,7 @@ def _ai_worker() -> None:
|
||||||
ai_text = "Mmm, something went wrong, darling 💜"
|
ai_text = "Mmm, something went wrong, darling 💜"
|
||||||
|
|
||||||
# ── Re-encrypt AI response ────────────────────────────────────────────
|
# ── Re-encrypt AI response ────────────────────────────────────────────
|
||||||
resp_ct, resp_nonce = _aesgcm_encrypt(task["transit_key"], ai_text)
|
resp_ct, resp_nonce = aesgcm_encrypt(task["transit_key"], ai_text)
|
||||||
|
|
||||||
ai_messages_used = task.get("ai_messages_used", 0)
|
ai_messages_used = task.get("ai_messages_used", 0)
|
||||||
has_ai_access = task.get("has_ai_access", False)
|
has_ai_access = task.get("has_ai_access", False)
|
||||||
|
|
@ -311,21 +249,6 @@ def _do_disconnect(sid: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _issue_jwt(user_id: int, username: str) -> str:
|
|
||||||
return pyjwt.encode(
|
|
||||||
{"user_id": user_id, "username": username,
|
|
||||||
"exp": datetime.utcnow() + timedelta(days=7)},
|
|
||||||
JWT_SECRET, algorithm="HS256",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_jwt(token: str):
|
|
||||||
try:
|
|
||||||
return pyjwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _save_pm(sender_id: int, recipient_id: int,
|
def _save_pm(sender_id: int, recipient_id: int,
|
||||||
encrypted_content: str, nonce: str) -> None:
|
encrypted_content: str, nonce: str) -> None:
|
||||||
msg = Message(
|
msg = Message(
|
||||||
|
|
@ -355,9 +278,7 @@ def create_app() -> Flask:
|
||||||
app = Flask(__name__, static_folder="static", template_folder=".")
|
app = Flask(__name__, static_folder="static", template_folder=".")
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SECRET_KEY=SECRET_KEY,
|
SECRET_KEY=SECRET_KEY,
|
||||||
SQLALCHEMY_DATABASE_URI=os.environ.get(
|
SQLALCHEMY_DATABASE_URI=DATABASE_URL,
|
||||||
"DATABASE_URL", "sqlite:///sexchat.db"
|
|
||||||
),
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE="Lax",
|
SESSION_COOKIE_SAMESITE="Lax",
|
||||||
|
|
@ -411,7 +332,7 @@ def on_connect(auth=None):
|
||||||
has_ai_access = False; ai_used = 0; jwt_username = None
|
has_ai_access = False; ai_used = 0; jwt_username = None
|
||||||
|
|
||||||
if auth and isinstance(auth, dict) and auth.get("token"):
|
if auth and isinstance(auth, dict) and auth.get("token"):
|
||||||
payload = _verify_jwt(auth["token"])
|
payload = verify_jwt(auth["token"])
|
||||||
if payload:
|
if payload:
|
||||||
db_user = db.session.get(User, payload.get("user_id"))
|
db_user = db.session.get(User, payload.get("user_id"))
|
||||||
if db_user:
|
if db_user:
|
||||||
|
|
@ -478,7 +399,7 @@ def on_join(data):
|
||||||
db.session.add(db_user); db.session.commit()
|
db.session.add(db_user); db.session.commit()
|
||||||
user.update(user_id=db_user.id, is_registered=True,
|
user.update(user_id=db_user.id, is_registered=True,
|
||||||
has_ai_access=False, ai_messages_used=0)
|
has_ai_access=False, ai_messages_used=0)
|
||||||
token = _issue_jwt(db_user.id, db_user.username)
|
token = issue_jwt(db_user.id, db_user.username)
|
||||||
|
|
||||||
elif mode == "login":
|
elif mode == "login":
|
||||||
db_user = User.query.filter(
|
db_user = User.query.filter(
|
||||||
|
|
@ -493,7 +414,7 @@ def on_join(data):
|
||||||
user["is_registered"] = True
|
user["is_registered"] = True
|
||||||
user["has_ai_access"] = db_user.has_ai_access
|
user["has_ai_access"] = db_user.has_ai_access
|
||||||
user["ai_messages_used"] = db_user.ai_messages_used
|
user["ai_messages_used"] = db_user.ai_messages_used
|
||||||
token = _issue_jwt(db_user.id, db_user.username)
|
token = issue_jwt(db_user.id, db_user.username)
|
||||||
|
|
||||||
elif mode == "restore":
|
elif mode == "restore":
|
||||||
if not user.get("user_id"):
|
if not user.get("user_id"):
|
||||||
|
|
@ -506,7 +427,7 @@ def on_join(data):
|
||||||
username = db_user.username
|
username = db_user.username
|
||||||
user["has_ai_access"] = db_user.has_ai_access
|
user["has_ai_access"] = db_user.has_ai_access
|
||||||
user["ai_messages_used"] = db_user.ai_messages_used
|
user["ai_messages_used"] = db_user.ai_messages_used
|
||||||
token = _issue_jwt(db_user.id, db_user.username)
|
token = issue_jwt(db_user.id, db_user.username)
|
||||||
|
|
||||||
else: # guest
|
else: # guest
|
||||||
if not username or not username.replace("_","").replace("-","").isalnum():
|
if not username or not username.replace("_","").replace("-","").isalnum():
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
"""
|
||||||
|
config.py – Shared configuration and utilities for SexyChat.
|
||||||
|
|
||||||
|
Centralises constants, config loading, AES-GCM helpers, and JWT helpers
|
||||||
|
so that app.py and routes.py share a single source of truth.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import base64
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
import jwt as pyjwt
|
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration Loader
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
conf = {}
|
||||||
|
config_path = os.path.join(os.path.dirname(__file__), "config.json")
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, "r") as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Warning: Failed to load config.json: {e}")
|
||||||
|
return conf
|
||||||
|
|
||||||
|
_CONFIG = load_config()
|
||||||
|
|
||||||
|
def get_conf(key, default=None):
|
||||||
|
"""Resolve a config value: Env Var → config.json → default."""
|
||||||
|
return os.environ.get(key, _CONFIG.get(key, default))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared Constants
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SECRET_KEY = get_conf("SECRET_KEY", uuid.uuid4().hex)
|
||||||
|
JWT_SECRET = get_conf("JWT_SECRET", uuid.uuid4().hex)
|
||||||
|
ADMIN_PASSWORD = get_conf("ADMIN_PASSWORD", "admin1234")
|
||||||
|
DATABASE_URL = get_conf("DATABASE_URL", "sqlite:///sexchat.db")
|
||||||
|
PAYMENT_SECRET = get_conf("PAYMENT_SECRET", "change-me-payment-secret")
|
||||||
|
CORS_ORIGINS = get_conf("CORS_ORIGINS", None)
|
||||||
|
|
||||||
|
MAX_MSG_LEN = 500
|
||||||
|
LOBBY = "lobby"
|
||||||
|
AI_FREE_LIMIT = int(get_conf("AI_FREE_LIMIT", 3))
|
||||||
|
AI_BOT_NAME = "Violet"
|
||||||
|
JWT_EXPIRY_DAYS = 7
|
||||||
|
MAX_HISTORY = 500
|
||||||
|
|
||||||
|
# Ollama
|
||||||
|
OLLAMA_URL = get_conf("OLLAMA_URL", "http://localhost:11434")
|
||||||
|
VIOLET_MODEL = get_conf("VIOLET_MODEL", "sam860/dolphin3-llama3.2:3b")
|
||||||
|
VIOLET_SYSTEM = (
|
||||||
|
"You are Violet, a flirtatious and sophisticated nightclub hostess at "
|
||||||
|
"an exclusive, dimly-lit members-only club. You are charming, witty, "
|
||||||
|
"and seductive — never crude or offensive. You speak with elegance, "
|
||||||
|
"mystery, and a hint of playful danger. Keep every reply to 1–3 "
|
||||||
|
"sentences maximum. You are in a private conversation with a special "
|
||||||
|
"guest who has caught your eye."
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# AES-GCM Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def aesgcm_encrypt(key_b64: str, plaintext: str) -> tuple:
|
||||||
|
"""Encrypt plaintext with AES-GCM. Returns (ciphertext_b64, nonce_b64)."""
|
||||||
|
key = base64.b64decode(key_b64)
|
||||||
|
nonce = os.urandom(12)
|
||||||
|
ct = AESGCM(key).encrypt(nonce, plaintext.encode("utf-8"), None)
|
||||||
|
return base64.b64encode(ct).decode(), base64.b64encode(nonce).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def aesgcm_decrypt(key_b64: str, ciphertext_b64: str, nonce_b64: str) -> str:
|
||||||
|
"""Decrypt AES-GCM ciphertext. Raises on authentication failure."""
|
||||||
|
key = base64.b64decode(key_b64)
|
||||||
|
ct = base64.b64decode(ciphertext_b64)
|
||||||
|
nonce = base64.b64decode(nonce_b64)
|
||||||
|
return AESGCM(key).decrypt(nonce, ct, None).decode("utf-8")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JWT Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def issue_jwt(user_id: int, username: str) -> str:
|
||||||
|
"""Issue a signed JWT with user_id and username claims."""
|
||||||
|
payload = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"username": username,
|
||||||
|
"exp": datetime.now(timezone.utc) + timedelta(days=JWT_EXPIRY_DAYS),
|
||||||
|
}
|
||||||
|
return pyjwt.encode(payload, JWT_SECRET, algorithm="HS256")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_jwt(token: str):
|
||||||
|
"""Decode and verify a JWT. Returns payload dict or None."""
|
||||||
|
try:
|
||||||
|
return pyjwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
||||||
|
except pyjwt.PyJWTError:
|
||||||
|
return None
|
||||||
59
routes.py
59
routes.py
|
|
@ -11,19 +11,19 @@ POST /api/payment/success – Validate webhook secret, unlock AI, push socke
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import base64
|
|
||||||
import hmac
|
import hmac
|
||||||
import random
|
import random
|
||||||
import functools
|
import functools
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import jwt as pyjwt
|
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
||||||
from flask import Blueprint, g, jsonify, request
|
from flask import Blueprint, g, jsonify, request
|
||||||
|
|
||||||
from database import db
|
from database import db
|
||||||
from models import User, Message
|
from models import User, Message
|
||||||
|
from config import (
|
||||||
|
AI_FREE_LIMIT, AI_BOT_NAME, PAYMENT_SECRET, MAX_HISTORY,
|
||||||
|
aesgcm_encrypt, aesgcm_decrypt, issue_jwt, verify_jwt,
|
||||||
|
)
|
||||||
|
|
||||||
api = Blueprint("api", __name__, url_prefix="/api")
|
api = Blueprint("api", __name__, url_prefix="/api")
|
||||||
|
|
||||||
|
|
@ -31,13 +31,6 @@ api = Blueprint("api", __name__, url_prefix="/api")
|
||||||
# Config
|
# Config
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
JWT_SECRET = os.environ.get("JWT_SECRET", "change-me-jwt-secret")
|
|
||||||
JWT_EXPIRY_DAYS = 7
|
|
||||||
AI_FREE_LIMIT = 3
|
|
||||||
PAYMENT_SECRET = os.environ.get("PAYMENT_SECRET", "change-me-payment-secret")
|
|
||||||
MAX_HISTORY = 500
|
|
||||||
AI_BOT_NAME = "Violet"
|
|
||||||
|
|
||||||
AI_RESPONSES = [
|
AI_RESPONSES = [
|
||||||
"Mmm, you have my full attention 💋",
|
"Mmm, you have my full attention 💋",
|
||||||
"Oh my... keep going 😈 Don't stop there.",
|
"Oh my... keep going 😈 Don't stop there.",
|
||||||
|
|
@ -60,22 +53,6 @@ AI_RESPONSES = [
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _issue_jwt(user_id: int, username: str) -> str:
|
|
||||||
payload = {
|
|
||||||
"user_id": user_id,
|
|
||||||
"username": username,
|
|
||||||
"exp": datetime.utcnow() + timedelta(days=JWT_EXPIRY_DAYS),
|
|
||||||
}
|
|
||||||
return pyjwt.encode(payload, JWT_SECRET, algorithm="HS256")
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_jwt(token: str):
|
|
||||||
try:
|
|
||||||
return pyjwt.decode(token, JWT_SECRET, algorithms=["HS256"])
|
|
||||||
except pyjwt.PyJWTError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _require_auth(f):
|
def _require_auth(f):
|
||||||
"""Decorator – parse Bearer JWT and populate g.current_user."""
|
"""Decorator – parse Bearer JWT and populate g.current_user."""
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
|
|
@ -83,7 +60,7 @@ def _require_auth(f):
|
||||||
auth_header = request.headers.get("Authorization", "")
|
auth_header = request.headers.get("Authorization", "")
|
||||||
if not auth_header.startswith("Bearer "):
|
if not auth_header.startswith("Bearer "):
|
||||||
return jsonify({"error": "Unauthorized"}), 401
|
return jsonify({"error": "Unauthorized"}), 401
|
||||||
payload = _verify_jwt(auth_header[7:])
|
payload = verify_jwt(auth_header[7:])
|
||||||
if not payload:
|
if not payload:
|
||||||
return jsonify({"error": "Invalid or expired token"}), 401
|
return jsonify({"error": "Invalid or expired token"}), 401
|
||||||
user = db.session.get(User, payload["user_id"])
|
user = db.session.get(User, payload["user_id"])
|
||||||
|
|
@ -94,22 +71,6 @@ def _require_auth(f):
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def _aesgcm_encrypt(key_b64: str, plaintext: str) -> tuple:
|
|
||||||
"""Encrypt plaintext with AES-GCM. Returns (ciphertext_b64, nonce_b64)."""
|
|
||||||
key_bytes = base64.b64decode(key_b64)
|
|
||||||
nonce = os.urandom(12)
|
|
||||||
ct = AESGCM(key_bytes).encrypt(nonce, plaintext.encode("utf-8"), None)
|
|
||||||
return base64.b64encode(ct).decode(), base64.b64encode(nonce).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def _aesgcm_decrypt(key_b64: str, ciphertext_b64: str, nonce_b64: str) -> str:
|
|
||||||
"""Decrypt AES-GCM ciphertext. Raises on authentication failure."""
|
|
||||||
key_bytes = base64.b64decode(key_b64)
|
|
||||||
ct = base64.b64decode(ciphertext_b64)
|
|
||||||
nonce = base64.b64decode(nonce_b64)
|
|
||||||
return AESGCM(key_bytes).decrypt(nonce, ct, None).decode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def _persist_message(sender_id: int, recipient_id: int,
|
def _persist_message(sender_id: int, recipient_id: int,
|
||||||
encrypted_content: str, nonce: str) -> None:
|
encrypted_content: str, nonce: str) -> None:
|
||||||
"""Save a PM to the database. Enforces MAX_HISTORY per conversation pair."""
|
"""Save a PM to the database. Enforces MAX_HISTORY per conversation pair."""
|
||||||
|
|
@ -188,7 +149,7 @@ def register():
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
token = _issue_jwt(user.id, user.username)
|
token = issue_jwt(user.id, user.username)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"token": token,
|
"token": token,
|
||||||
"user": {
|
"user": {
|
||||||
|
|
@ -212,7 +173,7 @@ def login():
|
||||||
if not user or not bcrypt.checkpw(password.encode(), user.password_hash.encode()):
|
if not user or not bcrypt.checkpw(password.encode(), user.password_hash.encode()):
|
||||||
return jsonify({"error": "Invalid username or password."}), 401
|
return jsonify({"error": "Invalid username or password."}), 401
|
||||||
|
|
||||||
token = _issue_jwt(user.id, user.username)
|
token = issue_jwt(user.id, user.username)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"token": token,
|
"token": token,
|
||||||
"user": {
|
"user": {
|
||||||
|
|
@ -296,7 +257,7 @@ def ai_message():
|
||||||
|
|
||||||
# ── Transit decrypt (message readable for AI; key NOT stored) ─────────────
|
# ── Transit decrypt (message readable for AI; key NOT stored) ─────────────
|
||||||
try:
|
try:
|
||||||
_plaintext = _aesgcm_decrypt(transit_key, ciphertext, nonce_b64)
|
_plaintext = aesgcm_decrypt(transit_key, ciphertext, nonce_b64)
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"error": "Decryption failed – wrong key or corrupted data"}), 400
|
return jsonify({"error": "Decryption failed – wrong key or corrupted data"}), 400
|
||||||
|
|
||||||
|
|
@ -306,7 +267,7 @@ def ai_message():
|
||||||
# ── Persist both legs encrypted in DB (server uses transit key) ──────────
|
# ── Persist both legs encrypted in DB (server uses transit key) ──────────
|
||||||
bot = _get_ai_bot()
|
bot = _get_ai_bot()
|
||||||
_persist_message(user.id, bot.id, ciphertext, nonce_b64) # user → AI
|
_persist_message(user.id, bot.id, ciphertext, nonce_b64) # user → AI
|
||||||
resp_ct, resp_nonce = _aesgcm_encrypt(transit_key, ai_text)
|
resp_ct, resp_nonce = aesgcm_encrypt(transit_key, ai_text)
|
||||||
_persist_message(bot.id, user.id, resp_ct, resp_nonce) # AI → user
|
_persist_message(bot.id, user.id, resp_ct, resp_nonce) # AI → user
|
||||||
|
|
||||||
# ── Update free trial counter ─────────────────────────────────────────────
|
# ── Update free trial counter ─────────────────────────────────────────────
|
||||||
|
|
@ -315,7 +276,7 @@ def ai_message():
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# ── Re-encrypt AI response for transit back ───────────────────────────────
|
# ── Re-encrypt AI response for transit back ───────────────────────────────
|
||||||
resp_ct_transit, resp_nonce_transit = _aesgcm_encrypt(transit_key, ai_text)
|
resp_ct_transit, resp_nonce_transit = aesgcm_encrypt(transit_key, ai_text)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"ciphertext": resp_ct_transit,
|
"ciphertext": resp_ct_transit,
|
||||||
|
|
|
||||||
22
start.py
22
start.py
|
|
@ -12,7 +12,6 @@ Usage:
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
|
|
@ -21,24 +20,11 @@ import eventlet
|
||||||
# Monkey-patch stdlib BEFORE any other import
|
# Monkey-patch stdlib BEFORE any other import
|
||||||
eventlet.monkey_patch()
|
eventlet.monkey_patch()
|
||||||
|
|
||||||
|
from config import get_conf
|
||||||
|
|
||||||
# PID file to track the daemon process
|
# PID file to track the daemon process
|
||||||
PID_FILE = "sexchat.pid"
|
PID_FILE = "sexchat.pid"
|
||||||
|
|
||||||
def load_config():
|
|
||||||
conf = {}
|
|
||||||
config_path = os.path.join(os.path.dirname(__file__), "config.json")
|
|
||||||
if os.path.exists(config_path):
|
|
||||||
try:
|
|
||||||
with open(config_path, "r") as f:
|
|
||||||
conf = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return conf
|
|
||||||
|
|
||||||
def _get_conf(key, default=None):
|
|
||||||
conf = load_config()
|
|
||||||
return os.environ.get(key, conf.get(key, default))
|
|
||||||
|
|
||||||
def get_pid():
|
def get_pid():
|
||||||
if os.path.exists(PID_FILE):
|
if os.path.exists(PID_FILE):
|
||||||
with open(PID_FILE, "r") as f:
|
with open(PID_FILE, "r") as f:
|
||||||
|
|
@ -68,7 +54,7 @@ def start_daemon():
|
||||||
"gunicorn",
|
"gunicorn",
|
||||||
"--worker-class", "eventlet",
|
"--worker-class", "eventlet",
|
||||||
"-w", "1",
|
"-w", "1",
|
||||||
"--bind", f"{_get_conf('HOST', '0.0.0.0')}:{_get_conf('PORT', 5000)}",
|
"--bind", f"{get_conf('HOST', '0.0.0.0')}:{get_conf('PORT', 5000)}",
|
||||||
"--daemon",
|
"--daemon",
|
||||||
"--pid", PID_FILE,
|
"--pid", PID_FILE,
|
||||||
"--access-logfile", "access.log",
|
"--access-logfile", "access.log",
|
||||||
|
|
@ -124,7 +110,7 @@ def run_debug():
|
||||||
"gunicorn",
|
"gunicorn",
|
||||||
"--worker-class", "eventlet",
|
"--worker-class", "eventlet",
|
||||||
"-w", "1",
|
"-w", "1",
|
||||||
"--bind", f"{_get_conf('HOST', '0.0.0.0')}:{_get_conf('PORT', 5000)}",
|
"--bind", f"{get_conf('HOST', '0.0.0.0')}:{get_conf('PORT', 5000)}",
|
||||||
"--log-level", "debug",
|
"--log-level", "debug",
|
||||||
"--access-logfile", "-",
|
"--access-logfile", "-",
|
||||||
"--error-logfile", "-",
|
"--error-logfile", "-",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue