diff --git a/backend/alembic/versions/1490781700bc_backfill_default_tree_author_id_to_.py b/backend/alembic/versions/1490781700bc_backfill_default_tree_author_id_to_.py index 8d42e44c..8b093f14 100644 --- a/backend/alembic/versions/1490781700bc_backfill_default_tree_author_id_to_.py +++ b/backend/alembic/versions/1490781700bc_backfill_default_tree_author_id_to_.py @@ -13,7 +13,6 @@ from typing import Sequence, Union from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import UUID as PG_UUID import uuid @@ -25,18 +24,40 @@ depends_on: Union[str, Sequence[str], None] = None SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com" SERVICE_ACCOUNT_NAME = "ResolutionFlow" +SYSTEM_ACCOUNT_NAME = "ResolutionFlow System" +SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1" def upgrade() -> None: conn = op.get_bind() - # Ensure service account exists + # Check if service user already exists row = conn.execute( - sa.text("SELECT id FROM users WHERE email = :email"), + sa.text("SELECT id, account_id FROM users WHERE email = :email"), {"email": SERVICE_ACCOUNT_EMAIL}, ).fetchone() - if row is None: + if row is not None: + service_id = str(row[0]) + else: + # Ensure a system account exists for the service user + acct_row = conn.execute( + sa.text("SELECT id FROM accounts WHERE display_code = :code"), + {"code": SYSTEM_ACCOUNT_DISPLAY_CODE}, + ).fetchone() + + if acct_row is None: + account_id = str(uuid.uuid4()) + conn.execute( + sa.text(""" + INSERT INTO accounts (id, name, display_code, created_at, updated_at) + VALUES (:id, :name, :code, NOW(), NOW()) + """), + {"id": account_id, "name": SYSTEM_ACCOUNT_NAME, "code": SYSTEM_ACCOUNT_DISPLAY_CODE}, + ) + else: + account_id = str(acct_row[0]) + service_id = str(uuid.uuid4()) conn.execute( sa.text(""" @@ -44,12 +65,12 @@ def upgrade() -> None: id, email, name, password_hash, role, is_super_admin, is_team_admin, is_active, is_service_account, must_change_password, - account_role, created_at + account_id, account_role, created_at ) VALUES ( :id, :email, :name, :password_hash, 'engineer', false, false, true, true, false, - 'engineer', NOW() + :account_id, 'engineer', NOW() ) """), { @@ -57,10 +78,9 @@ def upgrade() -> None: "email": SERVICE_ACCOUNT_EMAIL, "name": SERVICE_ACCOUNT_NAME, "password_hash": "!service-account-no-login", + "account_id": account_id, }, ) - else: - service_id = str(row[0]) # Backfill is_default trees that have no author result = conn.execute( @@ -75,7 +95,6 @@ def upgrade() -> None: def downgrade() -> None: - # Restore NULL on trees that were authored by the service account and are default conn = op.get_bind() row = conn.execute( sa.text("SELECT id FROM users WHERE email = :email"), diff --git a/backend/app/core/service_account.py b/backend/app/core/service_account.py index d16e91c8..a2175981 100644 --- a/backend/app/core/service_account.py +++ b/backend/app/core/service_account.py @@ -18,6 +18,31 @@ logger = logging.getLogger(__name__) SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com" SERVICE_ACCOUNT_NAME = "ResolutionFlow" +SYSTEM_ACCOUNT_NAME = "ResolutionFlow System" +SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1" + + +async def _ensure_system_account(db: AsyncSession) -> uuid.UUID: + """Get or create the ResolutionFlow system account. Returns its ID.""" + from app.models.account import Account + from sqlalchemy import text + + result = await db.execute( + select(Account).where(Account.display_code == SYSTEM_ACCOUNT_DISPLAY_CODE) + ) + account = result.scalar_one_or_none() + if account is not None: + return account.id + + new_account = Account( + id=uuid.uuid4(), + name=SYSTEM_ACCOUNT_NAME, + display_code=SYSTEM_ACCOUNT_DISPLAY_CODE, + ) + db.add(new_account) + await db.flush() + logger.info(f"[service_account] Created system account (id={new_account.id})") + return new_account.id async def ensure_service_account(db: AsyncSession) -> uuid.UUID: @@ -40,7 +65,8 @@ async def ensure_service_account(db: AsyncSession) -> uuid.UUID: await db.commit() return user.id - # Create the service account with a random, unusable password hash + account_id = await _ensure_system_account(db) + new_user = User( id=uuid.uuid4(), email=SERVICE_ACCOUNT_EMAIL, @@ -52,6 +78,7 @@ async def ensure_service_account(db: AsyncSession) -> uuid.UUID: is_active=True, is_service_account=True, must_change_password=False, + account_id=account_id, account_role="engineer", ) db.add(new_user)