rainycode 1.1.0__tar.gz → 1.1.1__tar.gz
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.
- {rainycode-1.1.0 → rainycode-1.1.1}/PKG-INFO +1 -1
- {rainycode-1.1.0 → rainycode-1.1.1}/common_depends/auth_depend.py +1 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_servers/api/auth_api.py +2 -1
- {rainycode-1.1.0 → rainycode-1.1.1}/common_servers/service/auth_svc.py +115 -11
- {rainycode-1.1.0 → rainycode-1.1.1}/common_utils/bcrypt_util.py +2 -4
- {rainycode-1.1.0 → rainycode-1.1.1}/pyproject.toml +1 -1
- {rainycode-1.1.0 → rainycode-1.1.1}/rainycode.egg-info/PKG-INFO +1 -1
- {rainycode-1.1.0 → rainycode-1.1.1}/rainycode.egg-info/SOURCES.txt +0 -1
- rainycode-1.1.0/common_servers/managers/wechat_user_manager.py +0 -98
- {rainycode-1.1.0 → rainycode-1.1.1}/common_base/aiorequests.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_base/consts.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_base/exception.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_base/logging.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_base/response.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_models/__init__.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_models/base_model.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_models/user_model.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_models/wechat_model.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_servers/api/wechat_api.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_servers/router.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_servers/schemas/__init__.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_servers/schemas/auth_schema.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_servers/service/wechat_svc.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_utils/captcha_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_utils/ip_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_utils/jwt_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_utils/snowflake_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/common_utils/wechat_util.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/core/base_config.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/core/databases/aiodb.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/core/databases/aioredis.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/core/middleware/http_middleware.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/core/start.py +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/rainycode.egg-info/dependency_links.txt +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/rainycode.egg-info/requires.txt +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/rainycode.egg-info/top_level.txt +0 -0
- {rainycode-1.1.0 → rainycode-1.1.1}/setup.cfg +0 -0
|
@@ -30,10 +30,11 @@ async def refresh_token(refresh_token: str = Body(..., embed=True)):
|
|
|
30
30
|
|
|
31
31
|
@router.post("/logout", summary="登出")
|
|
32
32
|
async def logout(
|
|
33
|
-
|
|
33
|
+
request: Request,
|
|
34
34
|
refresh_token: str = Body(..., embed=True),
|
|
35
35
|
_: User = Security(login_require),
|
|
36
36
|
):
|
|
37
|
+
token = request.state.token
|
|
37
38
|
await AuthService.logout(token, refresh_token)
|
|
38
39
|
return SuccessReturn(msg="登出成功")
|
|
39
40
|
|
|
@@ -16,6 +16,7 @@ from common_base.consts import (
|
|
|
16
16
|
REDIS_PREFIX_LOGIN_LOCK_IP,
|
|
17
17
|
)
|
|
18
18
|
from common_base.exception import CustomException
|
|
19
|
+
from common_base.logging import logger
|
|
19
20
|
from common_models.user_model import User
|
|
20
21
|
from common_models.wechat_model import WechatApp, WechatUser, AppTypeEnum
|
|
21
22
|
from common_utils.bcrypt_util import PwdUtil
|
|
@@ -23,7 +24,6 @@ from common_utils.jwt_util import JwtUtil
|
|
|
23
24
|
from common_utils.wechat_util import WechatUtil
|
|
24
25
|
from core.databases.aioredis import AioRedis
|
|
25
26
|
from common_servers.schemas.auth_schema import UserRegister, UserLogin, WechatLogin
|
|
26
|
-
from common_servers.managers.wechat_user_manager import WechatUserManager
|
|
27
27
|
from tortoise.transactions import atomic
|
|
28
28
|
from tortoise.expressions import Q
|
|
29
29
|
from core.base_config import base_config
|
|
@@ -75,6 +75,7 @@ class AuthService:
|
|
|
75
75
|
phone=user_in.phone,
|
|
76
76
|
nickname=user_in.nickname
|
|
77
77
|
)
|
|
78
|
+
logger.info(f"用户注册成功: username={user.username}, user_id={user.id}")
|
|
78
79
|
return user
|
|
79
80
|
|
|
80
81
|
@classmethod
|
|
@@ -83,7 +84,9 @@ class AuthService:
|
|
|
83
84
|
用户密码登录
|
|
84
85
|
"""
|
|
85
86
|
user = await cls._authenticate_password(user_in, client_ip)
|
|
86
|
-
|
|
87
|
+
token_data = await cls._create_tokens(user)
|
|
88
|
+
logger.info(f"用户登录成功: username={user.username}, user_id={user.id}, ip={client_ip}")
|
|
89
|
+
return token_data
|
|
87
90
|
|
|
88
91
|
@classmethod
|
|
89
92
|
async def login_wechat_mini(cls, wechat_in: WechatLogin) -> dict:
|
|
@@ -99,9 +102,12 @@ class AuthService:
|
|
|
99
102
|
刷新 Token
|
|
100
103
|
"""
|
|
101
104
|
user_id = await AioRedis.get(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
102
|
-
user_id = int(user_id) if user_id else None
|
|
103
105
|
if not user_id:
|
|
104
106
|
raise CustomException(msg="无效或已过期的刷新令牌")
|
|
107
|
+
try:
|
|
108
|
+
user_id = int(user_id)
|
|
109
|
+
except (ValueError, TypeError):
|
|
110
|
+
raise CustomException(msg="无效的刷新令牌")
|
|
105
111
|
|
|
106
112
|
user = await User.get_or_none(id=user_id)
|
|
107
113
|
if not user or not user.is_active:
|
|
@@ -109,7 +115,9 @@ class AuthService:
|
|
|
109
115
|
|
|
110
116
|
# Token 轮换:删除旧的,生成新的
|
|
111
117
|
await AioRedis.delete(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
112
|
-
|
|
118
|
+
token_data = await cls._create_tokens(user)
|
|
119
|
+
logger.info(f"Token刷新成功: username={user.username}, user_id={user.id}")
|
|
120
|
+
return token_data
|
|
113
121
|
|
|
114
122
|
@classmethod
|
|
115
123
|
async def logout(cls, access_token: str, refresh_token: str):
|
|
@@ -133,6 +141,12 @@ class AuthService:
|
|
|
133
141
|
if refresh_token:
|
|
134
142
|
await AioRedis.delete(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
135
143
|
|
|
144
|
+
# 记录登出日志
|
|
145
|
+
if payload:
|
|
146
|
+
user_id = payload.get("sub")
|
|
147
|
+
username = payload.get("username")
|
|
148
|
+
logger.info(f"用户登出: user_id={user_id}, username={username}")
|
|
149
|
+
|
|
136
150
|
@classmethod
|
|
137
151
|
async def get_wechat_oauth_url(cls, app_id: str, redirect_uri: str, state: str | None = None) -> str:
|
|
138
152
|
"""
|
|
@@ -241,8 +255,8 @@ class AuthService:
|
|
|
241
255
|
|
|
242
256
|
user_info = await WechatUtil.get_user_info(access_token, openid)
|
|
243
257
|
|
|
244
|
-
# 使用
|
|
245
|
-
user = await
|
|
258
|
+
# 使用 _bind_or_register_wechat_user 创建或绑定用户
|
|
259
|
+
user = await cls._bind_or_register_wechat_user(
|
|
246
260
|
openid=openid,
|
|
247
261
|
app_config=app_config,
|
|
248
262
|
unionid=wx_res.get("unionid"),
|
|
@@ -255,7 +269,7 @@ class AuthService:
|
|
|
255
269
|
|
|
256
270
|
# 新用户,需要 snsapi_userinfo 授权
|
|
257
271
|
# 缓存 state 信息供二次回调使用
|
|
258
|
-
new_state =
|
|
272
|
+
new_state = str(uuid.uuid4())
|
|
259
273
|
await AioRedis.set(f"{REDIS_PREFIX_OAUTH_STATE}{new_state}", cache_value, ex=300)
|
|
260
274
|
|
|
261
275
|
# 构建 snsapi_userinfo 授权链接
|
|
@@ -296,17 +310,19 @@ class AuthService:
|
|
|
296
310
|
user = await User.get_or_none(username=data.username)
|
|
297
311
|
if not user:
|
|
298
312
|
await cls._record_login_fail(data.username, client_ip)
|
|
313
|
+
logger.warning(f"登录失败: 用户名不存在, username={data.username}, ip={client_ip}")
|
|
299
314
|
raise CustomException(msg="用户名或密码错误")
|
|
300
315
|
|
|
301
316
|
if not PwdUtil.verify_password(data.password, user.password):
|
|
302
317
|
await cls._record_login_fail(data.username, client_ip)
|
|
318
|
+
logger.warning(f"登录失败: 密码错误, username={data.username}, ip={client_ip}")
|
|
303
319
|
raise CustomException(msg="用户名或密码错误")
|
|
304
320
|
|
|
305
321
|
if not user.is_active:
|
|
306
322
|
raise CustomException(msg="用户已被禁用")
|
|
307
323
|
|
|
308
324
|
# 清除失败记录
|
|
309
|
-
await cls._clear_login_fail(data.username)
|
|
325
|
+
await cls._clear_login_fail(data.username, client_ip)
|
|
310
326
|
|
|
311
327
|
# 更新最后登录时间
|
|
312
328
|
user.last_login = datetime.now()
|
|
@@ -357,8 +373,8 @@ class AuthService:
|
|
|
357
373
|
if phone and not re.match(r'^1[3-9]\d{9}$', phone):
|
|
358
374
|
raise CustomException(msg="手机号格式不正确")
|
|
359
375
|
|
|
360
|
-
# 6. 使用
|
|
361
|
-
return await
|
|
376
|
+
# 6. 使用 _bind_or_register_wechat_user 处理用户绑定/注册
|
|
377
|
+
return await cls._bind_or_register_wechat_user(
|
|
362
378
|
openid=openid,
|
|
363
379
|
app_config=app_config,
|
|
364
380
|
unionid=unionid,
|
|
@@ -397,9 +413,10 @@ class AuthService:
|
|
|
397
413
|
await AioRedis.expire(ip_key, LOGIN_LOCK_SECONDS)
|
|
398
414
|
|
|
399
415
|
@classmethod
|
|
400
|
-
async def _clear_login_fail(cls, username: str):
|
|
416
|
+
async def _clear_login_fail(cls, username: str, client_ip: str):
|
|
401
417
|
"""清除登录失败记录"""
|
|
402
418
|
await AioRedis.delete(f"{REDIS_PREFIX_LOGIN_LOCK_USER}{username}")
|
|
419
|
+
await AioRedis.delete(f"{REDIS_PREFIX_LOGIN_LOCK_IP}{client_ip}")
|
|
403
420
|
|
|
404
421
|
@classmethod
|
|
405
422
|
async def _create_tokens(cls, user: User) -> dict:
|
|
@@ -424,6 +441,93 @@ class AuthService:
|
|
|
424
441
|
"expires_in": base_config.access_token_expire_minutes * 60,
|
|
425
442
|
}
|
|
426
443
|
|
|
444
|
+
@classmethod
|
|
445
|
+
@atomic("main")
|
|
446
|
+
async def _bind_or_register_wechat_user(
|
|
447
|
+
cls,
|
|
448
|
+
openid: str,
|
|
449
|
+
app_config: WechatApp,
|
|
450
|
+
unionid: str | None = None,
|
|
451
|
+
phone: str | None = None,
|
|
452
|
+
nickname: str | None = None,
|
|
453
|
+
avatar: str | None = None,
|
|
454
|
+
session_key: str | None = None,
|
|
455
|
+
) -> User:
|
|
456
|
+
"""
|
|
457
|
+
绑定已有用户或创建新用户(微信登录专用)
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
openid: 微信 OpenID
|
|
461
|
+
app_config: 微信应用配置
|
|
462
|
+
unionid: 微信 UnionID(可选,用于跨应用关联)
|
|
463
|
+
phone: 手机号(可选,小程序一键登录获取)
|
|
464
|
+
nickname: 微信昵称
|
|
465
|
+
avatar: 微信头像
|
|
466
|
+
session_key: 会话密钥(仅小程序)
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
User: 用户对象
|
|
470
|
+
"""
|
|
471
|
+
# 1. 检查 WechatUser 是否已存在
|
|
472
|
+
wechat_user = await WechatUser.get_or_none(
|
|
473
|
+
openid=openid, wechat_app=app_config
|
|
474
|
+
).select_related("user")
|
|
475
|
+
|
|
476
|
+
if wechat_user:
|
|
477
|
+
# 已存在关联,更新信息
|
|
478
|
+
if session_key:
|
|
479
|
+
wechat_user.session_key = session_key
|
|
480
|
+
if nickname:
|
|
481
|
+
wechat_user.nickname = nickname
|
|
482
|
+
if avatar:
|
|
483
|
+
wechat_user.avatar = avatar
|
|
484
|
+
if phone and not wechat_user.user.phone:
|
|
485
|
+
wechat_user.user.phone = phone
|
|
486
|
+
await wechat_user.user.save()
|
|
487
|
+
await wechat_user.save()
|
|
488
|
+
return wechat_user.user
|
|
489
|
+
|
|
490
|
+
# 2. 检查 User 是否存在(通过 phone 或 unionid)
|
|
491
|
+
user = None
|
|
492
|
+
if phone:
|
|
493
|
+
user = await User.get_or_none(phone=phone)
|
|
494
|
+
|
|
495
|
+
# 如果有 UnionID,检查其他应用下的 WechatUser 是否关联了 User
|
|
496
|
+
if not user and unionid:
|
|
497
|
+
other_wechat_user = (
|
|
498
|
+
await WechatUser.filter(unionid=unionid)
|
|
499
|
+
.exclude(wechat_app=app_config)
|
|
500
|
+
.first()
|
|
501
|
+
.select_related("user")
|
|
502
|
+
)
|
|
503
|
+
if other_wechat_user:
|
|
504
|
+
user = other_wechat_user.user
|
|
505
|
+
|
|
506
|
+
# 3. 创建 User(如不存在)
|
|
507
|
+
if not user:
|
|
508
|
+
# 使用 UUID 避免并发冲突
|
|
509
|
+
username = f"wx_{uuid.uuid4().hex[:8]}"
|
|
510
|
+
user = await User.create(
|
|
511
|
+
username=username,
|
|
512
|
+
password="", # 微信用户无密码
|
|
513
|
+
phone=phone,
|
|
514
|
+
nickname=nickname or f"用户{username[-4:]}",
|
|
515
|
+
avatar=avatar,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# 4. 创建 WechatUser 关联
|
|
519
|
+
await WechatUser.create(
|
|
520
|
+
user=user,
|
|
521
|
+
wechat_app=app_config,
|
|
522
|
+
openid=openid,
|
|
523
|
+
unionid=unionid,
|
|
524
|
+
session_key=session_key,
|
|
525
|
+
nickname=nickname,
|
|
526
|
+
avatar=avatar,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return user
|
|
530
|
+
|
|
427
531
|
@classmethod
|
|
428
532
|
async def _build_redirect_response(cls, redirect_uri: str, token_data: dict, state: str | None = None) -> RedirectResponse:
|
|
429
533
|
"""构建带临时码的重定向响应"""
|
|
@@ -44,7 +44,7 @@ class PwdUtil:
|
|
|
44
44
|
@classmethod
|
|
45
45
|
def check_password_strength(cls, password: str) -> str | None:
|
|
46
46
|
"""
|
|
47
|
-
|
|
47
|
+
检查密码强度(不含长度检查,由 Schema min_length 处理)
|
|
48
48
|
|
|
49
49
|
Args:
|
|
50
50
|
password: 明文密码
|
|
@@ -52,12 +52,10 @@ class PwdUtil:
|
|
|
52
52
|
Returns:
|
|
53
53
|
str: 如果密码强度不够返回提示信息,否则返回None
|
|
54
54
|
"""
|
|
55
|
-
if len(password) < 6:
|
|
56
|
-
return "密码长度至少6位"
|
|
57
55
|
if not any(c.isupper() for c in password):
|
|
58
56
|
return "密码需要包含大写字母"
|
|
59
57
|
if not any(c.islower() for c in password):
|
|
60
|
-
return "密码需要包含小写字母"
|
|
58
|
+
return "密码需要包含小写字母"
|
|
61
59
|
if not any(c.isdigit() for c in password):
|
|
62
60
|
return "密码需要包含数字"
|
|
63
61
|
return None
|
|
@@ -12,7 +12,6 @@ common_models/wechat_model.py
|
|
|
12
12
|
common_servers/router.py
|
|
13
13
|
common_servers/api/auth_api.py
|
|
14
14
|
common_servers/api/wechat_api.py
|
|
15
|
-
common_servers/managers/wechat_user_manager.py
|
|
16
15
|
common_servers/schemas/__init__.py
|
|
17
16
|
common_servers/schemas/auth_schema.py
|
|
18
17
|
common_servers/service/auth_svc.py
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
from tortoise.transactions import atomic
|
|
3
|
-
from common_models.user_model import User
|
|
4
|
-
from common_models.wechat_model import WechatApp, WechatUser
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class WechatUserManager:
|
|
8
|
-
"""
|
|
9
|
-
微信用户绑定/注册统一处理
|
|
10
|
-
合并小程序和公众号的用户处理逻辑
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
@classmethod
|
|
14
|
-
@atomic("main")
|
|
15
|
-
async def bind_or_register(
|
|
16
|
-
cls,
|
|
17
|
-
openid: str,
|
|
18
|
-
app_config: WechatApp,
|
|
19
|
-
unionid: str | None = None,
|
|
20
|
-
phone: str | None = None,
|
|
21
|
-
nickname: str | None = None,
|
|
22
|
-
avatar: str | None = None,
|
|
23
|
-
session_key: str | None = None,
|
|
24
|
-
) -> User:
|
|
25
|
-
"""
|
|
26
|
-
绑定已有用户或创建新用户
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
openid: 微信 OpenID
|
|
30
|
-
app_config: 微信应用配置
|
|
31
|
-
unionid: 微信 UnionID(可选,用于跨应用关联)
|
|
32
|
-
phone: 手机号(可选,小程序一键登录获取)
|
|
33
|
-
nickname: 微信昵称
|
|
34
|
-
avatar: 微信头像
|
|
35
|
-
session_key: 会话密钥(仅小程序)
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
User: 用户对象
|
|
39
|
-
"""
|
|
40
|
-
# 1. 检查 WechatUser 是否已存在
|
|
41
|
-
wechat_user = await WechatUser.get_or_none(
|
|
42
|
-
openid=openid, wechat_app=app_config
|
|
43
|
-
).select_related("user")
|
|
44
|
-
|
|
45
|
-
if wechat_user:
|
|
46
|
-
# 已存在关联,更新信息
|
|
47
|
-
if session_key:
|
|
48
|
-
wechat_user.session_key = session_key
|
|
49
|
-
if nickname:
|
|
50
|
-
wechat_user.nickname = nickname
|
|
51
|
-
if avatar:
|
|
52
|
-
wechat_user.avatar = avatar
|
|
53
|
-
if phone and not wechat_user.user.phone:
|
|
54
|
-
wechat_user.user.phone = phone
|
|
55
|
-
await wechat_user.user.save()
|
|
56
|
-
await wechat_user.save()
|
|
57
|
-
return wechat_user.user
|
|
58
|
-
|
|
59
|
-
# 2. 检查 User 是否存在(通过 phone 或 unionid)
|
|
60
|
-
user = None
|
|
61
|
-
if phone:
|
|
62
|
-
user = await User.get_or_none(phone=phone)
|
|
63
|
-
|
|
64
|
-
# 如果有 UnionID,检查其他应用下的 WechatUser 是否关联了 User
|
|
65
|
-
if not user and unionid:
|
|
66
|
-
other_wechat_user = (
|
|
67
|
-
await WechatUser.filter(unionid=unionid)
|
|
68
|
-
.exclude(wechat_app=app_config)
|
|
69
|
-
.first()
|
|
70
|
-
.select_related("user")
|
|
71
|
-
)
|
|
72
|
-
if other_wechat_user:
|
|
73
|
-
user = other_wechat_user.user
|
|
74
|
-
|
|
75
|
-
# 3. 创建 User(如不存在)
|
|
76
|
-
if not user:
|
|
77
|
-
# 使用 UUID 避免并发冲突
|
|
78
|
-
username = f"wx_{uuid.uuid4().hex[:8]}"
|
|
79
|
-
user = await User.create(
|
|
80
|
-
username=username,
|
|
81
|
-
password="", # 微信用户无密码
|
|
82
|
-
phone=phone,
|
|
83
|
-
nickname=nickname or f"用户{username[-4:]}",
|
|
84
|
-
avatar=avatar,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# 4. 创建 WechatUser 关联
|
|
88
|
-
await WechatUser.create(
|
|
89
|
-
user=user,
|
|
90
|
-
wechat_app=app_config,
|
|
91
|
-
openid=openid,
|
|
92
|
-
unionid=unionid,
|
|
93
|
-
session_key=session_key,
|
|
94
|
-
nickname=nickname,
|
|
95
|
-
avatar=avatar,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
return user
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|