fix: credit tracking, voice clone TTL, avatar upload ui, asset serving fallback, OAuth encryption, free plan video renders, backlink outreach sprint

This commit is contained in:
ajaysi
2026-05-25 17:07:35 +05:30
parent 090d69761f
commit 9b3bec698b
99 changed files with 15892 additions and 1278 deletions

View File

@@ -0,0 +1,79 @@
"""
Shared OAuth callback utilities for Wix and WordPress integrations.
Provides hardened postMessage-based HTML callback generation, origin
validation, and string sanitization used across OAuth callback routes.
"""
import json
import os
from typing import Any, Optional
from urllib.parse import urlparse
def sanitize_string(value: Any, max_len: int = 500) -> str:
if value is None:
return ""
return " ".join(str(value).split())[:max_len]
def sanitize_error(error: Exception, max_len: int = 500) -> str:
return sanitize_string(error, max_len)
def normalize_origin(url: Optional[str]) -> Optional[str]:
if not url:
return None
parsed = urlparse(url.strip())
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
return None
return f"{parsed.scheme}://{parsed.netloc}"
def trusted_frontend_origin() -> Optional[str]:
origins_env = os.getenv("OAUTH_CALLBACK_ALLOWED_ORIGINS", "")
configured = [
origin
for origin in (normalize_origin(o) for o in origins_env.split(",") if o.strip())
if origin is not None
]
if configured:
return configured[0]
return normalize_origin(os.getenv("FRONTEND_URL"))
def build_oauth_callback_html(
payload: dict,
title: str,
heading: str,
message: str,
) -> str:
trusted_origin = trusted_frontend_origin()
payload_json = json.dumps(payload)
target_origin_json = json.dumps(trusted_origin or "")
heading_html = heading.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
message_html = message.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
return f"""
<!DOCTYPE html>
<html>
<head><title>{title}</title></head>
<body>
<h1>{heading_html}</h1>
<p>{message_html}</p>
<script>
(function() {{
var payload = {payload_json};
var targetOrigin = {target_origin_json};
var destination = window.opener || window.parent;
if (destination && targetOrigin) {{
try {{
destination.postMessage(payload, targetOrigin);
window.close();
return;
}} catch (_e) {{}}
}}
}})();
</script>
</body>
</html>
"""

View File

