Features: - Hybrid semantic + keyword quiz search (pgvector HNSW + PostgreSQL ILIKE) - AWS Bedrock Titan Embed V2 embeddings via LiteLLM proxy (0.71 cosine sim) - Multi-provider TTS: OpenAI, AWS Polly (neural), ElevenLabs, Google Cloud TTS - Unified Settings page (profile, theme, Nextcloud integration, admin shortcuts) - Good morning/afternoon greeting on dashboard - manage.py CLI: reset-password, list-users, reembed - Email verification enforced: register no longer returns JWT for unverified users - Quiz search with debounced input, semantic/keyword/title modes, highlighted snippets - TTS button: loading/playing states, voice selector locked during playback - TTS auto-stops when navigating between questions - Footer added; mobile quiz nav overflow fixed; markdown theme body selector fixed - OpenAI Alloy as default TTS voice; favicon added - SMTP configured via smtp2go; password reset rate limiting (3/hour) - PostgreSQL upgraded to pgvector/pgvector:pg16 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
100 lines
3.1 KiB
Python
100 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""PedQuiz management CLI.
|
|
|
|
Usage (inside container):
|
|
python manage.py reset-password <email> <new-password>
|
|
python manage.py list-users
|
|
python manage.py reembed # Regenerate all question embeddings
|
|
"""
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
# Ensure app is importable
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def reset_password(email: str, new_password: str):
|
|
from app.database import SessionLocal
|
|
from app.models.user import User
|
|
from app.utils.auth import get_password_hash
|
|
|
|
if len(new_password) < 8:
|
|
print("Error: Password must be at least 8 characters")
|
|
sys.exit(1)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.query(User).filter(User.email == email).first()
|
|
if not user:
|
|
print(f"Error: No user found with email '{email}'")
|
|
sys.exit(1)
|
|
user.hashed_password = get_password_hash(new_password)
|
|
db.commit()
|
|
print(f"Password reset for {user.email} (name={user.name!r}, role={user.role})")
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def list_users():
|
|
from app.database import SessionLocal
|
|
from app.models.user import User
|
|
from app.models.email_verification import EmailVerification
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
users = db.query(User).order_by(User.created_at).all()
|
|
print(f"{'ID':>4} {'Email':40} {'Role':12} {'Verified':8} Name")
|
|
print("-" * 90)
|
|
for u in users:
|
|
v = db.query(EmailVerification).filter(EmailVerification.user_id == u.id).first()
|
|
verified = "yes" if (v and v.verified_at) else "no"
|
|
print(f"{u.id:>4} {u.email:40} {u.role:12} {verified:8} {u.name}")
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def reembed():
|
|
"""Regenerate embeddings for all questions using current embedding model."""
|
|
from app.database import SessionLocal
|
|
from app.models.question import Question
|
|
from app.services import embedding_service
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
questions = db.query(Question).all()
|
|
print(f"Re-embedding {len(questions)} questions...")
|
|
ok = fail = 0
|
|
for q in questions:
|
|
q.embedding = None # force regeneration
|
|
if embedding_service.embed_question(q):
|
|
ok += 1
|
|
else:
|
|
fail += 1
|
|
db.commit()
|
|
print(f"Done: {ok} succeeded, {fail} failed")
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="PedQuiz management CLI")
|
|
sub = parser.add_subparsers(dest="command")
|
|
|
|
rp = sub.add_parser("reset-password", help="Reset a user password")
|
|
rp.add_argument("email", help="User email address")
|
|
rp.add_argument("password", help="New password (min 8 chars)")
|
|
|
|
sub.add_parser("list-users", help="List all users with verification status")
|
|
sub.add_parser("reembed", help="Regenerate all question embeddings")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "reset-password":
|
|
reset_password(args.email, args.password)
|
|
elif args.command == "list-users":
|
|
list_users()
|
|
elif args.command == "reembed":
|
|
reembed()
|
|
else:
|
|
parser.print_help()
|