- User model: new 'role' column (root > admin > mod > user) - End3r (id=2) set as 'root' (GOD admin) - Admin panel modal: Users tab (search, set roles, verify, grant AI), Bans tab (list/unban), Mutes tab (list/unmute) - Role-based permission checks: root can set admins, admins set mods, mods can kick/ban/mute/verify - Shield icon in header (visible to mod+) opens admin panel - Nicklist shows role icons: crown (root), swords (admin), shield (mod) - Context menu: added Mute/Unmute action - Live role_updated event pushes role changes to online users - role_power hierarchy prevents privilege escalation |
||
|---|---|---|
| static | ||
| .env.example | ||
| .gitignore | ||
| README.md | ||
| app.py | ||
| config.py | ||
| database.py | ||
| index.html | ||
| models.py | ||
| requirements.txt | ||
| routes.py | ||
| start.py | ||
README.md
Aphrodite — SexyChat
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.
Table of Contents
- Features
- Architecture Overview
- Tech Stack
- Project Structure
- Configuration
- Installation & Setup
- Running the Server
- Socket Protocol
- REST API Endpoints
- Encryption Design
- Moderation System
- Violet AI Companion
- Payment & Premium Access
- Database Schema
- Known Security Issues & Bugs
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.pywraps Gunicorn with daemon management (start, stop, restart, status, debug).
Architecture Overview
┌──────────────┐ WebSocket / HTTP ┌─────────────────────┐
│ Browser │ ◄──────────────────────────────►│ Flask-SocketIO │
│ (chat.js) │ │ (app.py) │
│ (crypto.js) │ │ │
└──────────────┘ │ ┌───────────────┐ │
│ │ REST Blueprint │ │
│ │ (routes.py) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ SQLAlchemy │ │
│ │ (database.py) │ │
│ │ (models.py) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ SQLite / PG │ │
│ └───────────────┘ │
│ │
│ ┌───────────────┐ │
│ │ Ollama (AI) │ │
│ └───────────────┘ │
└─────────────────────┘
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_typingindicators while busy. - Lobby is ephemeral — No lobby messages are ever written to the database.
- PMs are persisted for registered users — Encrypted messages are stored in the
messagestable. History is capped at 500 messages per conversation pair.
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) |
| Frontend | Vanilla JS, Socket.IO 4 client, custom CSS |
Project Structure
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)
├── 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)
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:
{
"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
Prerequisites
- Python 3.10+
- pip
- Ollama (optional — required only for live AI responses)
Install
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:
flask db migrate -m "description"
flask db upgrade
Ollama (for live AI)
# Install Ollama (https://ollama.com)
ollama pull sam860/dolphin3-llama3.2:3b
ollama serve # default :11434
If Ollama is unavailable, routes.py falls back to canned responses; app.py returns a placeholder message.
Running the Server
Process Manager (recommended for production)
python start.py start # Start as background daemon
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
gunicorn --worker-class eventlet -w 1 --bind 0.0.0.0:5000 start:application
Development (Flask built-in server)
from app import create_app, socketio
app = create_app()
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
Socket Protocol
Client → Server Events
| 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). |
Server → Client Events
| 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. |
REST API Endpoints
All endpoints are under the /api prefix.
POST /api/auth/register
Create a new account.
Body: { "username": "...", "password": "...", "email": "..." }
Response: 201 { "token": "jwt...", "user": { id, username, has_ai_access, ai_messages_used } }
POST /api/auth/login
Authenticate and receive a JWT.
Body: { "username": "...", "password": "..." }
Response: 200 { "token": "jwt...", "user": { ... } }
GET /api/pm/history?with=<username>
Retrieve encrypted PM history with another user. Requires Authorization: Bearer <token>.
Response: 200 { "messages": [{ from_me, ciphertext, nonce, ts }] }
POST /api/ai/message
Send an encrypted message to Violet via REST (alternative to socket).
Body: { "ciphertext": "...", "nonce": "...", "transit_key": "..." }
Response: 200 { "ciphertext": "...", "nonce": "...", "ai_messages_used": N, "has_ai_access": bool }
POST /api/payment/success
Payment webhook to unlock AI access.
Body: { "secret": "..." }
Response: 200 { "status": "ok", "has_ai_access": true }
Encryption Design
User-to-User PMs
- 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:<lowercase_username>". - Encryption — Messages are encrypted client-side with a random 12-byte nonce before being sent over the socket.
- Storage — The server stores only the base64-encoded ciphertext and nonce. It never sees the plaintext or key.
- Decryption — The recipient decrypts client-side using their own derived key.
Violet AI (Transit Encryption)
- The client encrypts the message with their derived key and also sends the key (
transit_key) to the server over HTTPS/WSS. - The server decrypts the message (transit), passes plaintext to Ollama for inference, then re-encrypts the AI response with the same transit key.
- The transit key is used only in memory and is never stored.
Moderation System
Moderators authenticate by providing the shared ADMIN_PASSWORD alongside their normal login credentials. Mod powers include:
| 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. |
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.
- 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_typingevents 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)
Payment & Premium Access
The current payment flow is a stub intended to be replaced with a real payment provider (e.g. Stripe):
- Client sends the
PAYMENT_SECRETtoPOST /api/payment/success. - Server validates with constant-time comparison, flips
user.has_ai_access = True. - An
ai_unlocksocket event is pushed to the user's active session.
For production: Replace the secret-comparison logic with stripe.Webhook.construct_event() using the raw request body and Stripe-Signature header.
Database Schema
users
| 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).
Known Security Issues & Bugs
The following issues have been identified during code review. They are listed by severity.
P0 — Critical (fix immediately)
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
@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:
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:
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 inindex.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
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
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
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.