From f888aab2c92da87e0c0272aee02a9c9edbf8e094 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 27 Aug 2026 20:33:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=BF=81=E7=A7=BB=E4=B8=8E=20Passkey=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/dependencies/auth.py | 2 +- app/api/endpoints/mfa.py | 4 +- .../{identity_migration.py => migration.py} | 2 +- app/application/security/passkey.py | 72 +++++++++++++++++- app/application/security/passkeys.py | 75 ------------------- app/startup/initializers/modules.py | 2 +- app/startup/initializers/plugins.py | 2 +- docs/architecture-optimization-checklist.md | 2 +- docs/architecture-overview.md | 4 +- docs/rules/05-architecture.md | 2 +- .../architecture/dependency-baseline.json | 28 ++++--- .../test_plugin_identity_startup_migration.py | 2 +- 12 files changed, 94 insertions(+), 103 deletions(-) rename app/application/plugin/{identity_migration.py => migration.py} (99%) delete mode 100644 app/application/security/passkeys.py diff --git a/app/api/dependencies/auth.py b/app/api/dependencies/auth.py index 48e05fcb8..d9d5e18d5 100644 --- a/app/api/dependencies/auth.py +++ b/app/api/dependencies/auth.py @@ -14,7 +14,7 @@ from app.application.security.auth import ( AuthService, AuthUserRepository, ) -from app.application.security.passkeys import PasskeyRepository, PasskeyService +from app.application.security.passkey import PasskeyRepository, PasskeyService from app.application.security.user import ( AsyncUnitOfWork, UserRepository, diff --git a/app/api/endpoints/mfa.py b/app/api/endpoints/mfa.py index 5caf5404a..6bb917713 100644 --- a/app/api/endpoints/mfa.py +++ b/app/api/endpoints/mfa.py @@ -22,12 +22,10 @@ from app.application.security.otp import OtpUtils from app.application.security.passkey import ( PasskeyChallengeStore, PassKeyHelper, + PasskeyService, PassKeyRegistrationOriginMismatchError, PassKeyRegistrationVerificationError, ) -from app.application.security.passkeys import ( - PasskeyService, -) from app.application.security.token import verify_password from app.application.security.user import ( UserService, diff --git a/app/application/plugin/identity_migration.py b/app/application/plugin/migration.py similarity index 99% rename from app/application/plugin/identity_migration.py rename to app/application/plugin/migration.py index 2ab470c52..243d6e9f1 100644 --- a/app/application/plugin/identity_migration.py +++ b/app/application/plugin/migration.py @@ -1,4 +1,4 @@ -"""存量插件来源身份的一次性启动迁移。""" +"""存量插件来源身份的幂等启动迁移。""" from __future__ import annotations diff --git a/app/application/security/passkey.py b/app/application/security/passkey.py index b72c028e2..0efda7bdf 100644 --- a/app/application/security/passkey.py +++ b/app/application/security/passkey.py @@ -7,7 +7,7 @@ import json import secrets import threading from dataclasses import dataclass -from typing import Any, Dict, List, Literal, Optional, Tuple +from typing import Any, Dict, List, Literal, Optional, Protocol, Tuple from urllib.parse import urlparse from webauthn import ( @@ -451,3 +451,73 @@ class PassKeyHelper: except Exception as e: logger.error(f"验证认证响应失败: {e}") return False, credential_current_sign_count + + +class PasskeyRepository(Protocol): + """PassKey 用例需要的最小同步数据端口。""" + + def list(self) -> list[Any]: + """列出全部启用凭证。""" + + def list_by_user_id(self, user_id: int) -> list[Any]: + """列出指定用户凭证。""" + + def get_by_credential_id(self, credential_id: str) -> Optional[Any]: + """按凭证 ID 查找凭证。""" + + def create(self, payload: dict[str, Any]) -> Any: + """创建凭证。""" + + def update_last_used(self, passkey: Any, sign_count: int) -> bool: + """更新凭证使用计数。""" + + def delete_by_id(self, passkey_id: int, user_id: int) -> bool: + """删除用户凭证。""" + + +class PasskeyService: + """编排 PassKey 凭证生命周期。""" + + def __init__(self, repository: PasskeyRepository) -> None: + """注入 PassKey 数据端口。""" + self._repository = repository + + def list(self) -> list[Any]: + """列出全部启用凭证。""" + return self._repository.list() + + def list_by_user_id(self, user_id: int) -> list[Any]: + """列出指定用户凭证。""" + return self._repository.list_by_user_id(user_id) + + def get_by_credential_id(self, credential_id: str) -> Optional[Any]: + """按凭证 ID 查找凭证。""" + return self._repository.get_by_credential_id(credential_id) + + def create(self, payload: dict[str, Any]) -> Any: + """创建凭证。""" + return self._repository.create(payload) + + def update_last_used(self, passkey: Any, sign_count: int) -> bool: + """更新凭证使用计数。""" + return self._repository.update_last_used(passkey, sign_count) + + def delete_by_id(self, passkey_id: int, user_id: int) -> bool: + """删除用户凭证。""" + return self._repository.delete_by_id(passkey_id, user_id) + + +_configured_passkey_service: Optional[PasskeyService] = None + + +def configure_passkey_service(service: PasskeyService) -> None: + """由启动组合根登记 PassKey 应用服务。""" + global _configured_passkey_service + _configured_passkey_service = service + + +def get_configured_passkey_service() -> PasskeyService: + """返回启动阶段登记的 PassKey 应用服务。""" + if _configured_passkey_service is None: + raise RuntimeError("PassKey 服务尚未配置") + return _configured_passkey_service diff --git a/app/application/security/passkeys.py b/app/application/security/passkeys.py deleted file mode 100644 index 6120a5fc2..000000000 --- a/app/application/security/passkeys.py +++ /dev/null @@ -1,75 +0,0 @@ -"""PassKey 认证凭证应用服务。""" - -from __future__ import annotations - -from typing import Any, Optional, Protocol - - -class PasskeyRepository(Protocol): - """PassKey 用例需要的最小同步数据端口。""" - - def list(self) -> list[Any]: - """列出全部启用凭证。""" - - def list_by_user_id(self, user_id: int) -> list[Any]: - """列出指定用户凭证。""" - - def get_by_credential_id(self, credential_id: str) -> Optional[Any]: - """按凭证 ID 查找凭证。""" - - def create(self, payload: dict[str, Any]) -> Any: - """创建凭证。""" - - def update_last_used(self, passkey: Any, sign_count: int) -> bool: - """更新凭证使用计数。""" - - def delete_by_id(self, passkey_id: int, user_id: int) -> bool: - """删除用户凭证。""" - - -class PasskeyService: - """编排 PassKey 凭证生命周期。""" - - def __init__(self, repository: PasskeyRepository) -> None: - """注入 PassKey 数据端口。""" - self._repository = repository - - def list(self) -> list[Any]: - """列出全部启用凭证。""" - return self._repository.list() - - def list_by_user_id(self, user_id: int) -> list[Any]: - """列出指定用户凭证。""" - return self._repository.list_by_user_id(user_id) - - def get_by_credential_id(self, credential_id: str) -> Optional[Any]: - """按凭证 ID 查找凭证。""" - return self._repository.get_by_credential_id(credential_id) - - def create(self, payload: dict[str, Any]) -> Any: - """创建凭证。""" - return self._repository.create(payload) - - def update_last_used(self, passkey: Any, sign_count: int) -> bool: - """更新凭证使用计数。""" - return self._repository.update_last_used(passkey, sign_count) - - def delete_by_id(self, passkey_id: int, user_id: int) -> bool: - """删除用户凭证。""" - return self._repository.delete_by_id(passkey_id, user_id) - - -_configured_passkey_service: PasskeyService | None = None - - -def configure_passkey_service(service: PasskeyService) -> None: - """由启动组合根登记 PassKey 应用服务。""" - global _configured_passkey_service - _configured_passkey_service = service - - -def get_configured_passkey_service() -> PasskeyService: - """返回启动阶段登记的 PassKey 应用服务。""" - if _configured_passkey_service is None: - raise RuntimeError("PassKey 服务尚未配置") - return _configured_passkey_service diff --git a/app/startup/initializers/modules.py b/app/startup/initializers/modules.py index 7c1084c54..eee8d49e4 100644 --- a/app/startup/initializers/modules.py +++ b/app/startup/initializers/modules.py @@ -89,7 +89,7 @@ from app.application.outbox import ( ) from app.application.plugin.runtime import configure_plugin_runtime from app.application.security.auth import AuthService, build_superuser_token_payload, configure_auth_service -from app.application.security.passkeys import PasskeyService, configure_passkey_service +from app.application.security.passkey import PasskeyService, configure_passkey_service from app.application.security.url import close_image_proxy_block_log_coalescer from app.application.security.user import configure_user_lookups from app.application.security.userconfig import ( diff --git a/app/startup/initializers/plugins.py b/app/startup/initializers/plugins.py index 35702bd33..a428711e9 100644 --- a/app/startup/initializers/plugins.py +++ b/app/startup/initializers/plugins.py @@ -30,7 +30,7 @@ from app.application.plugin.identity import ( TrustedPluginSourceType, normalize_physical_plugin_id, ) -from app.application.plugin.identity_migration import ( +from app.application.plugin.migration import ( PluginIdentityMigrationService, configure_plugin_identity_migration, get_plugin_identity_migration, diff --git a/docs/architecture-optimization-checklist.md b/docs/architecture-optimization-checklist.md index f6eec7acd..41b26d173 100644 --- a/docs/architecture-optimization-checklist.md +++ b/docs/architecture-optimization-checklist.md @@ -69,7 +69,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain` | 指标 | 当前值 | 解释 | |---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 843 / 6,883 | `dependency-baseline.json` 当前快照 | +| 宿主 Python 模块 / 内部依赖边 | 842 / 6,882 | `dependency-baseline.json` 当前快照 | | 非平凡 SCC | 2 | 新增 Chain 包根环;另一个是隔离的 29 模块 TMDB 移植包环 | | 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | | Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index fe7d65b59..d63fe97a0 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -704,8 +704,8 @@ flowchart LR | 指标 | 当前值 | |---|---:| -| Python 模块 | 843 | -| 内部导入边 | 6,883 | +| Python 模块 | 842 | +| 内部导入边 | 6,882 | | 非平凡 SCC | 2(`ARCH-107` 临时 Chain 包根环;精确 containment 的 TMDB 移植包环) | | Direct egress | 66(12 条待迁移债务,54 条精确 containment) | | Module Contract V2 spec | 217(其中 215 个进入 `run_module` 观察面) | diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index a188bff09..a30323c1c 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -69,7 +69,7 @@ to make the directory tree look symmetrical. | `app/application/agentdata.py` | Named Agent data ports; canonical Agent consumers use `get_agent_*_port()` and do not alias legacy proxies to Oper classes | | `app/application/outbox.py` | Durable intent and Outbox repository/dispatcher contracts for post-commit side effects | | `app/application/transfer_execution.py` | Durable transfer execution contracts: stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs; contains no SQLAlchemy or external I/O | -| `app/application/plugin/` | Plugin market catalog, installation command, installed-plugin identity contract, runtime port, folder operations and dynamic-route use cases; filenames remain single words (`catalog.py`, `identity.py`, `install.py`, `runtime.py`, `folders.py`, `routes.py`) | +| `app/application/plugin/` | Plugin market catalog, installation command, installed-plugin identity contract and startup migration, runtime port, folder operations and dynamic-route use cases; filenames remain single words (`catalog.py`, `identity.py`, `migration.py`, `install.py`, `runtime.py`, `folders.py`, `routes.py`) | | `app/application/server/` | MoviePilot Server reporting and sharing use cases; local data readers and transport callbacks are injected by startup | | `app/application/site/` | Configured site catalog, authentication level and index-resource capability; the generated extension and its data bundle stay together here | | `app/application/messaging/` | Message rendering/routing, interactions and the Agent-to-message bridge: `ingress.py` owns the single channel-to-host loopback boundary; `interaction.py` shared interaction contracts and view helpers; `router.py` unified interaction priority and callback dispatch; `site.py`/`subscribe.py`/`skill.py` per-command sessions, input parsing and views; `media.py` media interaction state while the business workflow stays in `MediaInteractionChain`; `plugin.py` plugin input capture and plugin button callbacks; `agent.py` agent choice state, callback protocol and WebAgent bridge; `message.py` notification rendering, templates and queue. Not a public SDK recommended for direct plugin use | diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 6dc04d645..50c467a19 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -1441,8 +1441,8 @@ "runtime_only": true } }, - "edge_count": 6883, - "edge_sha256": "f44ae63f222eda2fc1b8ce805ab5138550a560bef6c266e3f2fae6338a243cd2", + "edge_count": 6882, + "edge_sha256": "602a73df30503fec4f4f01e28020222497cdadcad731dfedca40a3e4b1133c0d", "edges": [ "app -> app.runtime", "app -> app.runtime.compat", @@ -3003,7 +3003,7 @@ "app.api.dependencies.auth -> app.application", "app.api.dependencies.auth -> app.application.security", "app.api.dependencies.auth -> app.application.security.auth", - "app.api.dependencies.auth -> app.application.security.passkeys", + "app.api.dependencies.auth -> app.application.security.passkey", "app.api.dependencies.auth -> app.application.security.user", "app.api.dependencies.auth -> app.schemas", "app.api.dependencies.auth -> app.schemas.token", @@ -3468,7 +3468,6 @@ "app.api.endpoints.mfa -> app.application.security.auth", "app.api.endpoints.mfa -> app.application.security.otp", "app.api.endpoints.mfa -> app.application.security.passkey", - "app.api.endpoints.mfa -> app.application.security.passkeys", "app.api.endpoints.mfa -> app.application.security.token", "app.api.endpoints.mfa -> app.application.security.user", "app.api.endpoints.mfa -> app.runtime", @@ -4215,12 +4214,6 @@ "app.application.plugin.identity -> app.application", "app.application.plugin.identity -> app.application.plugin", "app.application.plugin.identity -> app.application.plugin.declaration", - "app.application.plugin.identity_migration -> app.application", - "app.application.plugin.identity_migration -> app.application.plugin", - "app.application.plugin.identity_migration -> app.application.plugin.identity", - "app.application.plugin.identity_migration -> app.application.plugin.source", - "app.application.plugin.identity_migration -> app.runtime", - "app.application.plugin.identity_migration -> app.runtime.log", "app.application.plugin.install -> app.application", "app.application.plugin.install -> app.application.plugin", "app.application.plugin.install -> app.application.plugin.admission", @@ -4239,6 +4232,12 @@ "app.application.plugin.inventory -> app.application.plugin.source", "app.application.plugin.inventory -> app.foundation", "app.application.plugin.inventory -> app.foundation.environment", + "app.application.plugin.migration -> app.application", + "app.application.plugin.migration -> app.application.plugin", + "app.application.plugin.migration -> app.application.plugin.identity", + "app.application.plugin.migration -> app.application.plugin.source", + "app.application.plugin.migration -> app.runtime", + "app.application.plugin.migration -> app.runtime.log", "app.application.plugin.recovery -> app.application", "app.application.plugin.recovery -> app.application.plugin", "app.application.plugin.recovery -> app.application.plugin.install", @@ -7935,7 +7934,7 @@ "app.startup.initializers.modules -> app.application.plugin.transaction", "app.startup.initializers.modules -> app.application.security", "app.startup.initializers.modules -> app.application.security.auth", - "app.startup.initializers.modules -> app.application.security.passkeys", + "app.startup.initializers.modules -> app.application.security.passkey", "app.startup.initializers.modules -> app.application.security.url", "app.startup.initializers.modules -> app.application.security.user", "app.startup.initializers.modules -> app.application.security.userconfig", @@ -8046,10 +8045,10 @@ "app.startup.initializers.plugins -> app.application.plugin.data", "app.startup.initializers.plugins -> app.application.plugin.gateway", "app.startup.initializers.plugins -> app.application.plugin.identity", - "app.startup.initializers.plugins -> app.application.plugin.identity_migration", "app.startup.initializers.plugins -> app.application.plugin.install", "app.startup.initializers.plugins -> app.application.plugin.inventory", "app.startup.initializers.plugins -> app.application.plugin.lifecycle", + "app.startup.initializers.plugins -> app.application.plugin.migration", "app.startup.initializers.plugins -> app.application.plugin.recovery", "app.startup.initializers.plugins -> app.application.plugin.routes", "app.startup.initializers.plugins -> app.application.plugin.runtime", @@ -8328,7 +8327,7 @@ "app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow.actions" ], - "module_count": 843, + "module_count": 842, "modules": [ "app", "app.adapters", @@ -8627,10 +8626,10 @@ "app.application.plugin.folders", "app.application.plugin.gateway", "app.application.plugin.identity", - "app.application.plugin.identity_migration", "app.application.plugin.install", "app.application.plugin.inventory", "app.application.plugin.lifecycle", + "app.application.plugin.migration", "app.application.plugin.recovery", "app.application.plugin.routes", "app.application.plugin.runtime", @@ -8647,7 +8646,6 @@ "app.application.security.cookie", "app.application.security.otp", "app.application.security.passkey", - "app.application.security.passkeys", "app.application.security.token", "app.application.security.twofactor", "app.application.security.url", diff --git a/tests/test_plugin_identity_startup_migration.py b/tests/test_plugin_identity_startup_migration.py index 7fbd72f12..c0b26c14f 100644 --- a/tests/test_plugin_identity_startup_migration.py +++ b/tests/test_plugin_identity_startup_migration.py @@ -18,7 +18,7 @@ from app.application.plugin.identity import ( PluginPayloadSourceType, TrustedPluginSourceType, ) -from app.application.plugin.identity_migration import PluginIdentityMigrationService +from app.application.plugin.migration import PluginIdentityMigrationService from app.application.plugin.source import ( CandidateInventory, LocalCandidateRead,