@@ -8,7 +8,7 @@ import sqlite3
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from loguru import logger
from cryptography.fernet import Fernet, InvalidToken
from services.database import get_user_db_path
@@ -17,6 +17,66 @@ class WixOAuthService:
def __init__(self, db_path: Optional[str] = None):
self.db_path = db_path
self.token_encryption_key = (
os.getenv("WIX_TOKEN_ENCRYPTION_KEY")
or os.getenv("OAUTH_TOKEN_ENCRYPTION_KEY")
)
self._fernet = self._initialize_fernet()
self._migration_done: set = set()
def _initialize_fernet(self) -> Optional[Fernet]:
if not self.token_encryption_key:
logger.error("Wix token encryption key is not configured.")
return None
try:
return Fernet(self.token_encryption_key.encode("utf-8"))
except Exception:
logger.error("Wix token encryption key is invalid.")
return None
def _encrypt_token(self, token: Optional[str]) -> Optional[str]:
if not token:
return None
if not self._fernet:
raise ValueError("Token encryption is unavailable: missing/invalid managed key")
return self._fernet.encrypt(token.encode("utf-8")).decode("utf-8")
def _decrypt_token(self, token_blob: Optional[str]) -> Optional[str]:
if not token_blob:
return None
if not self._fernet:
raise ValueError("Token decryption is unavailable: missing/invalid managed key")
return self._fernet.decrypt(token_blob.encode("utf-8")).decode("utf-8")
def _is_likely_encrypted_blob(self, value: Optional[str]) -> bool:
return bool(value and value.startswith("gAAAAA"))
def _migrate_plaintext_tokens_if_needed(self, conn: sqlite3.Connection, user_id: str) -> None:
if not self._fernet or user_id in self._migration_done:
return
cursor = conn.cursor()
cursor.execute(
"SELECT id, access_token, refresh_token FROM wix_oauth_tokens WHERE user_id = ?",
(user_id,),
)
rows = cursor.fetchall()
migrated = 0
for token_id, access_token, refresh_token in rows:
needs_access = access_token and not self._is_likely_encrypted_blob(access_token)
needs_refresh = refresh_token and not self._is_likely_encrypted_blob(refresh_token)
if not (needs_access or needs_refresh):
continue
enc_access = self._encrypt_token(access_token) if needs_access else access_token
enc_refresh = self._encrypt_token(refresh_token) if needs_refresh else refresh_token
cursor.execute(
"UPDATE wix_oauth_tokens SET access_token = ?, refresh_token = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
(enc_access, enc_refresh, token_id, user_id),
)
migrated += 1
if migrated:
conn.commit()
logger.info(f"Wix OAuth token migration completed for user {user_id}; rows migrated={migrated}")
self._migration_done.add(user_id)
def _get_db_path(self, user_id: str) -> str:
if self.db_path:
@@ -173,13 +233,16 @@ class WixOAuthService:
if expires_in:
expires_at = datetime.now() + timedelta(seconds=expires_in)
encrypted_access = self._encrypt_token(access_token)
encrypted_refresh = self._encrypt_token(refresh_token) if refresh_token else None
with sqlite3.connect(db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO wix_oauth_tokens
(user_id, access_token, refresh_token, token_type, expires_at, expires_in, scope, site_id, member_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (user_id, access_token, refresh_token, token_type, expires_at, expires_in, scope, site_id, member_id))
''', (user_id, encrypted_access, encrypted_refresh, token_type, expires_at, expires_in, scope, site_id, member_id))
conn.commit()
logger.info(f"Wix OAuth: Token inserted into database for user {user_id}")
@@ -200,6 +263,7 @@ class WixOAuthService:
return []
with sqlite3.connect(db_path) as conn:
self._migrate_plaintext_tokens_if_needed(conn, user_id)
cursor = conn.cursor()
cursor.execute('''
SELECT id, access_token, refresh_token, token_type, expires_at, expires_in, scope, site_id, member_id, created_at
@@ -210,10 +274,29 @@ class WixOAuthService:
tokens = []
for row in cursor.fetchall():
access_token_val = row[1]
refresh_token_val = row[2]
try:
decrypted_access = (
self._decrypt_token(access_token_val)
if self._is_likely_encrypted_blob(access_token_val)
else access_token_val
)
except InvalidToken:
logger.error(f"Failed to decrypt Wix access token for user {user_id}, token_id={row[0]}")
continue
try:
decrypted_refresh = (
self._decrypt_token(refresh_token_val)
if self._is_likely_encrypted_blob(refresh_token_val)
else refresh_token_val
)
except InvalidToken:
decrypted_refresh = None
tokens.append({
"id": row[0],
"access_token": row[1],
"refresh_token": row[2],
"access_token": decrypted_access,
"refresh_token": decrypted_refresh,
"token_type": row[3],
"expires_at": row[4],
"expires_in": row[5],
@@ -248,9 +331,9 @@ class WixOAuthService:
}
with sqlite3.connect(db_path) as conn:
self._migrate_plaintext_tokens_if_needed(conn, user_id)
cursor = conn.cursor()
# Get all tokens (active and expired)
cursor.execute('''
SELECT id, access_token, refresh_token, token_type, expires_at, expires_in, scope, site_id, member_id, created_at, is_active
FROM wix_oauth_tokens
@@ -263,10 +346,29 @@ class WixOAuthService:
expired_tokens = []
for row in cursor.fetchall():
access_token_val = row[1]
refresh_token_val = row[2]
try:
decrypted_access = (
self._decrypt_token(access_token_val)
if self._is_likely_encrypted_blob(access_token_val)
else access_token_val
)
except InvalidToken:
decrypted_access = None
try:
decrypted_refresh = (
self._decrypt_token(refresh_token_val)
if self._is_likely_encrypted_blob(refresh_token_val)
else refresh_token_val
)
except InvalidToken:
decrypted_refresh = None
token_data = {
"id": row[0],
"access_token": row[1],
"refresh_token": row[2],
"access_token": decrypted_access,
"refresh_token": decrypted_refresh,
"token_type": row[3],
"expires_at": row[4],
"expires_in": row[5],
@@ -331,34 +433,46 @@ class WixOAuthService:
user_id: str,
access_token: str,
refresh_token: Optional[str] = None,
expires_in: Optional[int] = None
expires_in: Optional[int] = None,
token_id: Optional[int] = None
) -> bool:
"""Update tokens for a user (e.g., after refresh)."""
try:
# Ensure DB initialized for this user
self._init_db(user_id)
db_path = self._get_db_path(user_id)
expires_at = None
if expires_in:
expires_at = datetime.now() + timedelta(seconds=expires_in)
encrypted_access = self._encrypt_token(access_token)
encrypted_refresh = self._encrypt_token(refresh_token) if refresh_token else None
with sqlite3.connect(db_path) as conn:
self._migrate_plaintext_tokens_if_needed(conn, user_id)
cursor = conn.cursor()
if refresh_token:
cursor.execute('''
UPDATE wix_oauth_tokens
SET access_token = ?, refresh_token = ?, expires_at = ?, expires_in = ?,
is_active = TRUE, updated_at = datetime('now')
WHERE user_id = ? AND refresh_token = ?
''', (access_token, refresh_token, expires_at, expires_in, user_id, refresh_token))
if token_id:
if encrypted_refresh:
cursor.execute('''
UPDATE wix_oauth_tokens
SET access_token = ?, refresh_token = ?, expires_at = ?, expires_in = ?,
is_active = TRUE, updated_at = datetime('now')
WHERE user_id = ? AND id = ?
''', (encrypted_access, encrypted_refresh, expires_at, expires_in, user_id, token_id))
else:
cursor.execute('''
UPDATE wix_oauth_tokens
SET access_token = ?, expires_at = ?, expires_in = ?,
is_active = TRUE, updated_at = datetime('now')
WHERE user_id = ? AND id = ?
''', (encrypted_access, expires_at, expires_in, user_id, token_id))
else:
cursor.execute('''
UPDATE wix_oauth_tokens
SET access_token = ?, expires_at = ?, expires_in = ?,
is_active = TRUE, updated_at = datetime('now')
WHERE user_id = ? AND id = (SELECT id FROM wix_oauth_tokens WHERE user_id = ? ORDER BY created_at DESC LIMIT 1)
''', (access_token, expires_at, expires_in, user_id, user_id))
''', (encrypted_access, expires_at, expires_in, user_id, user_id))
conn.commit()
logger.info(f"Wix OAuth: Tokens updated for user {user_id}")