diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5053361..c2ce2a40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New dependencies in pyproject.toml: fastapi, uvicorn, sqlalchemy, alembic, pydantic-settings, passlib, argon2-cffi, python-multipart, python-jose, cryptography
- API documentation generated from FastAPI OpenAPI schema (available at /docs and /redoc)
+- User model enhancement with profile and API key management (Issue #3)
+ - Extended User model with tax_jurisdiction and timezone fields [file:tradingagents/api/models/user.py:47-54](tradingagents/api/models/user.py)
+ - Tax jurisdiction field supporting country (e.g., "US", "AU") and state/province level codes (e.g., "US-CA", "AU-NSW")
+ - IANA timezone identifier field (e.g., "America/New_York", "Australia/Sydney") with automatic validation
+ - Email verification status tracking via is_verified boolean field [file:tradingagents/api/models/user.py:60-64](tradingagents/api/models/user.py)
+ - Secure API key management with bcrypt hashing and unique constraints [file:tradingagents/api/models/user.py:55-59](tradingagents/api/models/user.py)
+ - API key service module with generate_api_key(), hash_api_key(), and verify_api_key() functions [file:tradingagents/api/services/api_key_service.py](tradingagents/api/services/api_key_service.py)
+ - API key generation using secrets.token_urlsafe() with 256-bit entropy and 'ta_' prefix
+ - Bcrypt-based API key hashing using pwdlib.PasswordHash for secure storage
+ - Constant-time verification to prevent timing attacks on API keys
+ - Timezone validator using IANA zoneinfo database [file:tradingagents/api/services/validators.py:134-191](tradingagents/api/services/validators.py)
+ - Tax jurisdiction validator supporting 50+ country codes and state/province subdivisions [file:tradingagents/api/services/validators.py:193-283](tradingagents/api/services/validators.py)
+ - Utility functions get_available_timezones() and get_available_tax_jurisdictions() for UI dropdowns [file:tradingagents/api/services/validators.py:285-333](tradingagents/api/services/validators.py)
+ - Database migration 002_add_user_profile_fields.py with proper defaults and constraints [file:migrations/versions/002_add_user_profile_fields.py](migrations/versions/002_add_user_profile_fields.py)
+ - Migration rollback support for reversible schema changes
+ - Comprehensive docstrings and security considerations for all functions
+
- Test fixtures directory with centralized mock data (Issue #51)
- FixtureLoader class for loading JSON fixtures with automatic datetime parsing [file:tests/fixtures/__init__.py](tests/fixtures/__init__.py)
- Stock data fixtures: US market OHLCV, Chinese market OHLCV, standardized data [file:tests/fixtures/stock_data/](tests/fixtures/stock_data/)
diff --git a/migrations/versions/002_add_user_profile_fields.py b/migrations/versions/002_add_user_profile_fields.py
new file mode 100644
index 00000000..1dc908f0
--- /dev/null
+++ b/migrations/versions/002_add_user_profile_fields.py
@@ -0,0 +1,115 @@
+"""Add user profile fields - tax_jurisdiction, timezone, api_key_hash, is_verified
+
+Revision ID: 002
+Revises: 001
+Create Date: 2025-12-26 13:00:00.000000
+
+This migration adds four new fields to the users table:
+- tax_jurisdiction: Tax jurisdiction code (default: AU)
+- timezone: IANA timezone identifier (default: Australia/Sydney)
+- api_key_hash: Bcrypt hash of API key for programmatic access (nullable)
+- is_verified: Email verification status (default: False)
+
+All existing users will get default values for the new required fields.
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '002'
+down_revision: Union[str, None] = '001'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ """Add tax_jurisdiction, timezone, api_key_hash, and is_verified columns to users table.
+
+ For existing rows:
+ - tax_jurisdiction defaults to "AU"
+ - timezone defaults to "Australia/Sydney"
+ - api_key_hash is NULL
+ - is_verified is False
+ """
+ # Add tax_jurisdiction column
+ op.add_column(
+ 'users',
+ sa.Column(
+ 'tax_jurisdiction',
+ sa.String(length=10),
+ nullable=False,
+ server_default='AU',
+ comment='Tax jurisdiction code (e.g., US, US-CA, AU-NSW)'
+ )
+ )
+
+ # Add timezone column
+ op.add_column(
+ 'users',
+ sa.Column(
+ 'timezone',
+ sa.String(length=50),
+ nullable=False,
+ server_default='Australia/Sydney',
+ comment='IANA timezone identifier (e.g., America/New_York, UTC)'
+ )
+ )
+
+ # Add api_key_hash column with unique constraint and index
+ op.add_column(
+ 'users',
+ sa.Column(
+ 'api_key_hash',
+ sa.String(length=255),
+ nullable=True,
+ comment='Bcrypt hash of API key for programmatic access'
+ )
+ )
+ # Create unique constraint for api_key_hash
+ op.create_unique_constraint(
+ 'uq_users_api_key_hash',
+ 'users',
+ ['api_key_hash']
+ )
+ # Create index for fast lookups
+ op.create_index(
+ 'ix_users_api_key_hash',
+ 'users',
+ ['api_key_hash'],
+ unique=False
+ )
+
+ # Add is_verified column
+ op.add_column(
+ 'users',
+ sa.Column(
+ 'is_verified',
+ sa.Boolean(),
+ nullable=False,
+ server_default='0',
+ comment='Whether user email has been verified'
+ )
+ )
+
+
+def downgrade() -> None:
+ """Remove tax_jurisdiction, timezone, api_key_hash, and is_verified columns from users table.
+
+ WARNING: This will permanently delete data in these columns!
+ """
+ # Remove is_verified column
+ op.drop_column('users', 'is_verified')
+
+ # Remove api_key_hash column (drop index and constraint first)
+ op.drop_index('ix_users_api_key_hash', 'users')
+ op.drop_constraint('uq_users_api_key_hash', 'users', type_='unique')
+ op.drop_column('users', 'api_key_hash')
+
+ # Remove timezone column
+ op.drop_column('users', 'timezone')
+
+ # Remove tax_jurisdiction column
+ op.drop_column('users', 'tax_jurisdiction')
diff --git a/tests/api/conftest.py b/tests/api/conftest.py
index 6df62144..b9eac5dd 100644
--- a/tests/api/conftest.py
+++ b/tests/api/conftest.py
@@ -253,6 +253,8 @@ def test_user_data() -> Dict[str, Any]:
"email": "test@example.com",
"password": "SecurePassword123!",
"full_name": "Test User",
+ "timezone": "America/New_York", # Issue #3
+ "tax_jurisdiction": "US-NY", # Issue #3
}
@@ -269,6 +271,8 @@ def second_user_data() -> Dict[str, Any]:
"email": "other@example.com",
"password": "AnotherPassword456!",
"full_name": "Other User",
+ "timezone": "America/Los_Angeles", # Issue #3
+ "tax_jurisdiction": "US-CA", # Issue #3
}
@@ -667,3 +671,228 @@ def sample_xss_payloads() -> list[str]:
"
",
"