feat(models): add UserIntegration model, encrypt IMAP passwords, add integrations API

- Add IntegrationDirection/IntegrationType constants and UserIntegration SQLAlchemy model
  with owner_id, direction, integration_type, name, config (JSON), credentials (encrypted),
  is_active, last_used_at, last_error, created_at, updated_at fields
- Fix UserImapAccount password encryption: encrypt_value() on create/update,
  decrypt_value() in IMAP tasks and test-connection endpoint (backward compatible)
- Add Alembic migration 023_add_user_integrations
- Add app/api/integrations.py: CRUD + credentials endpoint with owner-scoped access
- Register integrations router in app/api/__init__.py
- Add 39 tests in tests/test_api_integrations.py covering model, CRUD, encryption

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-08 15:02:38 +00:00
parent 077672d8c1
commit 743627ecc2
8 changed files with 1099 additions and 7 deletions
+14 -1
View File
@@ -37,6 +37,19 @@ logger = logging.getLogger(__name__)
# Initialize Redis connection using Celery's Redis settings
redis_client = redis.StrictRedis.from_url(settings.redis_url, decode_responses=True)
def _decrypt_imap_password(password: str | None) -> str | None:
"""Decrypt an IMAP account password stored in the database.
Passwords are stored encrypted (Fernet, ``enc:`` prefix) for new records;
legacy plaintext records are returned unchanged so existing accounts
continue to work until they are next updated via the API.
"""
from app.utils.encryption import decrypt_value
return decrypt_value(password)
LOCK_KEY = "imap_lock" # Unique key for locking
LOCK_EXPIRE = 300 # Lock expires in 5 minutes
@@ -160,7 +173,7 @@ def _pull_user_imap_accounts() -> None:
host=acct.host,
port=acct.port,
username=acct.username,
password=acct.password,
password=_decrypt_imap_password(acct.password),
use_ssl=acct.use_ssl,
delete_after_process=acct.delete_after_process,
)