feat: PostgreSQL migration, OAuth write MCP, Streamable HTTP, env-driven config, admin UI, landing page
- Migrate database from SQLite to PostgreSQL 16 (dual-backend with SQLite fallback) - Add Streamable HTTP MCP transport (replaces SSE): /readonly/mcp, /write/mcp, /mcp - Add OAuth ctxd.write scope and public write MCP surface - Add ctxd.write token validation (write-scoped tokens only on /write/mcp) - Add env-driven configuration (.env file with env var precedence over ctxd.yaml) - Add PostgreSQL to docker-compose.yml with healthcheck - Add psycopg dependency, migration script (SQLite → PostgreSQL) - Add admin UI: projects tab with typed-confirm delete, user management (list/manage subtabs) - Add OAuth client management: create, list, revoke (UI, CLI, API) - Add user active/inactive lifecycle (PATCH/DELETE APIs) - Add public landing page with themed login form (cookie-based session) - Add get_client_guide MCP tool (locked LLM-CLIENT.MD in ctxd-docs project) - Add DELETE /projects/<id> endpoint with cascading deletes - Add project_delete to db.py with FK ON DELETE SET NULL for audit_log - Add cookie-based session auth (ctxd_session cookie on login) - Add landing.html (public host) vs ui.html (internal dashboard) - Add schema_sqlite.sql for SQLite fallback - Add auth_password.py (PBKDF2-SHA256 password hashing) - Add .env.example template with all documented env vars - Add README.md with full setup, config, API, CLI, and troubleshooting docs - Add SKILL.md (canonical LLM client guide, lives in project root) - Update Traefik template: route everything except /mcp - Update OAuth discovery: advertise ctxd.write scope, /readonly/mcp resource - Update Hermes MCP config: /mcp endpoint with Bearer header - Remove DB-level audit_log triggers (conflict with FK ON DELETE SET NULL) - Remove SSE transport code (replaced by Streamable HTTP) - Untrack __pycache__ and data/ctxd.db from git
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""Password hashing for CTXD user accounts (stdlib only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
PBKDF2_ITERATIONS = 600_000
|
||||
SCHEME = "pbkdf2_sha256"
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
if not password:
|
||||
raise ValueError("password required")
|
||||
salt = secrets.token_hex(16)
|
||||
digest = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
password.encode("utf-8"),
|
||||
salt.encode("utf-8"),
|
||||
PBKDF2_ITERATIONS,
|
||||
)
|
||||
return f"{SCHEME}${PBKDF2_ITERATIONS}${salt}${digest.hex()}"
|
||||
|
||||
|
||||
def verify_password(password: str, token_hash: str | None) -> bool:
|
||||
if not password or not token_hash:
|
||||
return False
|
||||
try:
|
||||
scheme, iters_s, salt, expected_hex = token_hash.split("$", 3)
|
||||
if scheme != SCHEME:
|
||||
return False
|
||||
digest = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
password.encode("utf-8"),
|
||||
salt.encode("utf-8"),
|
||||
int(iters_s),
|
||||
)
|
||||
return secrets.compare_digest(digest.hex(), expected_hex)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
Reference in New Issue
Block a user