From e86d69ce35e6e8ac546fcc2c0c7a12a1f24a9056 Mon Sep 17 00:00:00 2001 From: 3nd3r Date: Sun, 12 Apr 2026 15:08:20 -0500 Subject: [PATCH] Rewrite README from scratch with comprehensive project documentation - Complete feature inventory (lobby, PMs, Violet AI, moderation, roles, themes, settings) - Full architecture diagram and tech stack table - All 6 database models with column definitions - Complete socket events reference (connection, lobby, PM, Violet, moderation, admin panel) - REST API endpoints with request/response examples - Role system hierarchy and admin panel documentation - Encryption design (PM encryption + Violet transit encryption) - Security measures overview - All 10 themes listed - Settings panel (4 tabs) documentation - Violet AI companion behavior, commands, guest handling, fallback - Premium/paywall flow - Dependencies table - Configuration reference with defaults - Setup and installation instructions --- README.md | 967 +++++++++++++++++++++++++++++------------------------- 1 file changed, 528 insertions(+), 439 deletions(-) diff --git a/README.md b/README.md index 2f74858..cee668f 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,122 @@ -# Aphrodite — SexyChat +# šŸ’‹ SexyChat (Aphrodite) -A real-time encrypted chat application built with Flask-SocketIO, featuring a public lobby, end-to-end encrypted private messaging, an AI companion ("Violet") powered by Ollama, moderation tools, and a payment-gated premium tier. +A real-time encrypted chat platform with an AI companion, role-based moderation, and a glassmorphism UI. Built with Flask-SocketIO, AES-GCM encryption, and a local Ollama-powered AI named **Violet**. --- ## Table of Contents - [Features](#features) -- [Architecture Overview](#architecture-overview) -- [Tech Stack](#tech-stack) +- [Architecture](#architecture) - [Project Structure](#project-structure) +- [Setup & Installation](#setup--installation) - [Configuration](#configuration) -- [Installation & Setup](#installation--setup) - [Running the Server](#running-the-server) -- [Socket Protocol](#socket-protocol) +- [Database Models](#database-models) +- [Socket Events Reference](#socket-events-reference) - [REST API Endpoints](#rest-api-endpoints) -- [Encryption Design](#encryption-design) -- [Moderation System](#moderation-system) +- [Role System & Admin Panel](#role-system--admin-panel) +- [Encryption & Security](#encryption--security) +- [Theme System](#theme-system) +- [Settings Panel](#settings-panel) - [Violet AI Companion](#violet-ai-companion) -- [Payment & Premium Access](#payment--premium-access) -- [Database Schema](#database-schema) -- [Known Security Issues & Bugs](#known-security-issues--bugs) +- [Premium / Paywall](#premium--paywall) --- ## Features -- **Public Lobby** — Ephemeral group chat room. Messages are never persisted; they live only in connected clients' browsers. -- **User Registration & Login** — Accounts with bcrypt-hashed passwords, JWT session tokens, and moderator-gated verification. -- **End-to-End Encrypted Private Messages** — AES-GCM-256 encryption using PBKDF2-derived keys. The server stores only ciphertext and nonces. -- **Violet AI Companion** — A flirtatious AI hostess backed by a local Ollama instance. Messages are transit-encrypted (decrypted server-side for inference, re-encrypted before storage and delivery). -- **Freemium AI Access** — Free users get a configurable number of AI messages before hitting a paywall. Paying users get unlimited access. -- **Moderation Tools** — Kick, ban, kickban, mute/unmute, and manual account verification. Moderator access is granted via a shared admin password at login. -- **Ignore System** — Registered users can ignore/unignore other users. Ignored users' messages are hidden client-side, and PM invitations from ignored users are silently blocked. -- **Responsive UI** — Glassmorphism design with a deep purple/neon magenta theme. Mobile-friendly with a collapsible nicklist sidebar. -- **Process Manager** — `start.py` wraps Gunicorn with daemon management (start, stop, restart, status, debug). +### Core Chat +- **Public Lobby** — Real-time ephemeral group chat. Messages are never persisted; they live only in connected clients' browsers. +- **Private Messaging** — AES-GCM-256 encrypted PMs with persistent history (up to 500 messages per conversation). +- **Guest Mode** — Join and chat anonymously without an account. +- **Registration & Login** — Bcrypt password hashing, JWT token auth (7-day expiry), automatic session restore on page refresh. +- **Rate Limiting** — 6 messages per 5-second window per session. + +### Violet AI Companion +- Local Ollama inference (`sadiq-bd/llama3.2-3b-uncensored:latest` by default). +- Transit-encrypted messages — decrypted server-side only for inference, never stored in plaintext. +- Per-user conversation memory (last 20 turns), persisted across sessions. `/reset` command to clear. +- Free trial (configurable, default: 3 messages), then paywall — admins can grant unlimited access. +- Real-time typing indicator while Violet is generating a response. +- Friendly signup prompt for unregistered guests who try to PM Violet. + +### Moderation & Roles +- **4-tier role hierarchy:** `root` (šŸ‘‘) > `admin` (āš”ļø) > `mod` (šŸ›”ļø) > `user` +- Kick, ban (username + IP), kickban, mute/unmute, verify/unverify. +- Persistent bans and mutes survive server restarts (stored in DB, loaded on startup). +- Admin panel with three tabs: Users, Bans, Mutes. +- Privilege escalation prevention — you can't modify users at or above your own power level. + +### User Features +- Persistent ignore list (stealth blocking — ignored users receive no indication). +- Context menu (right-click a username) for PM, ignore, and mod actions. +- Settings panel with four tabs: Account, Chat, Violet, Premium. +- Password change from settings (requires current password). +- Unread PM indicators on conversation tabs. + +### UI/UX +- Glassmorphism dark theme with CSS custom property theming. +- **10 color themes** (dark, light, neon, cyberpunk, and more). +- Responsive mobile layout with collapsible sidebar. +- Auto-expanding message textarea. +- Enter-to-send toggle (configurable). +- Google Fonts (Inter + Outfit). --- -## Architecture Overview +## Architecture ``` -ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” WebSocket / HTTP ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” -│ Browser │ ◄──────────────────────────────►│ Flask-SocketIO │ -│ (chat.js) │ │ (app.py) │ -│ (crypto.js) │ │ │ -ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ - │ │ REST Blueprint │ │ - │ │ (routes.py) │ │ - │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ - │ │ │ - │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ - │ │ SQLAlchemy │ │ - │ │ (database.py) │ │ - │ │ (models.py) │ │ - │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ - │ │ │ - │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ - │ │ SQLite / PG │ │ - │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ - │ │ - │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ - │ │ Ollama (AI) │ │ - │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ - ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” WebSocket / HTTP ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ Browser │ ◄────────────────────► │ Gunicorn + Eventlet │ +│ (chat.js │ │ Flask-SocketIO │ +│ crypto.js) │ │ │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ + │ │ REST Blueprint │ │ + │ │ (routes.py) │ │ + │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ + │ │ + │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ + │ │ AI Worker (greenlet) │ │ + │ │ Serialized Ollama queue │ │ + │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ Ollama (localhost:11434) │ + │ LLaMA 3.2 3B Uncensored │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ SQLite (instance/sexchat.db)│ + │ Users, Messages, Bans, │ + │ Mutes, VioletHistory │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ``` -**Key design decisions:** +### Key Design Decisions -- **Single-process, single-worker** — Eventlet async mode with one Gunicorn worker. All in-memory state (connected users, bans, mutes) lives in the process. -- **AI inference queue** — A dedicated eventlet greenlet serialises Ollama requests one at a time, broadcasting `violet_typing` indicators while busy. +- **Single-process, single-worker** — Eventlet async mode with one Gunicorn worker. All in-memory state (connected users, rooms, rate limits) lives in-process. +- **AI inference queue** — A dedicated eventlet greenlet serialises Ollama requests one at a time, broadcasting `violet_typing` indicators while busy. Runs within Flask app context for DB access. - **Lobby is ephemeral** — No lobby messages are ever written to the database. -- **PMs are persisted for registered users** — Encrypted messages are stored in the `messages` table. History is capped at 500 messages per conversation pair. +- **PMs are encrypted at rest** — The server stores only AES-GCM ciphertext and nonces for registered-user PMs. +- **Shared config module** — `config.py` centralises all constants, config loading, AES-GCM helpers, and JWT helpers so `app.py` and `routes.py` share a single source of truth. ---- - -## Tech Stack +### Tech Stack | Layer | Technology | |-------|-----------| -| Backend framework | Flask 3.x | -| Real-time transport | Flask-SocketIO 5.x (eventlet async) | -| WSGI server | Gunicorn 21.x with eventlet worker | -| Database ORM | Flask-SQLAlchemy 3.x | -| Migrations | Flask-Migrate 4.x (Alembic) | -| Database | SQLite (default) or PostgreSQL (via `DATABASE_URL`) | -| Password hashing | bcrypt 4.x | -| JWT auth | PyJWT 2.x | -| Server-side crypto | cryptography 42.x (AES-GCM) | -| Client-side crypto | Web Crypto API (SubtleCrypto) | -| AI inference | Ollama (local, HTTP API) | +| Web server | Gunicorn 21.x with eventlet worker | +| Framework | Flask 3.x + Flask-SocketIO 5.x | +| Concurrency | Eventlet (monkey-patched greenlets) | +| Database | SQLite (default) or PostgreSQL via `DATABASE_URL` | +| ORM | SQLAlchemy 3.x + Flask-Migrate 4.x (Alembic) | +| AI | Ollama HTTP API (local inference) | +| Auth | JWT (PyJWT 2.x) + bcrypt 4.x | +| Encryption | AES-GCM-256 (server: `cryptography` 42.x, client: Web Crypto API) | | Frontend | Vanilla JS, Socket.IO 4 client, custom CSS | +| Scaling | Optional Redis message queue for multi-worker deployments | --- @@ -98,108 +124,129 @@ A real-time encrypted chat application built with Flask-SocketIO, featuring a pu ``` aprhodite/ -ā”œā”€ā”€ app.py # Flask-SocketIO app factory, socket event handlers, AI queue -ā”œā”€ā”€ database.py # SQLAlchemy & Flask-Migrate initialisation, DB seeding -ā”œā”€ā”€ models.py # ORM models: User, Message, UserIgnore -ā”œā”€ā”€ routes.py # REST API blueprint (auth, PM history, AI message, payment) -ā”œā”€ā”€ start.py # Gunicorn process manager (start/stop/restart/status/debug) -ā”œā”€ā”€ index.html # Single-page frontend (join screen, chat, modals) +ā”œā”€ā”€ app.py # Flask-SocketIO app: socket events, AI worker, admin panel, moderation +ā”œā”€ā”€ config.py # Shared config loader, constants, AES-GCM helpers, JWT helpers, Violet prompt +ā”œā”€ā”€ database.py # SQLAlchemy + Flask-Migrate init, Violet bot user seeding +ā”œā”€ā”€ models.py # ORM models: User, Message, Ban, Mute, UserIgnore, VioletHistory +ā”œā”€ā”€ routes.py # REST API blueprint: auth, PM history, AI message, payment webhook +ā”œā”€ā”€ start.py # Process manager: start / stop / restart / status / debug +ā”œā”€ā”€ gunicorn.conf.py # Gunicorn configuration (workers, bind, worker class) +ā”œā”€ā”€ config.json # Runtime secrets & overrides (gitignored) ā”œā”€ā”€ requirements.txt # Python dependencies -ā”œā”€ā”€ config.json # Optional config file (alternative to env vars) -└── static/ - ā”œā”€ā”€ chat.js # Frontend logic (socket events, PM/AI flows, UI) - ā”œā”€ā”€ crypto.js # AES-GCM-256 encryption wrapper (PBKDF2 key derivation) - ā”œā”€ā”€ socket.io.min.js # Socket.IO v4 client library - └── style.css # Glassmorphism theme (deep purple / neon magenta) +ā”œā”€ā”€ index.html # Single-page app: join screen, chat, settings modal, admin panel modal +ā”œā”€ā”€ static/ +│ ā”œā”€ā”€ chat.js # Frontend logic: socket events, PMs, settings, admin panel, themes +│ ā”œā”€ā”€ crypto.js # Client-side AES-GCM encryption (PBKDF2 key derivation, SubtleCrypto) +│ ā”œā”€ā”€ style.css # All styles: glassmorphism, 10 themes, admin panel, responsive layout +│ └── socket.io.min.js# Socket.IO v4 client library +└── instance/ + └── sexchat.db # SQLite database (auto-created on first run) ``` --- -## Configuration - -Configuration is loaded with the following precedence: **Environment Variable → `config.json` → Default Value**. - -| Key | Default | Description | -|-----|---------|-------------| -| `SECRET_KEY` | Random UUID hex | Flask session secret | -| `JWT_SECRET` | Random UUID hex | HMAC signing key for JWTs | -| `ADMIN_PASSWORD` | `admin1234` | Shared moderator password | -| `DATABASE_URL` | `sqlite:///sexchat.db` | SQLAlchemy database URI | -| `AI_FREE_LIMIT` | `3` | Free AI messages before paywall | -| `OLLAMA_URL` | `http://localhost:11434` | Ollama API base URL | -| `VIOLET_MODEL` | `sam860/dolphin3-llama3.2:3b` | Ollama model tag for Violet | -| `PAYMENT_SECRET` | `change-me-payment-secret` | Webhook validation secret | -| `HOST` | `0.0.0.0` | Bind address | -| `PORT` | `5000` | Bind port | -| `SOCKETIO_MESSAGE_QUEUE` | `None` | Redis URL for multi-process Socket.IO | -| `REDIS_URL` | `None` | Fallback for `SOCKETIO_MESSAGE_QUEUE` | - -Create a `config.json` in the project root: - -```json -{ - "SECRET_KEY": "your-secret-key", - "JWT_SECRET": "your-jwt-secret", - "ADMIN_PASSWORD": "a-strong-moderator-password", - "DATABASE_URL": "postgresql://user:pass@localhost/sexchat", - "PAYMENT_SECRET": "your-stripe-webhook-secret", - "OLLAMA_URL": "http://localhost:11434", - "VIOLET_MODEL": "sam860/dolphin3-llama3.2:3b", - "AI_FREE_LIMIT": 3 -} -``` - ---- - -## Installation & Setup +## Setup & Installation ### Prerequisites -- Python 3.10+ -- pip -- Ollama (optional — required only for live AI responses) +- **Python 3.11+** +- **[Ollama](https://ollama.ai)** (required for Violet AI) ### Install ```bash git clone https://git.computertech.dev/lord3nd3r/aprhodite.git cd aprhodite + python -m venv venv source venv/bin/activate pip install -r requirements.txt ``` -### Database - -On first run, `database.py` calls `db.create_all()` and seeds the Violet bot user. No manual migration is needed for a fresh start. For schema changes: +### Pull the AI Model ```bash -flask db migrate -m "description" +ollama pull sadiq-bd/llama3.2-3b-uncensored:latest +``` + +### Create `config.json` + +```json +{ + "SECRET_KEY": "your-random-secret-key", + "JWT_SECRET": "your-random-jwt-secret", + "ADMIN_PASSWORD": "a-strong-moderator-password", + "AI_FREE_LIMIT": 3, + "HOST": "0.0.0.0", + "PORT": 5000 +} +``` + +> **Tip:** Generate secrets with `python -c "import uuid; print(uuid.uuid4().hex)"`. + +### Database + +On first run, `database.py` calls `db.create_all()` and seeds the **Violet** bot user automatically. No manual migration is needed for a fresh install. + +For schema changes after the initial setup: + +```bash +flask db migrate -m "description of changes" flask db upgrade ``` -### Ollama (for live AI) +--- -```bash -# Install Ollama (https://ollama.com) -ollama pull sam860/dolphin3-llama3.2:3b -ollama serve # default :11434 -``` +## Configuration -If Ollama is unavailable, `routes.py` falls back to canned responses; `app.py` returns a placeholder message. +Configuration is resolved with the following precedence: **Environment Variable → `config.json` → Default Value**. + +All config loading is centralised in `config.py`. + +| Key | Default | Description | +|-----|---------|-------------| +| `SECRET_KEY` | Random UUID hex | Flask session secret | +| `JWT_SECRET` | Random UUID hex | JWT HMAC signing key | +| `ADMIN_PASSWORD` | `"admin1234"` | Shared mod password for guest elevation | +| `DATABASE_URL` | `"sqlite:///sexchat.db"` | SQLAlchemy database URI | +| `PAYMENT_SECRET` | `"change-me-payment-secret"` | Payment webhook validation secret | +| `CORS_ORIGINS` | `None` | Socket.IO CORS allowlist | +| `HOST` | `"0.0.0.0"` | Server bind address | +| `PORT` | `5000` | Server bind port | +| `OLLAMA_URL` | `"http://localhost:11434"` | Ollama API endpoint | +| `VIOLET_MODEL` | `"sadiq-bd/llama3.2-3b-uncensored:latest"` | Ollama model tag for Violet | +| `AI_FREE_LIMIT` | `3` | Free Violet messages before paywall | +| `SOCKETIO_MESSAGE_QUEUE` / `REDIS_URL` | `None` | Redis URL for multi-worker Socket.IO | + +### Hardcoded Constants (in `config.py`) + +| Constant | Value | Description | +|----------|-------|-------------| +| `MAX_MSG_LEN` | 500 chars | Maximum message length | +| `MAX_HISTORY` | 500 messages | PM history cap per conversation pair | +| `JWT_EXPIRY_DAYS` | 7 days | JWT token lifespan | +| `AI_BOT_NAME` | `"Violet"` | AI bot display name | +| `PBKDF2_ITERATIONS` | 100,000 | Client-side key derivation iterations (`crypto.js`) | +| Rate limit | 6 msgs / 5 sec | Per-session lobby rate limit (`app.py`) | +| `MAX_HISTORY_PER_USER` | 20 turns | Violet conversation memory depth (`app.py`) | --- ## Running the Server -### Process Manager (recommended for production) +### Debug Mode (foreground, full logging) ```bash -python start.py start # Start as background daemon +python start.py debug +``` + +### Daemon Mode + +```bash +python start.py start # Start in background (PID written to .pid file) python start.py stop # Graceful shutdown python start.py restart # Stop + start python start.py status # Check if running -python start.py debug # Foreground with debug logging ``` ### Direct Gunicorn @@ -208,53 +255,158 @@ python start.py debug # Foreground with debug logging gunicorn --worker-class eventlet -w 1 --bind 0.0.0.0:5000 start:application ``` -### Development (Flask built-in server) - -```python -from app import create_app, socketio -app = create_app() -socketio.run(app, host="0.0.0.0", port=5000, debug=True) -``` +The server will be available at `http://localhost:5000`. --- -## Socket Protocol +## Database Models -### Client → Server Events +Six models defined in `models.py`. All timestamps use `datetime.now(timezone.utc)`. -| Event | Payload | Description | -|-------|---------|-------------| -| `join` | `{ mode, username, password?, email?, mod_password? }` | Authenticate and enter the lobby. `mode` is `guest`, `login`, `register`, or `restore`. | -| `message` | `{ text }` | Send a message to the lobby. | -| `pm_open` | `{ target }` | Request a private message room with `target` user. | -| `pm_accept` | `{ room }` | Accept an incoming PM invitation. | -| `pm_message` | `{ room, text? }` or `{ room, ciphertext, nonce, transit_key? }` | Send a PM. Include `transit_key` when messaging Violet. | -| `ai_message` | `{ ciphertext, nonce, transit_key }` | *Deprecated.* Use `pm_message` to Violet instead. | -| `mod_kick` | `{ target }` | Kick a user (admin only). | -| `mod_ban` | `{ target }` | Ban a user by username + IP (admin only). | -| `mod_kickban` | `{ target }` | Kick and ban simultaneously (admin only). | -| `mod_mute` | `{ target }` | Toggle mute on a user (admin only). | -| `mod_verify` | `{ target }` | Verify a registered user's account (admin only). | -| `user_ignore` | `{ target }` | Add user to ignore list (registered only). | -| `user_unignore` | `{ target }` | Remove user from ignore list (registered only). | +### `users` -### Server → Client Events +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer, PK | Auto-increment | +| `username` | String(20), unique, indexed | Alphanumeric + `-` + `_` | +| `password_hash` | String(128) | Bcrypt hash | +| `email` | String(255), unique, nullable | Optional email | +| `role` | String(10), default `"user"` | `root`, `admin`, `mod`, or `user` | +| `has_ai_access` | Boolean, default `False` | Unlimited Violet access flag | +| `ai_messages_used` | Integer, default `0` | Free trial counter | +| `is_verified` | Boolean, default `False` | Manual verification status | +| `created_at` | DateTime | UTC timestamp | -| Event | Payload | Description | -|-------|---------|-------------| -| `joined` | `{ username, is_admin, is_registered, has_ai_access, ai_messages_used, token?, ignored_list? }` | Successful join confirmation. | -| `nicklist` | `{ users: [{ username, is_admin, is_registered, is_verified, is_ai? }] }` | Updated user list. | -| `message` | `{ username, text, is_admin, is_registered, ts }` | Lobby message. | -| `system` | `{ msg, ts }` | System announcement (joins, parts, mod actions). | -| `error` | `{ msg }` | Error message. | -| `kicked` | `{ msg }` | Notification that the user has been kicked/banned. | -| `pm_invite` | `{ from, room }` | Incoming PM invitation. | -| `pm_ready` | `{ with, room }` | PM room is ready (sent to initiator). | -| `pm_message` | `{ from, text?, ciphertext?, nonce?, room, ts }` | Private message. | -| `violet_typing` | `{ busy: bool, room? }` | Violet AI typing indicator. | -| `ai_response` | `{ ciphertext, nonce, ai_messages_used, has_ai_access }` or `{ error }` | AI response (legacy event). | -| `ai_unlock` | `{ msg }` | AI access unlocked after payment. | -| `ignore_status` | `{ target, ignored: bool }` | Confirmation of ignore/unignore action. | +### `messages` (Encrypted PM History) + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer, PK | | +| `sender_id` | FK → `users.id` | | +| `recipient_id` | FK → `users.id` | | +| `encrypted_content` | Text | Base64-encoded AES-GCM ciphertext | +| `nonce` | String(64) | Base64-encoded 12-byte IV | +| `timestamp` | DateTime | Indexed with sender/recipient for query performance | + +### `bans` + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer, PK | | +| `username` | String(20), indexed | Banned username (lowercase) | +| `ip` | String(45), nullable, indexed | Banned IP address | +| `reason` | String(255), nullable | Ban reason | +| `created_at` | DateTime | | + +### `mutes` + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer, PK | | +| `username` | String(20), unique, indexed | Muted username | +| `created_at` | DateTime | | + +### `user_ignores` + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer, PK | | +| `ignorer_id` | FK → `users.id` | User doing the ignoring | +| `ignored_id` | FK → `users.id` | User being ignored | +| `created_at` | DateTime | | + +Unique composite index on `(ignorer_id, ignored_id)`. + +### `violet_history` + +| Column | Type | Description | +|--------|------|-------------| +| `id` | Integer, PK | | +| `user_id` | FK → `users.id` | Owning user | +| `role` | String(10) | `"user"` or `"assistant"` | +| `text` | Text | Plaintext conversation turn | +| `timestamp` | DateTime | | + +--- + +## Socket Events Reference + +### Connection & Auth + +| Direction | Event | Data | Description | +|-----------|-------|------|-------------| +| C→S | `join` | `{ mode, username, password?, email?, mod_password? }` | Authenticate and enter lobby. `mode`: `guest`, `login`, `register`, or `restore`. | +| S→C | `joined` | `{ username, is_admin, role, is_registered, has_ai_access, ai_messages_used, email?, token?, ignored_list? }` | Auth success — includes JWT token for session restore. | +| S→C | `error` | `{ msg }` | Error message. | +| S→C | `kicked` | `{ msg }` | User was kicked/banned. | + +### Lobby Chat + +| Direction | Event | Data | Description | +|-----------|-------|------|-------------| +| C→S | `message` | `{ text }` | Send lobby message (rate-limited). | +| S→C | `message` | `{ username, text, is_admin, is_registered, ts }` | Broadcast lobby message. | +| S→C | `system` | `{ msg, ts }` | System notification (joins, parts, mod actions). | +| S→C | `nicklist` | `{ users: [{ username, is_admin, is_registered, is_verified, role }] }` | Full online user list (includes Violet bot). | + +### Private Messaging + +| Direction | Event | Data | Description | +|-----------|-------|------|-------------| +| C→S | `pm_open` | `{ target }` | Initiate PM with target user. | +| C→S | `pm_accept` | `{ room }` | Accept an incoming PM invitation (validated against pending invites). | +| C→S | `pm_message` | `{ room, text? }` or `{ room, ciphertext, nonce, transit_key? }` | Send PM — plaintext (guests) or encrypted (registered). Include `transit_key` for Violet. | +| S→C | `pm_invite` | `{ from, room, room_key }` | Incoming PM invitation with server-derived room key. | +| S→C | `pm_ready` | `{ with, room, room_key }` | PM room opened — sent to the initiator. | +| S→C | `pm_message` | `{ from, text?, ciphertext?, nonce?, room, ts }` | Receive a PM. | + +### Violet AI + +| Direction | Event | Data | Description | +|-----------|-------|------|-------------| +| C→S | `pm_message` (to Violet room) | `{ room, ciphertext, nonce, transit_key }` | Encrypted message routed to Violet via AI worker. | +| C→S | `violet_reset` | _(none)_ | Clear your conversation memory with Violet. | +| S→C | `violet_typing` | `{ busy, room? }` | AI processing indicator (shown while Ollama generates). | +| S→C | `ai_response` | `{ error: "ai_limit_reached", room }` | Free trial exhausted. | +| S→C | `ai_unlock` | `{ has_ai_access, msg? }` | Premium access granted or revoked (pushed live). | + +### Moderation + +| Direction | Event | Data | Description | +|-----------|-------|------|-------------| +| C→S | `mod_kick` | `{ target }` | Kick user (mod+). | +| C→S | `mod_ban` | `{ target }` | Ban username + IP (mod+). | +| C→S | `mod_kickban` | `{ target }` | Kick + ban simultaneously (mod+). | +| C→S | `mod_mute` | `{ target }` | Toggle mute on user (mod+). | +| C→S | `mod_verify` | `{ target }` | Toggle verification on user (mod+). | + +### User Actions + +| Direction | Event | Data | Description | +|-----------|-------|------|-------------| +| C→S | `user_ignore` | `{ target }` | Ignore a user (registered only). | +| C→S | `user_unignore` | `{ target }` | Unignore a user. | +| C→S | `change_password` | `{ old_password, new_password }` | Change password (6-char minimum). | +| S→C | `ignore_status` | `{ target, ignored }` | Ignore list update confirmation. | +| S→C | `password_changed` | `{ success, msg? }` | Password change result. | +| S→C | `role_updated` | `{ role }` | Live notification when your role is changed by an admin. | + +### Admin Panel + +| Direction | Event | Data | Description | +|-----------|-------|------|-------------| +| C→S | `admin_get_users` | _(none)_ | Request full user list (mod+). | +| C→S | `admin_get_bans` | _(none)_ | Request ban list (mod+). | +| C→S | `admin_get_mutes` | _(none)_ | Request mute list (mod+). | +| C→S | `admin_set_role` | `{ user_id, role }` | Change a user's role (admin+ only, can't set ≄ own role). | +| C→S | `admin_verify_user` | `{ user_id }` | Toggle verification (mod+). | +| C→S | `admin_toggle_ai` | `{ user_id }` | Toggle unlimited AI access (admin+). Notifies user in real-time. | +| C→S | `admin_unban` | `{ ban_id }` | Remove a ban (mod+). | +| C→S | `admin_unmute` | `{ mute_id }` | Remove a mute (mod+). | +| S→C | `admin_users` | `{ users: [{ id, username, role, is_verified, has_ai_access, email, created_at, online }] }` | User list with online status. | +| S→C | `admin_bans` | `{ bans: [{ id, username, ip?, reason?, created_at }] }` | Ban list. | +| S→C | `admin_mutes` | `{ mutes: [{ id, username, created_at }] }` | Mute list. | +| S→C | `admin_action_ok` | `{ msg }` | Action success toast (panel auto-refreshes). | --- @@ -266,327 +418,264 @@ All endpoints are under the `/api` prefix. Create a new account. -**Body:** `{ "username": "...", "password": "...", "email": "..." }` -**Response:** `201 { "token": "jwt...", "user": { id, username, has_ai_access, ai_messages_used } }` +```json +// Request +{ "username": "alice", "password": "hunter2", "email": "alice@example.com" } + +// Response 201 +{ + "token": "eyJ...", + "user": { "id": 3, "username": "alice", "has_ai_access": false, "ai_messages_used": 0 } +} +``` ### `POST /api/auth/login` Authenticate and receive a JWT. -**Body:** `{ "username": "...", "password": "..." }` -**Response:** `200 { "token": "jwt...", "user": { ... } }` +```json +// Request +{ "username": "alice", "password": "hunter2" } -### `GET /api/pm/history?with=` +// Response 200 +{ "token": "eyJ...", "user": { "id": 3, "username": "alice", ... } } +``` -Retrieve encrypted PM history with another user. Requires `Authorization: Bearer `. +### `GET /api/pm/history?with={username}` -**Response:** `200 { "messages": [{ from_me, ciphertext, nonce, ts }] }` +Fetch encrypted PM history with another user. Requires `Authorization: Bearer `. + +```json +// Response 200 +{ + "room_key": "base64...", + "messages": [ + { "from_me": true, "ciphertext": "...", "nonce": "...", "ts": "2025-04-12T..." }, + { "from_me": false, "ciphertext": "...", "nonce": "...", "ts": "2025-04-12T..." } + ] +} +``` ### `POST /api/ai/message` -Send an encrypted message to Violet via REST (alternative to socket). +Send an encrypted message to Violet via REST (alternative to the socket path). -**Body:** `{ "ciphertext": "...", "nonce": "...", "transit_key": "..." }` -**Response:** `200 { "ciphertext": "...", "nonce": "...", "ai_messages_used": N, "has_ai_access": bool }` +```json +// Request (Authorization: Bearer ) +{ "ciphertext": "...", "nonce": "...", "transit_key": "..." } + +// Response 200 +{ "ciphertext": "...", "nonce": "...", "ai_messages_used": 2, "has_ai_access": false } +``` ### `POST /api/payment/success` -Payment webhook to unlock AI access. +Payment webhook (server-to-server). Validates `secret` with constant-time HMAC comparison. -**Body:** `{ "secret": "..." }` -**Response:** `200 { "status": "ok", "has_ai_access": true }` +```json +// Request +{ "secret": "PAYMENT_SECRET", "user_id": 3 } + +// Response 200 +{ "status": "ok", "has_ai_access": true } +``` --- -## Encryption Design +## Role System & Admin Panel -### User-to-User PMs +### Hierarchy -1. **Key Derivation** — The client derives an AES-GCM-256 key from `(password, username)` using PBKDF2 with 100,000 iterations and SHA-256. The salt is `"sexychat:v1:"`. -2. **Encryption** — Messages are encrypted client-side with a random 12-byte nonce before being sent over the socket. -3. **Storage** — The server stores only the base64-encoded ciphertext and nonce. It never sees the plaintext or key. -4. **Decryption** — The recipient decrypts client-side using their own derived key. +| Role | Power Level | Badge | Capabilities | +|------|-------------|-------|-------------| +| `root` | 3 | šŸ‘‘ | Everything. Can promote to admin. Cannot be demoted. | +| `admin` | 2 | āš”ļø | Set roles (mod/user), toggle AI access, all mod actions. | +| `mod` | 1 | šŸ›”ļø | Kick, ban, mute, verify, view admin panel. | +| `user` | 0 | — | Standard chat, PMs, Violet AI (free trial). | -### Violet AI (Transit Encryption) +**Rules:** +- You cannot modify a user whose power level is ≄ your own. +- You cannot assign a role whose power level is ≄ your own. +- The first registered human user should be set to `root` in the database. +- **Legacy:** Guests can enter the shared mod password at login to gain temporary `mod` session access. -1. The client encrypts the message with their derived key and also sends the key (`transit_key`) to the server over HTTPS/WSS. -2. The server decrypts the message (transit), passes plaintext to Ollama for inference, then re-encrypts the AI response with the same transit key. -3. The transit key is used only in memory and is never stored. +### Admin Panel + +Accessible via the shield icon (šŸ›”ļø) in the header (visible to `mod` and above). Three tabs: + +| Tab | Features | +|-----|----------| +| **Users** | Searchable user list. Change roles (dropdown), toggle verification (checkmark), grant/revoke AI access (brain icon). Shows online status. | +| **Bans** | All active bans with username, IP, and date. One-click unban button. | +| **Mutes** | All active mutes. One-click unmute button. | + +All actions trigger a system message in the lobby, auto-refresh the panel, and (where applicable) push live updates to the affected user's session. --- -## Moderation System +## Encryption & Security -Moderators authenticate by providing the shared `ADMIN_PASSWORD` alongside their normal login credentials. Mod powers include: +### PM Encryption (AES-GCM-256) -| Action | Effect | -|--------|--------| -| **Kick** | Disconnects the target. They can rejoin immediately. | -| **Ban** | Adds username + IP to in-memory ban lists. Persists until server restart. | -| **Kickban** | Kick + ban in one action. | -| **Mute / Unmute** | Toggles mute. Muted users cannot send lobby messages. | -| **Verify** | Marks a registered account as verified, allowing them to log in. New registrations require moderator verification before the account can be used. | +``` +1. Server generates a deterministic room key: HMAC-SHA256(JWT_SECRET, room_name) +2. Room key is delivered to both clients via pm_invite / pm_ready events +3. Client encrypts each message with AES-GCM-256 using the room key + random 12-byte nonce +4. Server stores only base64(ciphertext) + base64(nonce) — never sees plaintext +5. Recipient decrypts client-side with the same room key +``` + +### Violet AI Transit Encryption + +``` +1. Client generates a random AES-256 transit key +2. Client encrypts message with transit key → sends { ciphertext, nonce, transit_key } +3. Server decrypts with transit key → sends plaintext to Ollama for inference +4. Server encrypts Violet's response with the same transit key → delivers to client +5. Transit key is ephemeral — used only in memory, never stored +``` + +### Security Measures + +| Category | Implementation | +|----------|---------------| +| **Password hashing** | Bcrypt with adaptive cost factor | +| **Authentication** | JWT tokens (HMAC-SHA256), 7-day expiry | +| **Session restore** | JWT stored in `localStorage`, validated on reconnect | +| **Input validation** | Regex-enforced usernames (`[a-zA-Z0-9_-]`, 1–20 chars), message length limits, SQLAlchemy parameterized queries | +| **Rate limiting** | 6 messages per 5-second window per session | +| **RBAC** | `@_require_role()` decorator with power-level hierarchy checks | +| **Webhook auth** | Constant-time HMAC comparison for payment secret | +| **Ban persistence** | Username + IP stored in DB `bans` table, loaded into memory on startup | +| **Mute persistence** | Stored in DB `mutes` table, loaded on startup | +| **PM invite validation** | Server-side pending invite mapping — `pm_accept` only works for legitimately invited rooms | + +--- + +## Theme System + +10 themes available in **Settings → Chat → Theme**. Each theme is a set of CSS custom properties applied via the `data-theme` attribute on the document root. + +| Theme | Style | Palette | +|-------|-------|---------| +| **Midnight Purple** | Dark | Deep purple + neon magenta *(default)* | +| **Crimson Noir** | Dark | Black + crimson red | +| **Ocean Deep** | Dark | Navy + cyan / teal | +| **Ember** | Dark | Dark brown + orange / amber | +| **Neon Green** | Dark | Black + bright lime | +| **Cyberpunk** | Dark | Purple + electric yellow | +| **Rose Gold** | Dark | Dark rose + blush pink | +| **Arctic** | Light | Cool blue-white + indigo accents | +| **Daylight** | Light | White + pink / purple accents | +| **Midnight Blue** | Dark | Deep navy + blue glow | + +Theme preference is saved to `localStorage` (`sc_theme`) and applied immediately on page load — no flash of default theme. + +--- + +## Settings Panel + +Accessed via the gear icon (āš™ļø) in the header. Four tabs: + +### Account +- View your username and email. +- Change password (requires current password, 6-character minimum for the new one). + +### Chat +- **Theme** — 10-option grid with gradient color swatches. +- **Font Size** — 12–20px slider. +- **Timestamp Format** — 12-hour / 24-hour toggle. +- **Enter to Send** — On/Off. When off, Enter inserts a newline and Shift+Enter sends. +- **Notification Sounds** — On/Off. + +### Violet +- AI access status display (Unlimited āœ“ or X remaining free messages). +- Messages used counter. +- Reset conversation memory button. + +### Premium +- Feature showcase (Unlimited Violet, Encrypted PMs, Priority Access, Custom Themes). +- Pricing display ($10/month). +- Coming soon placeholder. + +All preferences are persisted in `localStorage` with the `sc_` prefix (e.g., `sc_theme`, `sc_fontSize`, `sc_enterSend`). --- ## Violet AI Companion -Violet is an AI persona backed by a local Ollama model. She is configured as a "flirtatious and sophisticated nightclub hostess" via a system prompt. +Violet is an AI chat companion powered by a local Ollama model. She's configured as a *"flirtatious and sophisticated nightclub hostess at an exclusive, dimly-lit members-only club"* via her system prompt in `config.py`. -- **Model**: Configurable via `VIOLET_MODEL` (default: `sam860/dolphin3-llama3.2:3b`) -- **Inference**: Serialised through an eventlet queue — one request at a time to avoid overloading the local GPU/CPU -- **Typing indicator**: `violet_typing` events are broadcast while Ollama is processing -- **Fallback**: If Ollama fails, a graceful fallback message is returned -- **Free tier**: Configurable via `AI_FREE_LIMIT` (default: 3 messages) +### How It Works + +1. User clicks **Violet** in the nicklist → PM room opens automatically (no invite needed for the bot). +2. User's message is transit-encrypted (AES-GCM) and queued to a single-threaded inference greenlet. +3. The AI worker decrypts the message, builds a prompt from the last 20 conversation turns (`violet_history` table), and calls Ollama's `/api/generate` endpoint. +4. The response is transit-encrypted with the same key and delivered as a PM back to the user. +5. Both the user's message and Violet's reply are saved to `violet_history` for context continuity across sessions. + +### Commands + +| Command | Description | +|---------|-------------| +| `/reset` | Clear Violet's memory of your conversation (deletes all `violet_history` rows for your user). | + +### Guest Behavior + +Unregistered guests who PM Violet receive a friendly plaintext reply: + +> *"Hey hun šŸ’œ You'll need to register an account before we can chat privately. Go back to the join screen and sign up — I'll be waiting for you! 😘"* + +### Free Trial & Paywall + +Users get `AI_FREE_LIMIT` (default: 3) free messages. After that, a paywall modal is shown. Admins can grant unlimited access via the admin panel's "Grant AI" button — the user is notified in real-time via `ai_unlock`. + +### Fallback + +If Ollama is unreachable or returns an error, Violet sends a graceful fallback message instead of crashing. --- -## Payment & Premium Access +## Premium / Paywall -The current payment flow is a **stub** intended to be replaced with a real payment provider (e.g. Stripe): +When a user exhausts their free Violet messages, a paywall modal appears with pricing and feature highlights. Premium access (`has_ai_access = True`) can be granted two ways: -1. Client sends the `PAYMENT_SECRET` to `POST /api/payment/success`. -2. Server validates with constant-time comparison, flips `user.has_ai_access = True`. -3. An `ai_unlock` socket event is pushed to the user's active session. +1. **Admin Panel** — Any `admin`+ can click the brain icon (🧠) on a user in the Users tab to toggle unlimited AI access. The user receives a real-time notification. +2. **Payment Webhook** — `POST /api/payment/success` with the payment secret and user ID. Validated with constant-time comparison. -**For production**: Replace the secret-comparison logic with `stripe.Webhook.construct_event()` using the raw request body and `Stripe-Signature` header. +Premium unlocks unlimited Violet messages. The `ai_unlock` event is pushed to the user's active socket session immediately. + +> **Note:** The payment flow is currently a stub. For production, replace the secret-comparison logic with a proper payment provider webhook (e.g., Stripe's `stripe.Webhook.construct_event()`). --- -## Database Schema +## Dependencies -### `users` +From `requirements.txt`: -| Column | Type | Notes | -|--------|------|-------| -| `id` | Integer PK | Auto-increment | -| `username` | String(20) | Unique, indexed | -| `password_hash` | String(128) | bcrypt | -| `email` | String(255) | Unique, nullable | -| `has_ai_access` | Boolean | Premium flag | -| `ai_messages_used` | Integer | Free trial counter | -| `is_verified` | Boolean | Moderator-verified flag | -| `created_at` | DateTime | UTC timestamp | - -### `messages` - -| Column | Type | Notes | -|--------|------|-------| -| `id` | Integer PK | Auto-increment | -| `sender_id` | FK → users.id | | -| `recipient_id` | FK → users.id | | -| `encrypted_content` | Text | Base64 AES-GCM ciphertext | -| `nonce` | String(64) | Base64 AES-GCM nonce/IV | -| `timestamp` | DateTime | Indexed with sender/recipient | - -### `user_ignores` - -| Column | Type | Notes | -|--------|------|-------| -| `id` | Integer PK | Auto-increment | -| `ignorer_id` | FK → users.id | | -| `ignored_id` | FK → users.id | | -| `created_at` | DateTime | | - -Unique composite index on `(ignorer_id, ignored_id)`. +| Package | Version | Purpose | +|---------|---------|---------| +| `flask` | ≄3.0 | Web framework | +| `flask-socketio` | ≄5.3 | WebSocket support | +| `eventlet` | ≄0.35 | Async concurrency | +| `gunicorn` | ≄21.0 | Production WSGI server | +| `flask-sqlalchemy` | ≄3.1 | ORM | +| `flask-migrate` | ≄4.0 | Database migrations (Alembic) | +| `psycopg2-binary` | ≄2.9 | PostgreSQL driver (optional) | +| `bcrypt` | ≄4.0 | Password hashing | +| `PyJWT` | ≄2.8 | JWT auth tokens | +| `cryptography` | ≄42.0 | Server-side AES-GCM | +| `redis` | ≄5.0 | Socket.IO multi-worker adapter (optional) | +| `requests` | ≄2.31 | Ollama HTTP client | --- -## Known Security Issues & Bugs +## License -The following issues have been identified during code review. They are listed by severity. +Private project. All rights reserved. --- -### P0 — Critical (fix immediately) +## Credits -#### 1. JWT Secret Mismatch Between `app.py` and `routes.py` - -**File:** `app.py` line 84, `routes.py` line 30 - -`app.py` generates a random `JWT_SECRET` via `uuid.uuid4().hex` on every server restart. `routes.py` independently hardcodes `"change-me-jwt-secret"` as its default. These are two separate values — JWTs issued by the socket layer (`app.py`) will fail validation in the REST API (`routes.py`) and vice versa. **Authentication is broken across the two entry points.** - -**Impact:** Users who log in via sockets cannot use the REST API, and vice versa. In the worst case, if `routes.py`'s default is left in production, its JWT secret is publicly known from the source code, allowing anyone to forge tokens. - -**Fix:** Use a single shared `JWT_SECRET` loaded once from configuration. Remove the duplicate constant in `routes.py` and import from `app.py` or a shared config module. - ---- - -#### 2. Unauthorised PM Room Join (`pm_accept`) - -**File:** `app.py` — `on_pm_accept` handler - -```python -@socketio.on("pm_accept") -def on_pm_accept(data): - join_room(data.get("room")) # no validation -``` - -Any client can emit `pm_accept` with an arbitrary room name (e.g. `pm:alice:bob`) and silently join that room, receiving all future messages. There is no check that the user was actually invited to this room. - -**Impact:** Complete compromise of private messaging confidentiality (for plaintext/guest PMs). For encrypted PMs, the attacker receives ciphertext but not keys — however, it still leaks metadata (who is talking, when, message sizes). - -**Fix:** Maintain a server-side mapping of pending invitations. Only allow `pm_accept` if the user's SID has a pending invite for that specific room. - ---- - -#### 3. Client-Exploitable Payment Endpoint - -**Files:** `routes.py` — `payment_success`, `chat.js` lines 560–574 - -The payment "verification" consists of the client sending a secret string to the server. The default `PAYMENT_SECRET` is `"change-me-payment-secret"` (known from source). Even if changed, the client-side code in `chat.js` hardcodes the secret and sends it directly: - -```javascript -const secret = "change-me-payment-webhook-secret"; -``` - -Any user with a valid JWT can call `POST /api/payment/success` with the secret and unlock premium AI access for free. - -**Impact:** Complete bypass of the payment system. Any registered user gets unlimited AI access without paying. - -**Fix:** Replace with a server-side Stripe/payment-provider webhook. The payment confirmation must originate from the payment provider's servers, not from the client. - ---- - -#### 4. PM Invite Broadcast When PMing Violet - -**File:** `app.py` — `on_pm_open` handler, lines 607–612 - -When a user opens a PM with Violet, `target_sid` is `None` (Violet has no socket connection). The code then executes: - -```python -socketio.emit("pm_invite", {"from": user["username"], "room": room}, to=None) -``` - -Emitting `to=None` broadcasts the event to **all connected clients**, informing everyone that the user is starting a private conversation with Violet. - -**Impact:** Privacy leak — all connected users learn who is chatting with Violet. - -**Fix:** Skip the `pm_invite` emit when the target is Violet. The initiator already receives `pm_ready`; no invite is needed for a bot. - ---- - -### P1 — High Severity - -#### 5. E2E Encryption Is Fundamentally Broken for User-to-User PMs - -**File:** `static/crypto.js` — `deriveKey` - -Keys are derived from `(password, username)` using PBKDF2. When User A sends a PM to User B: -- User A encrypts with a key derived from *A's password and A's username*. -- User B attempts to decrypt with a key derived from *B's password and B's username*. -- These are entirely different keys. **Decryption will always fail.** - -The only scenario where this works is when a user decrypts their own messages (e.g. PM history reload), since the key matches. But real-time cross-user PMs cannot be decrypted. - -**Impact:** User-to-user encrypted PMs are non-functional. Users will see decryption errors for every received message. - -**Fix:** Implement a proper key exchange protocol (e.g. ECDH / X25519) where both parties derive a shared secret, or use a server-mediated key-wrapping scheme. - ---- - -#### 6. Null DOM Element References Crash the Frontend - -**File:** `static/chat.js` - -- Line 547: `$("tab-ai-violet")` — references an element ID that does not exist in `index.html`. -- Line 44: `$("violet-trial-badge")` — also missing from the HTML. - -These return `null`, and any property access (e.g. `.onclick`, `.classList`) will throw `TypeError: Cannot read properties of null`. - -**Impact:** JavaScript execution halts at these points, potentially breaking logout, tab switching, or the entire chat UI on load. - -**Fix:** Add the missing elements to `index.html`, or add null guards before accessing these elements. - ---- - -#### 7. CORS Wildcard on WebSocket - -**File:** `app.py` line 381 - -```python -socketio.init_app(app, cors_allowed_origins="*", ...) -``` - -This allows any website on the internet to open WebSocket connections to the server. A malicious page could connect on behalf of a visiting user if cookies/tokens are available. - -**Impact:** Cross-site WebSocket hijacking. An attacker's page could impersonate users, send messages, or eavesdrop on the lobby. - -**Fix:** Restrict `cors_allowed_origins` to the actual domain(s) serving the frontend. - ---- - -### P2 — Medium Severity - -#### 8. Massive Code Duplication Between `app.py` and `routes.py` - -Both files independently define their own copies of: -- `_aesgcm_encrypt` / `_aesgcm_decrypt` -- `_issue_jwt` / `_verify_jwt` -- `_save_pm` / `_persist_message` -- Constants: `AI_FREE_LIMIT`, `AI_BOT_NAME`, `JWT_SECRET` - -They have already diverged — `routes.py` uses mock AI responses (`random.choice(AI_RESPONSES)`) while `app.py` calls Ollama. The JWT secrets are different (see issue #1). - -**Impact:** Bugs fixed in one file won't be fixed in the other. Behaviour differs depending on whether the user interacts via sockets or REST. Maintenance burden grows over time. - -**Fix:** Extract shared utilities (crypto, JWT, DB helpers, constants) into a common module (e.g. `utils.py` or `config.py`). Import from there in both `app.py` and `routes.py`. - ---- - -#### 9. Context Menu Breaks After First Use - -**File:** `static/chat.js` — `showContextMenu` function, lines 453–455 - -```javascript -const newMenu = contextMenu.cloneNode(true); -contextMenu.replaceWith(newMenu); -``` - -The function clones and replaces the context menu DOM node to clear event listeners. However, the module-level `contextMenu` variable still references the old (now-removed) node. On the second right-click, the code operates on a detached element. - -**Impact:** Context menu stops working after the first use. Users can only PM/ignore/mod-action once per page load. - -**Fix:** Update the variable reference after replacement, or use `removeEventListener` / event delegation instead of cloning. - ---- - -#### 10. All Runtime State Is In-Memory Only - -**File:** `app.py` lines 113–118 - -`connected_users`, `banned_usernames`, `banned_ips`, and `muted_users` are plain Python dicts/sets. All bans, mutes, and IP blocks are lost on every server restart. - -**Impact:** Banned users can return simply by waiting for a server restart. Moderator actions have no lasting effect. - -**Fix:** Persist bans and mutes to the database. Load them into memory on startup. - ---- - -### P3 — Low Severity - -#### 11. Deprecated `datetime.utcnow()` Usage - -**File:** `models.py` — all `default=datetime.utcnow` column definitions - -`datetime.utcnow()` is deprecated as of Python 3.12. It returns a naive datetime with no timezone info, which can cause subtle bugs with timezone-aware code. - -**Impact:** Deprecation warnings on Python 3.12+. Potential timezone-related bugs if the codebase later mixes aware and naive datetimes. - -**Fix:** Use `datetime.now(datetime.timezone.utc)` or `func.now()` (SQLAlchemy server default) instead. - ---- - -#### 12. Default Admin Password - -**File:** `app.py` line 85 - -```python -ADMIN_PASSWORD = _get_conf("ADMIN_PASSWORD", "admin1234") -``` - -If the operator does not set a custom admin password, anyone who reads the source code or guesses `admin1234` gains full moderator access (kick, ban, mute, verify users). - -**Impact:** Unauthorised moderator access in default-configured deployments. - -**Fix:** Require `ADMIN_PASSWORD` to be explicitly set in config. Refuse to start (or disable mod features) if it's left at the default value. +Built by **End3r** — [git.computertech.dev/lord3nd3r/aprhodite](https://git.computertech.dev/lord3nd3r/aprhodite)