diff --git a/app/database.py b/app/database.py index 8cff2d6..59e20e1 100644 --- a/app/database.py +++ b/app/database.py @@ -84,6 +84,10 @@ def _migrate_assets_db() -> None: conn.execute(sa.text("ALTER TABLE assets ADD COLUMN renew_url VARCHAR")) if "cancel_url" not in cols: conn.execute(sa.text("ALTER TABLE assets ADD COLUMN cancel_url VARCHAR")) + if "account_id" not in cols: + conn.execute(sa.text("ALTER TABLE assets ADD COLUMN account_id INTEGER")) + # 资产账号引用从 name 字符串迁移到 account_id 外键(幂等) + _backfill_asset_account_id(conn) if insp.has_table("providers"): cols = {c["name"] for c in insp.get_columns("providers")} if "last_synced_at" not in cols: @@ -110,14 +114,24 @@ def _migrate_assets_db() -> None: conn.execute(sa.text("ALTER TABLE accounts ADD COLUMN last_synced_at DATETIME")) # 凭证下沉:平台 api_config 迁到默认账号;AI 资产自动挂靠账号 _backfill_account_credentials(conn) + # 合并存量 (platform,name) 重复账号,为建唯一索引做准备 + _dedupe_accounts(conn) def _migrate_indexes() -> None: """为已有数据库补充复合索引(create_all 不会为已存在的表补索引,IF NOT EXISTS 幂等)""" import sqlalchemy as sa + # 旧的全局 name 唯一索引(早期 Account.name unique=True 产物)会阻止跨平台同名, + # 与新的 (platform,name) 联合唯一冲突,需先删除 + with assets_engine.begin() as conn: + conn.execute(sa.text("DROP INDEX IF EXISTS ix_accounts_name")) + stmts = [ (assets_engine, "CREATE INDEX IF NOT EXISTS ix_assets_provider_ext_type ON assets (provider_id, external_id, asset_type)"), + (assets_engine, "CREATE INDEX IF NOT EXISTS ix_assets_account_id ON assets (account_id)"), + # (platform, name) 联合唯一:同邮箱可跨平台复用,同平台内不重名 + (assets_engine, "CREATE UNIQUE INDEX IF NOT EXISTS uq_accounts_platform_name ON accounts (platform, name)"), (metrics_engine, "CREATE INDEX IF NOT EXISTS ix_metric_points_asset_ts ON metric_points (asset_id, ts)"), (metrics_engine, "CREATE INDEX IF NOT EXISTS ix_security_checks_asset_ts ON security_checks (asset_id, ts)"), ] @@ -163,6 +177,80 @@ def _backfill_provider_services(conn) -> None: ) +def _dedupe_accounts(conn) -> None: + """建 (platform,name) 唯一索引前的防护:合并存量重复账号。 + + platform 统一按 COALESCE(platform,'') 规范化比较(避免 NULL 绕过)。 + 重复时保留 id 最小者,将其余行的资产引用(account_id/account)与凭证并入后删除。 + 当前数据量小,预期无重复;此函数仅在发现重复时产生写操作。 + """ + import sqlalchemy as sa + + rows = conn.execute( + sa.text( + "SELECT COALESCE(platform,'') AS p, name, COUNT(*) AS c, MIN(id) AS keep_id " + "FROM accounts GROUP BY p, name HAVING c > 1" + ) + ).fetchall() + for p, name, _c, keep_id in rows: + dups = conn.execute( + sa.text( + "SELECT id FROM accounts WHERE COALESCE(platform,'') = :p AND name = :n AND id != :keep" + ), + {"p": p, "n": name, "keep": keep_id}, + ).fetchall() + for (dup_id,) in dups: + # 资产引用并入保留行 + conn.execute( + sa.text("UPDATE assets SET account_id = :keep WHERE account_id = :dup"), + {"keep": keep_id, "dup": dup_id}, + ) + # 凭证:保留行为空时才从重复行拷贝 + conn.execute( + sa.text( + "UPDATE accounts SET " + "api_config_encrypted = COALESCE(api_config_encrypted, (SELECT api_config_encrypted FROM accounts WHERE id = :dup)), " + "login_password_encrypted = COALESCE(login_password_encrypted, (SELECT login_password_encrypted FROM accounts WHERE id = :dup)), " + "login_user = COALESCE(login_user, (SELECT login_user FROM accounts WHERE id = :dup)) " + "WHERE id = :keep" + ), + {"keep": keep_id, "dup": dup_id}, + ) + conn.execute(sa.text("DELETE FROM accounts WHERE id = :dup"), {"dup": dup_id}) + + +def _backfill_asset_account_id(conn) -> None: + """资产账号引用迁移(幂等):把 Asset.account 的 name 字符串引用回填为 account_id 外键。 + + 匹配规则:按 accounts.name 匹配;同名多个时优先 platform 与 asset.provider 一致者。 + 匹配不到(历史自由文本/未登记账号)则跳过并保留 account 字符串,供人工处理。 + """ + import sqlalchemy as sa + + rows = conn.execute( + sa.text( + "SELECT id, account, provider FROM assets " + "WHERE account_id IS NULL AND account IS NOT NULL AND account != ''" + ) + ).fetchall() + if not rows: + return + for asset_id, acc_name, provider in rows: + # 同名账号可能多个:优先 platform 与资产 provider 一致的 + cand = conn.execute( + sa.text( + "SELECT id, COALESCE(platform,'') FROM accounts WHERE name = :n " + "ORDER BY (COALESCE(platform,'') = :prov) DESC, id ASC" + ), + {"n": acc_name, "prov": provider or ""}, + ).fetchall() + if cand: + conn.execute( + sa.text("UPDATE assets SET account_id = :aid WHERE id = :id"), + {"aid": cand[0][0], "id": asset_id}, + ) + + def _backfill_account_credentials(conn) -> None: """凭证下沉迁移(幂等,仅在首次加列后产生效果): @@ -179,14 +267,21 @@ def _backfill_account_credentials(conn) -> None: from app.core.crypto import decrypt, encrypt def _ensure_account(name: str, platform: str) -> int: - row = conn.execute(sa.text("SELECT id FROM accounts WHERE name = :name"), {"name": name}).first() + # 按 (platform, name) 查,避免同名跨平台账号混淆 + row = conn.execute( + sa.text("SELECT id FROM accounts WHERE name = :name AND COALESCE(platform,'') = :platform"), + {"name": name, "platform": platform}, + ).first() if row: return row[0] conn.execute( sa.text("INSERT INTO accounts (name, platform, created_at) VALUES (:name, :platform, :ts)"), {"name": name, "platform": platform, "ts": datetime.now(timezone.utc).replace(tzinfo=None)}, ) - return conn.execute(sa.text("SELECT id FROM accounts WHERE name = :name"), {"name": name}).first()[0] + return conn.execute( + sa.text("SELECT id FROM accounts WHERE name = :name AND COALESCE(platform,'') = :platform"), + {"name": name, "platform": platform}, + ).first()[0] # 1. 平台凭证下沉到默认账号 for pid, slug, cfg in conn.execute( @@ -200,26 +295,27 @@ def _backfill_account_credentials(conn) -> None: conn.execute( sa.text( "UPDATE accounts SET api_config_encrypted = :cfg " - "WHERE name = :name AND (api_config_encrypted IS NULL OR api_config_encrypted = '')" + "WHERE name = :name AND COALESCE(platform,'') = :platform " + "AND (api_config_encrypted IS NULL OR api_config_encrypted = '')" ), - {"cfg": cfg, "name": acc_name}, + {"cfg": cfg, "name": acc_name, "platform": slug}, ) - # 2. 无账号的 AI 资产按 provider 挂靠 + # 2. 无账号的 AI 资产按 provider 挂靠(account_id 外键 + account 字符串兼容) rows = conn.execute( sa.text( "SELECT a.id, COALESCE(ai.provider, '') FROM assets a " "JOIN ai_accounts ai ON ai.asset_id = a.id " - "WHERE a.asset_type = 'ai_agent' AND (a.account IS NULL OR a.account = '')" + "WHERE a.asset_type = 'ai_agent' AND a.account_id IS NULL AND (a.account IS NULL OR a.account = '')" ) ).fetchall() for asset_id, provider in rows: if not provider: continue - _ensure_account(provider, provider) + acc_id = _ensure_account(provider, provider) conn.execute( - sa.text("UPDATE assets SET account = :acc WHERE id = :id"), - {"acc": provider, "id": asset_id}, + sa.text("UPDATE assets SET account = :acc, account_id = :aid WHERE id = :id"), + {"acc": provider, "aid": acc_id, "id": asset_id}, ) # 账号尚无 API 配置时,把该资产的 api_key 写入账号配置(解密后重组 JSON 再加密) key_row = conn.execute( @@ -235,9 +331,10 @@ def _backfill_account_credentials(conn) -> None: conn.execute( sa.text( "UPDATE accounts SET api_config_encrypted = :cfg " - "WHERE name = :name AND (api_config_encrypted IS NULL OR api_config_encrypted = '')" + "WHERE name = :name AND COALESCE(platform,'') = :platform " + "AND (api_config_encrypted IS NULL OR api_config_encrypted = '')" ), - {"cfg": cfg_enc, "name": provider}, + {"cfg": cfg_enc, "name": provider, "platform": provider}, ) diff --git a/app/models/asset.py b/app/models/asset.py index ddc0928..279e357 100644 --- a/app/models/asset.py +++ b/app/models/asset.py @@ -5,14 +5,14 @@ - VPSDetail: VPS 资产详情(与 Asset 一对一关联) - DomainDetail: 域名资产详情(与 Asset 一对一关联) - AIAccount: AI Agent 账号详情(与 Asset 一对一关联) -- Account: 平台账号字典(Asset.account 按名称引用) +- Account: 平台账号字典(Asset.account_id 外键引用) """ from datetime import date, datetime from enum import Enum from typing import Optional -from sqlalchemy import Index +from sqlalchemy import Index, UniqueConstraint from sqlmodel import Field, SQLModel from app.core.timeutils import utcnow @@ -65,7 +65,10 @@ class Asset(SQLModel, table=True): default=None, description="取消订阅网址(去哪取消)" ) account: Optional[str] = Field( - default=None, description="所属账号标识(如 Cloudflare 账号邮箱)" + default=None, description="所属账号标识(历史字段,迁移后由 account_id 取代,仅作迁移数据源)" + ) + account_id: Optional[int] = Field( + default=None, foreign_key="accounts.id", index=True, description="所属账号 ID(外键关联 accounts)" ) expiry_date: Optional[date] = Field( default=None, index=True, description="到期/续费日期,用于续费提醒" @@ -201,15 +204,18 @@ class CloudflareDetail(SQLModel, table=True): class Account(SQLModel, table=True): """平台账号字典:统一管理各平台下的账号(多账号场景区分归属) - 资产的 Asset.account 字段按名称引用本表,重命名时由服务层同步更新引用。 + 资产的 Asset.account_id 字段外键引用本表,重命名账号不影响资产归属。 + 唯一性:(platform, name) 联合唯一——同一邮箱/用户名可跨平台复用。 凭证层:登录用户名/密码(网站登录)+ API 配置 JSON(SDK 同步用), 平台本身不再持有凭证(Provider.api_config_encrypted 已弃用,仅留历史值)。 """ __tablename__ = "accounts" + # (platform, name) 联合唯一:同一邮箱/用户名可跨平台复用,同平台内不允许重名 + __table_args__ = (UniqueConstraint("platform", "name", name="uq_accounts_platform_name"),) id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(unique=True, index=True, description="账号标识(如邮箱/用户名)") + name: str = Field(index=True, description="账号标识(如邮箱/用户名)") platform: Optional[str] = Field( default=None, index=True, description="所属平台(slug 或名称)" ) diff --git a/app/routers/accounts.py b/app/routers/accounts.py index c8b09bd..827bc18 100644 --- a/app/routers/accounts.py +++ b/app/routers/accounts.py @@ -1,7 +1,7 @@ """平台账号 CRUD 路由 统一入口 /api/accounts,写操作(POST/PUT/DELETE)受 API Key 保护。 -资产的 Asset.account 按名称引用账号;重命名账号时服务层会同步更新引用资产。 +资产的 Asset.account_id 外键引用账号;删除账号时引用资产的 account_id 置 NULL。 凭证(登录密码/API 配置)存在账号上;账号维度的 SDK 测试/同步见 test/sync 端点。 """ diff --git a/app/schemas/asset.py b/app/schemas/asset.py index 2bdaca7..35bbc41 100644 --- a/app/schemas/asset.py +++ b/app/schemas/asset.py @@ -84,7 +84,7 @@ class AssetBase(SQLModel): renewal_cycle: Optional[str] = None renew_url: Optional[str] = None cancel_url: Optional[str] = None - account: Optional[str] = None + account_id: Optional[int] = None expiry_date: Optional[date] = None auto_renew: bool = False cost: float = 0 @@ -113,7 +113,7 @@ class AssetUpdate(SQLModel): renewal_cycle: Optional[str] = None renew_url: Optional[str] = None cancel_url: Optional[str] = None - account: Optional[str] = None + account_id: Optional[int] = None expiry_date: Optional[date] = None auto_renew: Optional[bool] = None cost: Optional[float] = None @@ -185,6 +185,7 @@ class AssetRead(AssetBase): updated_at: datetime days_to_expiry: Optional[int] = None provider_name: Optional[str] = None + account_name: Optional[str] = None # 账号标识(展示用,由 service 按 account_id 填充) vps_detail: Optional[VPSDetailRead] = None domain_detail: Optional[DomainDetailRead] = None ai_detail: Optional[AIAccountRead] = None diff --git a/app/services/account_service.py b/app/services/account_service.py index 408e3c8..b81e1f8 100644 --- a/app/services/account_service.py +++ b/app/services/account_service.py @@ -1,7 +1,7 @@ """平台账号业务逻辑 -账号与资产的关系:Asset.account 按名称引用账号(字符串,兼容历史自由文本)。 -重命名账号时同步更新所有引用资产,保证两边一致。 +账号与资产的关系:Asset.account_id 外键关联账号(重命名账号不影响引用)。 +唯一性:(platform, name) 联合唯一——同一邮箱/用户名可跨平台复用,同平台内不重名。 凭证层:登录密码与 API 配置加密存储,Read 仅返回布尔标记。 """ @@ -16,19 +16,19 @@ from app.models.asset import Account, Asset from app.schemas.account import AccountCreate, AccountRead, AccountUpdate -def _asset_counts(session: Session) -> Dict[str, int]: - """按 account 名称统计引用资产数""" +def _asset_counts(session: Session) -> Dict[int, int]: + """按 account_id 统计引用资产数""" rows = session.exec( - select(Asset.account, func.count(Asset.id)) - .where(Asset.account.is_not(None)) # type: ignore[union-attr] - .group_by(Asset.account) + select(Asset.account_id, func.count(Asset.id)) + .where(Asset.account_id.is_not(None)) # type: ignore[union-attr] + .group_by(Asset.account_id) ).all() - return {name: cnt for name, cnt in rows} + return {aid: cnt for aid, cnt in rows} -def _to_read(account: Account, counts: Dict[str, int]) -> AccountRead: +def _to_read(account: Account, counts: Dict[int, int]) -> AccountRead: read = AccountRead.model_validate(account) - read.asset_count = counts.get(account.name, 0) + read.asset_count = counts.get(account.id, 0) read.has_login_password = bool(account.login_password_encrypted) read.has_api_config = bool(account.api_config_encrypted) return read @@ -47,13 +47,20 @@ def _get_account(session: Session, account_id: int) -> Account: return account -def _check_name_taken(session: Session, name: str, exclude_id: int | None = None) -> None: - stmt = select(Account).where(Account.name == name) +def _norm_platform(platform: str | None) -> str: + """platform 规范化为非空字符串(避免 NULL 绕过 (platform,name) 唯一约束)""" + return (platform or "").strip() + + +def _check_name_taken(session: Session, name: str, platform: str, exclude_id: int | None = None) -> None: + """校验 (platform, name) 联合唯一:同平台内不允许重名,跨平台可复用""" + stmt = select(Account).where(Account.name == name, Account.platform == platform) if exclude_id is not None: stmt = stmt.where(Account.id != exclude_id) if session.exec(stmt).first(): raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=f"账号已存在:{name}" + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"该平台下账号已存在:{name}", ) @@ -61,8 +68,9 @@ def create_account(session: Session, data: AccountCreate) -> AccountRead: name = data.name.strip() if not name: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="账号名称不能为空") - _check_name_taken(session, name) - account = Account(name=name, platform=data.platform, remark=data.remark, login_user=data.login_user) + platform = _norm_platform(data.platform) + _check_name_taken(session, name, platform) + account = Account(name=name, platform=platform, remark=data.remark, login_user=data.login_user) account.login_password_encrypted = crypto.encrypt(data.login_password) account.api_config_encrypted = crypto.encrypt(data.api_config) session.add(account) @@ -73,20 +81,17 @@ def create_account(session: Session, data: AccountCreate) -> AccountRead: def update_account(session: Session, account_id: int, data: AccountUpdate) -> AccountRead: account = _get_account(session, account_id) + # platform 可能随本次更新变化,校验重名时用更新后的值 + new_platform = _norm_platform(data.platform) if data.platform is not None else _norm_platform(account.platform) if data.name is not None: new_name = data.name.strip() if not new_name: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="账号名称不能为空") - if new_name != account.name: - _check_name_taken(session, new_name, exclude_id=account.id) - # 同步更新引用该账号的资产,避免重命名后资产端失联 - assets = session.exec(select(Asset).where(Asset.account == account.name)).all() - for a in assets: - a.account = new_name - session.add(a) + if new_name != account.name or new_platform != _norm_platform(account.platform): + _check_name_taken(session, new_name, new_platform, exclude_id=account.id) account.name = new_name if data.platform is not None: - account.platform = data.platform or None + account.platform = new_platform if data.remark is not None: account.remark = data.remark or None if data.login_user is not None: @@ -104,10 +109,14 @@ def update_account(session: Session, account_id: int, data: AccountUpdate) -> Ac def delete_account(session: Session, account_id: int) -> Dict[str, int]: account = _get_account(session, account_id) - affected = len(session.exec(select(Asset.id).where(Asset.account == account.name)).all()) + # 引用该账号的资产:account_id 置 NULL(保留资产,仅解除关联) + assets = session.exec(select(Asset).where(Asset.account_id == account_id)).all() + for a in assets: + a.account_id = None + session.add(a) + affected = len(assets) session.delete(account) session.commit() - # 资产端保留原账号名文本(不级联清空),由用户自行处理 return {"affected_assets": affected} diff --git a/app/services/asset_service.py b/app/services/asset_service.py index f069f7c..da4f9e7 100644 --- a/app/services/asset_service.py +++ b/app/services/asset_service.py @@ -12,6 +12,7 @@ from sqlmodel import Session, select from app.core import crypto from app.models.asset import ( + Account, AIAccount, Asset, AssetStatus, @@ -93,11 +94,29 @@ def _provider_name(session: Session, asset: Asset) -> Optional[str]: return None +def _account_name(session: Session, asset: Asset) -> Optional[str]: + """取关联账号标识(展示用)""" + if asset.account_id: + account = session.get(Account, asset.account_id) + if account: + return account.name + return None + + +def _validate_account(session: Session, account_id: Optional[int]) -> None: + """account_id 非空时确认账号存在,防外键悬空""" + if account_id is not None and not session.get(Account, account_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=f"账号不存在:id={account_id}" + ) + + def _to_read(session: Session, asset: Asset, detail) -> AssetRead: """组装 AssetRead 输出(主表 + 详情 + 计算字段)""" read = AssetRead.model_validate(asset) read.days_to_expiry = _days_to_expiry(asset.expiry_date) read.provider_name = _provider_name(session, asset) + read.account_name = _account_name(session, asset) if isinstance(detail, VPSDetail): read.vps_detail = _detail_to_read(detail) elif isinstance(detail, DomainDetail): @@ -156,6 +175,12 @@ def _get_details_batch(session: Session, assets: list) -> dict: if provider_ids: for p in session.exec(select(Provider).where(Provider.id.in_(provider_ids))).all(): provider_map[p.id] = p.name + # 批量查 Account 名 + account_ids = {a.account_id for a in assets if a.account_id} + account_map = {} + if account_ids: + for acc in session.exec(select(Account).where(Account.id.in_(account_ids))).all(): + account_map[acc.id] = acc.name # 批量查各类型详情 detail_map = {} for asset_type, (_, model) in DETAIL_MAP.items(): @@ -163,7 +188,7 @@ def _get_details_batch(session: Session, assets: list) -> dict: if typed_ids: for d in session.exec(select(model).where(model.asset_id.in_(typed_ids))).all(): detail_map[d.asset_id] = d - return {"providers": provider_map, "details": detail_map} + return {"providers": provider_map, "accounts": account_map, "details": detail_map} def _to_read_batch(session: Session, asset: Asset, batch: dict) -> AssetRead: @@ -171,6 +196,7 @@ def _to_read_batch(session: Session, asset: Asset, batch: dict) -> AssetRead: read = AssetRead.model_validate(asset) read.days_to_expiry = _days_to_expiry(asset.expiry_date) read.provider_name = batch["providers"].get(asset.provider_id) + read.account_name = batch.get("accounts", {}).get(asset.account_id) detail = batch["details"].get(asset.id) if isinstance(detail, VPSDetail): read.vps_detail = _detail_to_read(detail) @@ -203,6 +229,7 @@ def _create_asset_no_commit(session: Session, data: AssetCreate) -> Asset: asset_data = data.model_dump( exclude={"vps_detail", "domain_detail", "ai_detail", "cloudflare_detail"} ) + _validate_account(session, asset_data.get("account_id")) asset = Asset(**asset_data) session.add(asset) session.flush() # 获取 asset.id,但不提交 @@ -239,6 +266,8 @@ def _update_asset_no_commit(session: Session, asset_id: int, data: AssetUpdate) main_fields = data.model_dump( exclude_unset=True, exclude={"vps_detail", "domain_detail", "ai_detail", "cloudflare_detail"} ) + if "account_id" in main_fields: + _validate_account(session, main_fields["account_id"]) for key, value in main_fields.items(): setattr(asset, key, value) session.add(asset) diff --git a/app/services/sync_service.py b/app/services/sync_service.py index 67c18be..53016fb 100644 --- a/app/services/sync_service.py +++ b/app/services/sync_service.py @@ -169,7 +169,7 @@ def _find_asset(session: Session, provider_id: int, external_id: str, asset_type ).first() -def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter, account_name: Optional[str] = None) -> dict: +def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter, account_id: Optional[int] = None) -> dict: created = updated = 0 for vps in adapter.list_vps(): existing = _find_asset(session, provider.id, vps.external_id, AssetType.VPS) @@ -179,8 +179,8 @@ def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter, accoun if vps.monthly_cost is not None: existing.cost = vps.monthly_cost existing.currency = vps.currency - if account_name: - existing.account = account_name + if account_id: + existing.account_id = account_id session.add(existing) detail = session.exec( select(VPSDetail).where(VPSDetail.asset_id == existing.id) @@ -201,7 +201,7 @@ def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter, accoun provider=provider.slug, provider_id=provider.id, external_id=vps.external_id, - account=account_name, + account_id=account_id, status=_norm_status(vps.status), cost=vps.monthly_cost or 0, currency=vps.currency, @@ -224,7 +224,7 @@ def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter, accoun return {"created": created, "updated": updated} -def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter, account_name: Optional[str] = None) -> dict: +def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter, account_id: Optional[int] = None) -> dict: created = updated = 0 for dom in adapter.list_domains(): existing = _find_asset(session, provider.id, dom.external_id, AssetType.DOMAIN) @@ -232,8 +232,8 @@ def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter, ac existing.status = _norm_status(dom.status) if dom.expiry_date: existing.expiry_date = dom.expiry_date - if account_name: - existing.account = account_name + if account_id: + existing.account_id = account_id session.add(existing) detail = session.exec( select(DomainDetail).where(DomainDetail.asset_id == existing.id) @@ -250,7 +250,7 @@ def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter, ac provider=provider.slug, provider_id=provider.id, external_id=dom.external_id, - account=account_name, + account_id=account_id, status=_norm_status(dom.status), expiry_date=dom.expiry_date, ) @@ -295,12 +295,12 @@ def sync_account(session: Session, account_id: int) -> dict: if caps["list_vps"]: try: - result["vps"] = _sync_vps(session, provider, adapter, account.name) + result["vps"] = _sync_vps(session, provider, adapter, account.id) except Exception as e: # noqa: BLE001 result["vps_error"] = str(e) if caps["list_domains"]: try: - result["domains"] = _sync_domains(session, provider, adapter, account.name) + result["domains"] = _sync_domains(session, provider, adapter, account.id) except Exception as e: # noqa: BLE001 result["domains_error"] = str(e) if caps["get_account"]: diff --git a/static/js/modals.js b/static/js/modals.js index 9a91b2f..1355c4a 100644 --- a/static/js/modals.js +++ b/static/js/modals.js @@ -41,10 +41,10 @@ const AssetModal = { +