From 21167659f8e53e985b3f100d3323f5f1b4b2595e Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 3 Apr 2026 01:23:42 +0800 Subject: [PATCH] perf: disable read-time sync and keep provider sync manual --- backend/app/api/admin_catalog.py | 5 -- backend/app/services/idp_catalog_sync.py | 100 +++++++++++++++++++++++ docs/ARCHITECTURE.md | 3 +- docs/FRONTEND_HANDOFF.md | 1 + docs/LOCAL_DEV_RUNBOOK.md | 1 + 5 files changed, 104 insertions(+), 6 deletions(-) diff --git a/backend/app/api/admin_catalog.py b/backend/app/api/admin_catalog.py index 1708d8c..b050e03 100644 --- a/backend/app/api/admin_catalog.py +++ b/backend/app/api/admin_catalog.py @@ -138,7 +138,6 @@ def list_companies( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_provider(db) repo = CompaniesRepository(db) items, total = repo.list(keyword=keyword, limit=limit, offset=offset) return ListResponse(items=[_company_item(i) for i in items], total=total, limit=limit, offset=offset) @@ -231,7 +230,6 @@ def list_sites( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_provider(db) companies_repo = CompaniesRepository(db) sites_repo = SitesRepository(db) company_id = None @@ -356,7 +354,6 @@ def list_systems( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_provider(db) repo = SystemsRepository(db) items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset) return ListResponse(items=[_system_item(i) for i in items], total=total, limit=limit, offset=offset) @@ -386,7 +383,6 @@ def list_roles( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_provider(db) systems_repo = SystemsRepository(db) roles_repo = RolesRepository(db) @@ -679,7 +675,6 @@ def list_members( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_provider(db) repo = UsersRepository(db) rows, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset) return ListResponse(items=[_member_item(r) for r in rows], total=total, limit=limit, offset=offset) diff --git a/backend/app/services/idp_catalog_sync.py b/backend/app/services/idp_catalog_sync.py index cfad473..5b50755 100644 --- a/backend/app/services/idp_catalog_sync.py +++ b/backend/app/services/idp_catalog_sync.py @@ -31,6 +31,7 @@ BUILTIN_CLIENT_IDS = { _sync_lock = threading.Lock() _last_synced_at = 0.0 +_last_systems_synced_at = 0.0 _min_sync_interval_sec = 30.0 @@ -304,3 +305,102 @@ def sync_from_provider(db: Session, *, force: bool = False) -> dict[str, int]: } finally: _sync_lock.release() + + +def sync_systems_from_provider(db: Session, *, force: bool = False) -> dict[str, int]: + global _last_systems_synced_at + now = time.time() + if not force and now - _last_systems_synced_at < _min_sync_interval_sec: + return {"synced": 0} + + if not _sync_lock.acquire(blocking=False): + return {"synced": 0} + + try: + now = time.time() + if not force and now - _last_systems_synced_at < _min_sync_interval_sec: + return {"synced": 0} + + idp = ProviderAdminService(get_settings()) + systems_repo = SystemsRepository(db) + roles_repo = RolesRepository(db) + + systems_created = 0 + systems_updated = 0 + roles_created = 0 + roles_updated = 0 + + client_rows = idp.list_clients() + for client in client_rows: + client_uuid = str(client.get("id", "")).strip() + client_id = str(client.get("clientId", "")).strip() + if not client_uuid or not client_id: + continue + if client_id in BUILTIN_CLIENT_IDS: + continue + + system = db.scalar(select(System).where(System.provider_client_id == client_id)) + system_name = str(client.get("name", "")).strip() or client_id + system_status = "active" if client.get("enabled", True) else "inactive" + if system is None: + system_key = _generate_unique_key("SY", lambda key: systems_repo.get_by_key(key) is not None) + system = systems_repo.create( + system_key=system_key, + name=system_name, + provider_client_id=client_id, + status=system_status, + ) + systems_created += 1 + else: + system = systems_repo.update( + system, + name=system_name, + status=system_status, + ) + systems_updated += 1 + + client_roles = idp.list_client_roles(client_uuid) + for role_row in client_roles: + if not isinstance(role_row, dict): + continue + role_name = str(role_row.get("name", "")).strip() + if not role_name: + continue + role_desc = str(role_row.get("description", "")).strip() or None + role_status = "active" if not role_row.get("composite", False) else "active" + role = db.scalar( + select(Role).where( + Role.system_id == system.id, + Role.provider_role_name == role_name, + ) + ) + if role is None: + role_key = _generate_unique_key("RL", lambda key: roles_repo.get_by_key(key) is not None) + roles_repo.create( + role_key=role_key, + system_id=system.id, + name=role_name, + description=role_desc, + provider_role_name=role_name, + status=role_status, + ) + roles_created += 1 + else: + roles_repo.update( + role, + name=role_name, + description=role_desc, + status=role_status, + ) + roles_updated += 1 + + _last_systems_synced_at = time.time() + return { + "synced": 1, + "systems_created": systems_created, + "systems_updated": systems_updated, + "roles_created": roles_created, + "roles_updated": roles_updated, + } + finally: + _sync_lock.release() diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 665526b..ccfced4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -26,7 +26,8 @@ - 群組階層:`Company Group -> Site SubGroup`。 - 系統角色:以 Keycloak client role 表示,對應 DB `roles`。 - `site_roles` 代表某 Site 擁有的 Keycloak role 集合。 -- 補齊策略:若 Keycloak 有、DB 沒有,後台同步流程會自動 upsert 到 DB。 +- 同步策略改為手動觸發:不在列表讀取 (`R`) 時自動同步。 +- 補齊策略:僅在手動同步按鈕(`POST /admin/sync/from-provider`)或 CUD 流程時同步。 - 使用者加入 Site 時,透過同步邏輯使其在 IdP 端取得對應角色能力。 ## 後台安全線 diff --git a/docs/FRONTEND_HANDOFF.md b/docs/FRONTEND_HANDOFF.md index 2befc8a..d4630df 100644 --- a/docs/FRONTEND_HANDOFF.md +++ b/docs/FRONTEND_HANDOFF.md @@ -18,6 +18,7 @@ - 欄位:`system_key`, `name`, `provider_client_id`, `status` - 系統詳情需顯示底下 `roles` 列表 - 建立/修改/刪除在 Keycloak 處理,member 後台提供「同步 Keycloak」按鈕 +- 所有資料列表頁不自動同步;需由使用者按下「同步」按鈕才觸發。 4. 角色管理(DB 關聯為主) - 欄位:`role_key`, `system_key`, `name`, `description`, `provider_role_name`, `status` diff --git a/docs/LOCAL_DEV_RUNBOOK.md b/docs/LOCAL_DEV_RUNBOOK.md index 8839f7a..a215c2c 100644 --- a/docs/LOCAL_DEV_RUNBOOK.md +++ b/docs/LOCAL_DEV_RUNBOOK.md @@ -59,6 +59,7 @@ npm run dev 3. `GET /me` 登入後應有資料。 4. 非 admin 群組帳號打 `/admin/*` 應為 403。 5. `POST /admin/sync/from-provider?force=true` 可手動觸發全量補齊同步。 +6. 列表 API 不會自動同步 IdP(避免高負載),需手動按同步按鈕或呼叫同步 API。 ## 6) 新模型驗收路徑 1. 新增 Company、Site。