chih.yikuan
🔧 Fix OAuth: auto-detect redirect_uri + sync all ClassLens updates
e5c2788
"""SQLite database for token storage."""
from __future__ import annotations
import aiosqlite
import json
import base64
import os
from pathlib import Path
from datetime import datetime
from typing import Optional
from cryptography.fernet import Fernet
DATABASE_PATH = Path(__file__).parent.parent / "classlens.db"
def get_encryption_key() -> bytes:
"""Get or generate encryption key."""
key = os.getenv("ENCRYPTION_KEY", "")
if not key:
# Generate a new key for development (should be set in production)
key = Fernet.generate_key().decode()
print(f"⚠️ No ENCRYPTION_KEY set. Generated temporary key: {key}")
print("⚠️ Set this in your .env file for persistent token storage.")
return key.encode() if isinstance(key, str) else key
def encrypt_token(token_data: dict) -> str:
"""Encrypt token data for storage."""
key = get_encryption_key()
f = Fernet(key)
json_data = json.dumps(token_data)
encrypted = f.encrypt(json_data.encode())
return base64.b64encode(encrypted).decode()
def decrypt_token(encrypted_data: str) -> dict:
"""Decrypt stored token data."""
key = get_encryption_key()
f = Fernet(key)
encrypted = base64.b64decode(encrypted_data.encode())
decrypted = f.decrypt(encrypted)
return json.loads(decrypted.decode())
async def init_database():
"""Initialize the database with required tables."""
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS oauth_tokens (
teacher_email TEXT PRIMARY KEY,
encrypted_tokens TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS analysis_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
teacher_email TEXT NOT NULL,
exam_title TEXT,
report_markdown TEXT,
report_json TEXT,
created_at TEXT NOT NULL
)
""")
await db.commit()
async def save_oauth_tokens(teacher_email: str, tokens: dict):
"""Save encrypted OAuth tokens for a teacher."""
encrypted = encrypt_token(tokens)
now = datetime.utcnow().isoformat()
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute("""
INSERT INTO oauth_tokens (teacher_email, encrypted_tokens, created_at, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(teacher_email) DO UPDATE SET
encrypted_tokens = excluded.encrypted_tokens,
updated_at = excluded.updated_at
""", (teacher_email, encrypted, now, now))
await db.commit()
async def get_oauth_tokens(teacher_email: str) -> Optional[dict]:
"""Retrieve OAuth tokens for a teacher."""
async with aiosqlite.connect(DATABASE_PATH) as db:
async with db.execute(
"SELECT encrypted_tokens FROM oauth_tokens WHERE teacher_email = ?",
(teacher_email,)
) as cursor:
row = await cursor.fetchone()
if row:
return decrypt_token(row[0])
return None
async def delete_oauth_tokens(teacher_email: str):
"""Delete OAuth tokens for a teacher."""
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute(
"DELETE FROM oauth_tokens WHERE teacher_email = ?",
(teacher_email,)
)
await db.commit()
async def save_report(teacher_email: str, exam_title: str, report_markdown: str, report_json: str):
"""Save an analysis report."""
now = datetime.utcnow().isoformat()
async with aiosqlite.connect(DATABASE_PATH) as db:
await db.execute("""
INSERT INTO analysis_reports (teacher_email, exam_title, report_markdown, report_json, created_at)
VALUES (?, ?, ?, ?, ?)
""", (teacher_email, exam_title, report_markdown, report_json, now))
await db.commit()
async def get_teacher_reports(teacher_email: str, limit: int = 10) -> list[dict]:
"""Get recent reports for a teacher."""
async with aiosqlite.connect(DATABASE_PATH) as db:
db.row_factory = aiosqlite.Row
async with db.execute("""
SELECT id, exam_title, report_markdown, report_json, created_at
FROM analysis_reports
WHERE teacher_email = ?
ORDER BY created_at DESC
LIMIT ?
""", (teacher_email, limit)) as cursor:
rows = await cursor.fetchall()
return [dict(row) for row in rows]