huace-aigc-auth-client 1.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- huace_aigc_auth_client/__init__.py +101 -0
- huace_aigc_auth_client/legacy_adapter.py +625 -0
- huace_aigc_auth_client/sdk.py +726 -0
- huace_aigc_auth_client/webhook.py +128 -0
- huace_aigc_auth_client-1.1.7.dist-info/METADATA +797 -0
- huace_aigc_auth_client-1.1.7.dist-info/RECORD +9 -0
- huace_aigc_auth_client-1.1.7.dist-info/WHEEL +5 -0
- huace_aigc_auth_client-1.1.7.dist-info/licenses/LICENSE +22 -0
- huace_aigc_auth_client-1.1.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIGC Auth Legacy System Adapter
|
|
3
|
+
|
|
4
|
+
提供旧系统接入支持,包括:
|
|
5
|
+
1. 字段映射配置
|
|
6
|
+
2. 用户数据同步
|
|
7
|
+
3. 密码处理策略
|
|
8
|
+
4. Webhook 推送支持
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import hmac
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import requests
|
|
17
|
+
from typing import Optional, List, Dict, Any, Callable, TypeVar, Generic
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PasswordMode(Enum):
|
|
26
|
+
"""密码处理模式"""
|
|
27
|
+
UNIFIED = "unified" # 统一初始密码
|
|
28
|
+
CUSTOM_MAPPING = "custom_mapping" # 自定义映射函数
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SyncDirection(Enum):
|
|
32
|
+
"""同步方向"""
|
|
33
|
+
AUTH_TO_LEGACY = "auth_to_legacy" # aigc-auth → 旧系统
|
|
34
|
+
LEGACY_TO_AUTH = "legacy_to_auth" # 旧系统 → aigc-auth
|
|
35
|
+
BIDIRECTIONAL = "bidirectional" # 双向同步
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class FieldMapping:
|
|
40
|
+
"""字段映射配置"""
|
|
41
|
+
auth_field: str # aigc-auth 字段名
|
|
42
|
+
legacy_field: str # 旧系统字段名
|
|
43
|
+
transform_to_legacy: Optional[Callable[[Any], Any]] = None # auth → legacy 转换函数
|
|
44
|
+
transform_to_auth: Optional[Callable[[Any], Any]] = None # legacy → auth 转换函数
|
|
45
|
+
required: bool = False # 是否必填
|
|
46
|
+
default_value: Any = None # 默认值
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class SyncConfig:
|
|
51
|
+
"""同步配置"""
|
|
52
|
+
# 基本配置
|
|
53
|
+
enabled: bool = True
|
|
54
|
+
direction: SyncDirection = SyncDirection.AUTH_TO_LEGACY
|
|
55
|
+
|
|
56
|
+
# 字段映射
|
|
57
|
+
field_mappings: List[FieldMapping] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
# 唯一标识字段(用于匹配用户)
|
|
60
|
+
unique_field: str = "username"
|
|
61
|
+
|
|
62
|
+
# 密码处理
|
|
63
|
+
password_mode: PasswordMode = PasswordMode.UNIFIED
|
|
64
|
+
unified_password: str = "Abc@123456" # 统一初始密码
|
|
65
|
+
password_mapper: Optional[Callable[[Dict], str]] = None # 自定义密码映射函数(接收用户数据字典)
|
|
66
|
+
password_is_hashed: bool = False # password_mapper 返回的是否已经是加密后的密码
|
|
67
|
+
|
|
68
|
+
# Webhook 配置
|
|
69
|
+
webhook_enabled: bool = False
|
|
70
|
+
webhook_url: Optional[str] = None
|
|
71
|
+
webhook_secret: Optional[str] = None
|
|
72
|
+
webhook_retry_count: int = 3
|
|
73
|
+
webhook_timeout: int = 10
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class LegacyUserData:
|
|
78
|
+
"""旧系统用户数据"""
|
|
79
|
+
data: Dict[str, Any]
|
|
80
|
+
|
|
81
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
82
|
+
return self.data.get(key, default)
|
|
83
|
+
|
|
84
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
85
|
+
return self.data.copy()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class SyncResult:
|
|
90
|
+
"""同步结果"""
|
|
91
|
+
success: bool
|
|
92
|
+
user_id: Optional[int] = None
|
|
93
|
+
auth_user_id: Optional[int] = None
|
|
94
|
+
legacy_user_id: Optional[Any] = None
|
|
95
|
+
message: str = ""
|
|
96
|
+
errors: List[str] = field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class LegacySystemAdapter(ABC):
|
|
100
|
+
"""
|
|
101
|
+
旧系统适配器抽象基类
|
|
102
|
+
|
|
103
|
+
接入系统需要继承此类并实现以下方法:
|
|
104
|
+
- get_user_by_unique_field: 通过唯一字段获取用户
|
|
105
|
+
- create_user: 创建用户
|
|
106
|
+
- update_user: 更新用户(可选)
|
|
107
|
+
- get_all_users: 获取所有用户(用于初始化同步)
|
|
108
|
+
- handle_webhook: 处理 webhook 通知(可选,异步方法)
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, sync_config: SyncConfig, auth_client=None):
|
|
112
|
+
"""
|
|
113
|
+
初始化适配器
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
sync_config: 同步配置
|
|
117
|
+
auth_client: AigcAuthClient 实例(可选,用于批量同步)
|
|
118
|
+
"""
|
|
119
|
+
self.config = sync_config
|
|
120
|
+
self.auth_client = auth_client
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def get_user_by_unique_field(self, value: Any) -> Optional[LegacyUserData]:
|
|
124
|
+
"""通过唯一字段获取旧系统用户"""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def create_user(self, user_data: Dict[str, Any]) -> Optional[Any]:
|
|
129
|
+
"""在旧系统创建用户,返回用户ID"""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def update_user(self, unique_value: Any, user_data: Dict[str, Any]) -> bool:
|
|
133
|
+
"""更新旧系统用户(可选实现)"""
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def get_all_users(self) -> List[LegacyUserData]:
|
|
137
|
+
"""获取所有旧系统用户(用于初始化同步)"""
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
async def get_user_by_username_async(self, username: str) -> Optional[LegacyUserData]:
|
|
142
|
+
"""异步获取用户(子类必须实现)
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
username: 用户名
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Optional[LegacyUserData]: 用户数据,不存在返回 None
|
|
149
|
+
"""
|
|
150
|
+
raise NotImplementedError("Subclass must implement get_user_by_username_async method")
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
async def _create_user_async(self, user_data: Dict[str, Any]) -> Optional[Any]:
|
|
154
|
+
"""异步创建用户(子类必须实现)
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
user_data: 用户数据字典
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Optional[Any]: 创建的用户 ID 或其他标识
|
|
161
|
+
"""
|
|
162
|
+
raise NotImplementedError("Subclass must implement _create_user_async method")
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
async def _update_user_async(self, username: str, user_data: Dict[str, Any]) -> bool:
|
|
166
|
+
"""异步更新用户(子类必须实现)
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
username: 用户名
|
|
170
|
+
user_data: 要更新的用户数据
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
bool: 更新成功返回 True
|
|
174
|
+
"""
|
|
175
|
+
raise NotImplementedError("Subclass must implement _update_user_async method")
|
|
176
|
+
|
|
177
|
+
@abstractmethod
|
|
178
|
+
async def _delete_user_async(self, username: str) -> bool:
|
|
179
|
+
"""异步删除用户(子类必须实现)
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
username: 用户名
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
bool: 删除成功返回 True
|
|
186
|
+
"""
|
|
187
|
+
raise NotImplementedError("Subclass must implement _delete_user_async method")
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
async def get_all_users_async(self) -> List[LegacyUserData]:
|
|
191
|
+
"""获取所有用户(子类必须实现,用于批量同步)
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List[LegacyUserData]: 所有用户数据列表
|
|
195
|
+
"""
|
|
196
|
+
raise NotImplementedError("Subclass must implement get_all_users_async method")
|
|
197
|
+
|
|
198
|
+
async def upsert_user_async(self, user_data: Dict[str, Any], auth_data: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
199
|
+
"""异步创建或更新用户(存在则更新,不存在则新增)
|
|
200
|
+
|
|
201
|
+
这是一个默认实现,子类可以选择性覆盖以优化性能。
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
user_data: 用户数据字典(必须包含 username)
|
|
205
|
+
auth_data: 鉴权系统用户数据字典(可选)
|
|
206
|
+
Returns:
|
|
207
|
+
Dict: 操作结果 {"created": bool, "user_id": Any}
|
|
208
|
+
"""
|
|
209
|
+
username = user_data.get("username")
|
|
210
|
+
if not username:
|
|
211
|
+
raise ValueError("username is required for upsert operation")
|
|
212
|
+
|
|
213
|
+
# 检查用户是否存在
|
|
214
|
+
existing = await self.get_user_by_username_async(username)
|
|
215
|
+
|
|
216
|
+
if existing:
|
|
217
|
+
# 用户存在,执行更新
|
|
218
|
+
if not auth_data or auth_data.get("updatedFields") is None or len(auth_data.get("updatedFields")) == 0:
|
|
219
|
+
# 如果没有提供 auth_data 或 updatedFields,则不更新
|
|
220
|
+
return {"created": False, "user_id": existing.get("id")}
|
|
221
|
+
await self._update_user_async(username, user_data)
|
|
222
|
+
return {"created": False, "user_id": existing.get("id")}
|
|
223
|
+
else:
|
|
224
|
+
# 用户不存在,执行创建
|
|
225
|
+
user_id = await self._create_user_async(user_data)
|
|
226
|
+
return {"created": True, "user_id": user_id}
|
|
227
|
+
|
|
228
|
+
async def batch_sync_to_auth(self) -> Dict[str, Any]:
|
|
229
|
+
"""批量同步旧系统用户到 aigc-auth(默认实现)
|
|
230
|
+
|
|
231
|
+
子类可以选择性覆盖此方法以自定义同步逻辑。
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Dict: 同步结果统计
|
|
235
|
+
"""
|
|
236
|
+
if not self.auth_client:
|
|
237
|
+
raise ValueError("auth_client is required for batch_sync_to_auth. Please provide it in constructor.")
|
|
238
|
+
|
|
239
|
+
users = await self.get_all_users_async()
|
|
240
|
+
|
|
241
|
+
results = {
|
|
242
|
+
"total": len(users),
|
|
243
|
+
"success": 0,
|
|
244
|
+
"failed": 0,
|
|
245
|
+
"skipped": 0,
|
|
246
|
+
"errors": []
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for user in users:
|
|
250
|
+
try:
|
|
251
|
+
auth_data = self.transform_legacy_to_auth(user)
|
|
252
|
+
|
|
253
|
+
# 获取密码(支持新的元组返回格式)
|
|
254
|
+
password_result = self.get_password_for_sync(user)
|
|
255
|
+
if isinstance(password_result, tuple):
|
|
256
|
+
password, is_hashed = password_result
|
|
257
|
+
else:
|
|
258
|
+
password, is_hashed = password_result, False
|
|
259
|
+
|
|
260
|
+
# 根据是否已加密选择不同的字段
|
|
261
|
+
if is_hashed:
|
|
262
|
+
auth_data["password_hashed"] = password
|
|
263
|
+
else:
|
|
264
|
+
auth_data["password"] = password
|
|
265
|
+
|
|
266
|
+
result = self.auth_client.sync_user_to_auth(auth_data)
|
|
267
|
+
|
|
268
|
+
if result.get("success"):
|
|
269
|
+
if result.get("created"):
|
|
270
|
+
results["success"] += 1
|
|
271
|
+
else:
|
|
272
|
+
results["skipped"] += 1
|
|
273
|
+
else:
|
|
274
|
+
results["failed"] += 1
|
|
275
|
+
results["errors"].append({
|
|
276
|
+
"user": user.get("username"),
|
|
277
|
+
"error": result.get("message")
|
|
278
|
+
})
|
|
279
|
+
except Exception as e:
|
|
280
|
+
results["failed"] += 1
|
|
281
|
+
results["errors"].append({
|
|
282
|
+
"user": user.get("username"),
|
|
283
|
+
"error": str(e)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return results
|
|
287
|
+
|
|
288
|
+
async def handle_webhook(self, event: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
289
|
+
"""
|
|
290
|
+
处理来自 aigc-auth 的 webhook 通知(默认实现)
|
|
291
|
+
|
|
292
|
+
子类可以选择性覆盖此方法以自定义 webhook 处理逻辑。
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
event: 事件类型,如 "user.created", "user.updated", "user.deleted"
|
|
296
|
+
data: 事件数据(用户信息)
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Dict: 处理结果
|
|
300
|
+
"""
|
|
301
|
+
if event == "user.created" or event == "user.updated" or event == "user.login":
|
|
302
|
+
# 转换数据格式
|
|
303
|
+
legacy_data = self.transform_auth_to_legacy(data)
|
|
304
|
+
|
|
305
|
+
# 获取密码
|
|
306
|
+
password_result = self.get_password_for_sync()
|
|
307
|
+
if isinstance(password_result, tuple):
|
|
308
|
+
password, is_hashed = password_result
|
|
309
|
+
else:
|
|
310
|
+
password, is_hashed = password_result, False
|
|
311
|
+
legacy_data["password"] = password
|
|
312
|
+
|
|
313
|
+
# 创建或更新用户
|
|
314
|
+
result = await self.upsert_user_async(legacy_data)
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
"success": True,
|
|
318
|
+
"message": "User created" if result["created"] else "User updated",
|
|
319
|
+
"created": result["created"],
|
|
320
|
+
"user_id": result["user_id"]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
elif event == "user.deleted":
|
|
324
|
+
# 禁用用户而不是删除
|
|
325
|
+
username = data.get("username")
|
|
326
|
+
if not username:
|
|
327
|
+
logger.error("username is required for user.deleted event")
|
|
328
|
+
return {"success": False, "message": "username is required for user.deleted event"}
|
|
329
|
+
await self._delete_user_async(username)
|
|
330
|
+
|
|
331
|
+
return {"success": True, "message": "User disabled"}
|
|
332
|
+
|
|
333
|
+
elif event == "user.init_sync_auth":
|
|
334
|
+
# 初始化同步:批量同步旧系统用户到 aigc-auth
|
|
335
|
+
if not self.auth_client:
|
|
336
|
+
return {"success": False, "message": "auth_client is required for init_sync_auth event. Please provide it in constructor."}
|
|
337
|
+
|
|
338
|
+
results = await self.batch_sync_to_auth()
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
"success": True,
|
|
342
|
+
"message": f"Batch sync completed: {results['success']} created, {results['skipped']} skipped, {results['failed']} failed",
|
|
343
|
+
"results": results
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {"success": True, "message": "Event ignored"}
|
|
347
|
+
|
|
348
|
+
def transform_auth_to_legacy(self, auth_user: Dict[str, Any]) -> Dict[str, Any]:
|
|
349
|
+
"""将 aigc-auth 用户数据转换为旧系统格式"""
|
|
350
|
+
result = {}
|
|
351
|
+
|
|
352
|
+
for mapping in self.config.field_mappings:
|
|
353
|
+
auth_value = auth_user.get(mapping.auth_field)
|
|
354
|
+
|
|
355
|
+
if auth_value is None:
|
|
356
|
+
if mapping.required and mapping.default_value is None:
|
|
357
|
+
raise ValueError(f"Required field '{mapping.auth_field}' is missing")
|
|
358
|
+
auth_value = mapping.default_value
|
|
359
|
+
|
|
360
|
+
if mapping.transform_to_legacy:
|
|
361
|
+
auth_value = mapping.transform_to_legacy(auth_value)
|
|
362
|
+
|
|
363
|
+
if auth_value is not None:
|
|
364
|
+
result[mapping.legacy_field] = auth_value
|
|
365
|
+
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
def transform_legacy_to_auth(self, legacy_user: LegacyUserData) -> Dict[str, Any]:
|
|
369
|
+
"""将旧系统用户数据转换为 aigc-auth 格式"""
|
|
370
|
+
result = {}
|
|
371
|
+
|
|
372
|
+
for mapping in self.config.field_mappings:
|
|
373
|
+
legacy_value = legacy_user.get(mapping.legacy_field)
|
|
374
|
+
|
|
375
|
+
if legacy_value is None:
|
|
376
|
+
if mapping.required and mapping.default_value is None:
|
|
377
|
+
raise ValueError(f"Required field '{mapping.legacy_field}' is missing")
|
|
378
|
+
legacy_value = mapping.default_value
|
|
379
|
+
|
|
380
|
+
if mapping.transform_to_auth:
|
|
381
|
+
legacy_value = mapping.transform_to_auth(legacy_value)
|
|
382
|
+
|
|
383
|
+
if legacy_value is not None:
|
|
384
|
+
result[mapping.auth_field] = legacy_value
|
|
385
|
+
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
def get_password_for_sync(self, legacy_user: Optional[LegacyUserData] = None) -> tuple:
|
|
389
|
+
"""获取同步时使用的密码
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
tuple: (password, is_hashed)
|
|
393
|
+
- password: 密码字符串
|
|
394
|
+
- is_hashed: 是否已经是加密后的密码
|
|
395
|
+
"""
|
|
396
|
+
if self.config.password_mode == PasswordMode.UNIFIED:
|
|
397
|
+
return (self.config.unified_password, False)
|
|
398
|
+
elif self.config.password_mode == PasswordMode.CUSTOM_MAPPING:
|
|
399
|
+
if self.config.password_mapper and legacy_user:
|
|
400
|
+
password = self.config.password_mapper(legacy_user.data)
|
|
401
|
+
return (password, self.config.password_is_hashed)
|
|
402
|
+
return (self.config.unified_password, False)
|
|
403
|
+
return (self.config.unified_password, False)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class WebhookSender:
|
|
407
|
+
"""Webhook 发送器"""
|
|
408
|
+
|
|
409
|
+
def __init__(self, config: SyncConfig):
|
|
410
|
+
self.config = config
|
|
411
|
+
|
|
412
|
+
def generate_signature(self, payload: str) -> str:
|
|
413
|
+
"""生成 HMAC-SHA256 签名"""
|
|
414
|
+
if not self.config.webhook_secret:
|
|
415
|
+
return ""
|
|
416
|
+
|
|
417
|
+
return hmac.new(
|
|
418
|
+
self.config.webhook_secret.encode('utf-8'),
|
|
419
|
+
payload.encode('utf-8'),
|
|
420
|
+
hashlib.sha256
|
|
421
|
+
).hexdigest()
|
|
422
|
+
|
|
423
|
+
def send(self, event_type: str, data: Dict[str, Any]) -> bool:
|
|
424
|
+
"""发送 webhook 通知"""
|
|
425
|
+
if not self.config.webhook_enabled or not self.config.webhook_url:
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
payload = json.dumps({
|
|
429
|
+
"event": event_type,
|
|
430
|
+
"data": data
|
|
431
|
+
}, ensure_ascii=False)
|
|
432
|
+
|
|
433
|
+
signature = self.generate_signature(payload)
|
|
434
|
+
|
|
435
|
+
headers = {
|
|
436
|
+
"Content-Type": "application/json",
|
|
437
|
+
"X-Webhook-Signature": signature,
|
|
438
|
+
"X-Webhook-Event": event_type
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for attempt in range(self.config.webhook_retry_count):
|
|
442
|
+
try:
|
|
443
|
+
response = requests.post(
|
|
444
|
+
self.config.webhook_url,
|
|
445
|
+
data=payload,
|
|
446
|
+
headers=headers,
|
|
447
|
+
timeout=self.config.webhook_timeout
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if response.status_code == 200:
|
|
451
|
+
logger.info(f"Webhook sent successfully: {event_type}")
|
|
452
|
+
return True
|
|
453
|
+
else:
|
|
454
|
+
logger.warning(f"Webhook failed with status {response.status_code}: {response.text}")
|
|
455
|
+
|
|
456
|
+
except requests.RequestException as e:
|
|
457
|
+
logger.error(f"Webhook request failed (attempt {attempt + 1}): {e}")
|
|
458
|
+
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class UserSyncService:
|
|
463
|
+
"""
|
|
464
|
+
用户同步服务
|
|
465
|
+
|
|
466
|
+
提供以下功能:
|
|
467
|
+
1. 登录时自动同步(auth → legacy)
|
|
468
|
+
2. 初始化批量同步(legacy → auth)
|
|
469
|
+
3. Webhook 增量推送
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
def __init__(
|
|
473
|
+
self,
|
|
474
|
+
auth_client, # AigcAuthClient
|
|
475
|
+
legacy_adapter: LegacySystemAdapter
|
|
476
|
+
):
|
|
477
|
+
self.auth_client = auth_client
|
|
478
|
+
self.adapter = legacy_adapter
|
|
479
|
+
self.config = legacy_adapter.config
|
|
480
|
+
self.webhook_sender = WebhookSender(self.config)
|
|
481
|
+
|
|
482
|
+
# 确保 adapter 也持有 auth_client 引用
|
|
483
|
+
if not self.adapter.auth_client:
|
|
484
|
+
self.adapter.auth_client = auth_client
|
|
485
|
+
|
|
486
|
+
def _user_info_to_dict(self, user_info) -> Dict[str, Any]:
|
|
487
|
+
"""将 UserInfo 对象转换为字典"""
|
|
488
|
+
return {
|
|
489
|
+
"id": user_info.id,
|
|
490
|
+
"username": user_info.username,
|
|
491
|
+
"nickname": user_info.nickname,
|
|
492
|
+
"email": user_info.email,
|
|
493
|
+
"phone": user_info.phone,
|
|
494
|
+
"avatar": user_info.avatar,
|
|
495
|
+
"roles": user_info.roles,
|
|
496
|
+
"permissions": user_info.permissions,
|
|
497
|
+
"department": user_info.department,
|
|
498
|
+
"company": user_info.company,
|
|
499
|
+
"is_admin": user_info.is_admin,
|
|
500
|
+
"status": user_info.status,
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async def sync_on_login_async(self, auth_user_info) -> Dict[str, Any]:
|
|
504
|
+
"""
|
|
505
|
+
登录时异步同步用户到旧系统
|
|
506
|
+
|
|
507
|
+
当用户通过 aigc-auth 登录成功后调用,
|
|
508
|
+
如果旧系统没有该用户则自动创建。
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
auth_user_info: aigc-auth 返回的 UserInfo 对象
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Dict: 同步结果
|
|
515
|
+
"""
|
|
516
|
+
if not self.config.enabled:
|
|
517
|
+
return {"success": True, "message": "Sync disabled"}
|
|
518
|
+
|
|
519
|
+
# 转换用户数据
|
|
520
|
+
auth_data = self._user_info_to_dict(auth_user_info)
|
|
521
|
+
legacy_data = self.adapter.transform_auth_to_legacy(auth_data)
|
|
522
|
+
|
|
523
|
+
# 获取密码(支持新的元组返回格式)
|
|
524
|
+
password_result = self.adapter.get_password_for_sync()
|
|
525
|
+
if isinstance(password_result, tuple):
|
|
526
|
+
password, is_hashed = password_result
|
|
527
|
+
else:
|
|
528
|
+
password, is_hashed = password_result, False
|
|
529
|
+
legacy_data["password"] = password
|
|
530
|
+
|
|
531
|
+
# 使用 upsert 方法(存在则更新,不存在则创建)
|
|
532
|
+
result = await self.adapter.upsert_user_async(legacy_data)
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
"success": True,
|
|
536
|
+
"auth_user_id": auth_user_info.id,
|
|
537
|
+
"legacy_user_id": result["user_id"],
|
|
538
|
+
"created": result["created"],
|
|
539
|
+
"message": "User created" if result["created"] else "User updated"
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async def batch_sync_to_auth_async(self) -> Dict[str, Any]:
|
|
543
|
+
"""
|
|
544
|
+
异步批量同步旧系统用户到 aigc-auth
|
|
545
|
+
|
|
546
|
+
直接委托给 adapter 的默认实现
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Dict: 同步结果统计
|
|
550
|
+
"""
|
|
551
|
+
return await self.adapter.batch_sync_to_auth()
|
|
552
|
+
|
|
553
|
+
# ============ 预设字段映射 ============
|
|
554
|
+
|
|
555
|
+
def create_default_field_mappings() -> List[FieldMapping]:
|
|
556
|
+
"""创建默认字段映射配置(通用基础映射)"""
|
|
557
|
+
return [
|
|
558
|
+
FieldMapping(
|
|
559
|
+
auth_field="username",
|
|
560
|
+
legacy_field="username",
|
|
561
|
+
required=True
|
|
562
|
+
),
|
|
563
|
+
FieldMapping(
|
|
564
|
+
auth_field="email",
|
|
565
|
+
legacy_field="email"
|
|
566
|
+
),
|
|
567
|
+
FieldMapping(
|
|
568
|
+
auth_field="nickname",
|
|
569
|
+
legacy_field="nickname"
|
|
570
|
+
),
|
|
571
|
+
FieldMapping(
|
|
572
|
+
auth_field="phone",
|
|
573
|
+
legacy_field="phone"
|
|
574
|
+
),
|
|
575
|
+
FieldMapping(
|
|
576
|
+
auth_field="avatar",
|
|
577
|
+
legacy_field="avatar"
|
|
578
|
+
),
|
|
579
|
+
FieldMapping(
|
|
580
|
+
auth_field="company",
|
|
581
|
+
legacy_field="company"
|
|
582
|
+
),
|
|
583
|
+
FieldMapping(
|
|
584
|
+
auth_field="department",
|
|
585
|
+
legacy_field="department"
|
|
586
|
+
),
|
|
587
|
+
]
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ============ 便捷函数 ============
|
|
591
|
+
|
|
592
|
+
def create_sync_config(
|
|
593
|
+
field_mappings: List[FieldMapping] = None,
|
|
594
|
+
password_mode: PasswordMode = PasswordMode.UNIFIED,
|
|
595
|
+
unified_password: str = "Abc@123456",
|
|
596
|
+
webhook_url: Optional[str] = None,
|
|
597
|
+
webhook_secret: Optional[str] = None,
|
|
598
|
+
direction: SyncDirection = SyncDirection.AUTH_TO_LEGACY,
|
|
599
|
+
**kwargs
|
|
600
|
+
) -> SyncConfig:
|
|
601
|
+
"""
|
|
602
|
+
创建同步配置的便捷函数
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
field_mappings: 字段映射列表,默认使用通用映射
|
|
606
|
+
password_mode: 密码处理模式
|
|
607
|
+
unified_password: 统一初始密码
|
|
608
|
+
webhook_url: Webhook 接收地址
|
|
609
|
+
webhook_secret: Webhook 签名密钥
|
|
610
|
+
direction: 同步方向
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
SyncConfig: 同步配置对象
|
|
614
|
+
"""
|
|
615
|
+
return SyncConfig(
|
|
616
|
+
enabled=True,
|
|
617
|
+
direction=direction,
|
|
618
|
+
field_mappings=field_mappings or create_default_field_mappings(),
|
|
619
|
+
password_mode=password_mode,
|
|
620
|
+
unified_password=unified_password,
|
|
621
|
+
webhook_enabled=bool(webhook_url),
|
|
622
|
+
webhook_url=webhook_url,
|
|
623
|
+
webhook_secret=webhook_secret,
|
|
624
|
+
**kwargs
|
|
625
|
+
)
|