mdbq 4.0.91__py3-none-any.whl → 4.0.93__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.
mdbq/__version__.py CHANGED
@@ -1 +1 @@
1
- VERSION = '4.0.91'
1
+ VERSION = '4.0.93'
mdbq/auth/auth_backend.py CHANGED
@@ -189,6 +189,7 @@ class StandaloneAuthManager:
189
189
  CREATE TABLE IF NOT EXISTS users (
190
190
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
191
191
  username VARCHAR(50) NOT NULL,
192
+ email VARCHAR(100) NOT NULL,
192
193
  password_hash VARCHAR(128) NOT NULL,
193
194
  password_plain TEXT NOT NULL,
194
195
  salt VARCHAR(64) NOT NULL,
@@ -203,6 +204,7 @@ class StandaloneAuthManager:
203
204
  password_reset_expires TIMESTAMP(3) NULL DEFAULT NULL COMMENT '重置令牌过期时间',
204
205
 
205
206
  UNIQUE KEY uk_users_username (username),
207
+ UNIQUE KEY uk_users_email (email),
206
208
  UNIQUE KEY uk_users_reset_token (password_reset_token),
207
209
  KEY idx_users_role (role),
208
210
  KEY idx_users_created_at (created_at),
@@ -372,15 +374,15 @@ class StandaloneAuthManager:
372
374
  cursor.close()
373
375
  conn.close()
374
376
 
375
- def register_user(self, username, password, role='user', permissions=None):
377
+ def register_user(self, username, password, role='user', permissions=None, email=None):
376
378
  """用户注册"""
377
379
  conn = self.pool.connection()
378
380
  cursor = conn.cursor()
379
381
 
380
382
  try:
381
383
  # 验证输入
382
- if not username or not password:
383
- return {'success': False, 'message': '用户名和密码不能为空'}
384
+ if not username or not password or not email:
385
+ return {'success': False, 'message': '用户名、密码和邮箱不能为空'}
384
386
 
385
387
  if len(username.strip()) < 3:
386
388
  return {'success': False, 'message': '用户名至少需要3个字符'}
@@ -388,13 +390,25 @@ class StandaloneAuthManager:
388
390
  if len(password) < 6:
389
391
  return {'success': False, 'message': '密码至少需要6个字符'}
390
392
 
393
+ # 邮箱格式验证
394
+ import re
395
+ email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
396
+ if not re.match(email_pattern, email):
397
+ return {'success': False, 'message': '请输入有效的邮箱地址'}
398
+
391
399
  username = username.strip()
400
+ email = email.strip().lower()
392
401
 
393
402
  # 检查用户名是否已存在
394
403
  cursor.execute('SELECT id FROM users WHERE username = %s', (username,))
395
404
  if cursor.fetchone():
396
405
  return {'success': False, 'message': '用户名已被占用'}
397
406
 
407
+ # 检查邮箱是否已存在
408
+ cursor.execute('SELECT id FROM users WHERE email = %s', (email,))
409
+ if cursor.fetchone():
410
+ return {'success': False, 'message': '邮箱已被注册'}
411
+
398
412
  # 生成盐值和密码哈希
399
413
  salt = secrets.token_hex(32)
400
414
  password_hash = self._hash_password(password, salt)
@@ -406,9 +420,9 @@ class StandaloneAuthManager:
406
420
 
407
421
  # 创建新用户
408
422
  cursor.execute('''
409
- INSERT INTO users (username, password_hash, password_plain, salt, role, permissions, is_active)
410
- VALUES (%s, %s, %s, %s, %s, %s, %s)
411
- ''', (username, password_hash, password, salt, role, permissions_json, True))
423
+ INSERT INTO users (username, email, password_hash, password_plain, salt, role, permissions, is_active)
424
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
425
+ ''', (username, email, password_hash, password, salt, role, permissions_json, True))
412
426
 
413
427
  user_id = cursor.lastrowid
414
428
 
@@ -418,6 +432,7 @@ class StandaloneAuthManager:
418
432
  'user': {
419
433
  'id': user_id,
420
434
  'username': username,
435
+ 'email': email,
421
436
  'role': role,
422
437
  'permissions': permissions
423
438
  }
@@ -429,13 +444,13 @@ class StandaloneAuthManager:
429
444
  cursor.close()
430
445
  conn.close()
431
446
 
432
- def authenticate_user(self, username, password, ip_address=None, user_agent=None):
433
- """用户身份验证"""
447
+ def authenticate_user(self, username_or_email, password, ip_address=None, user_agent=None):
448
+ """用户身份验证 - 支持用户名或邮箱登录"""
434
449
 
435
450
  # 检查IP是否被限流
436
451
  ip_check = self._check_ip_rate_limit(ip_address, 'login')
437
452
  if ip_check['blocked']:
438
- self._log_login_attempt(username, ip_address, user_agent, 'failure', f'ip_blocked_{ip_check["remaining_time"]}s')
453
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'ip_blocked_{ip_check["remaining_time"]}s')
439
454
  return {
440
455
  'success': False,
441
456
  'error': 'ip_blocked',
@@ -447,24 +462,26 @@ class StandaloneAuthManager:
447
462
  cursor = conn.cursor()
448
463
 
449
464
  try:
450
- # 获取用户信息
465
+ # 获取用户信息 - 支持用户名或邮箱
451
466
  cursor.execute('''
452
- SELECT id, username, password_hash, salt, role, permissions,
467
+ SELECT id, username, email, password_hash, salt, role, permissions,
453
468
  is_active, login_attempts, locked_until
454
- FROM users WHERE username = %s
455
- ''', (username,))
469
+ FROM users WHERE username = %s OR email = %s
470
+ ''', (username_or_email, username_or_email))
456
471
 
457
472
  user = cursor.fetchone()
458
473
  if not user:
459
- self._log_login_attempt(username, ip_address, user_agent, 'failure', 'user_not_found')
474
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'user_not_found')
460
475
  self._record_ip_failure(ip_address, 'login')
461
476
  return {
462
477
  'success': False,
463
478
  'error': 'invalid_credentials',
464
- 'message': '用户名或密码错误'
479
+ 'message': '用户名/邮箱或密码错误'
465
480
  }
466
481
 
467
482
  user_id = user['id']
483
+ username = user['username']
484
+ email = user['email']
468
485
  password_hash = user['password_hash']
469
486
  salt = user['salt']
470
487
  role = user['role']
@@ -475,7 +492,7 @@ class StandaloneAuthManager:
475
492
 
476
493
  # 检查账户状态
477
494
  if not is_active:
478
- self._log_login_attempt(username, ip_address, user_agent, 'failure', 'account_disabled', user_id)
495
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'account_disabled', user_id)
479
496
  self._record_ip_failure(ip_address, 'login')
480
497
  return {
481
498
  'success': False,
@@ -493,7 +510,7 @@ class StandaloneAuthManager:
493
510
 
494
511
  if locked_until > current_time_utc:
495
512
  remaining_seconds = int((locked_until - current_time_utc).total_seconds())
496
- self._log_login_attempt(username, ip_address, user_agent, 'failure', 'account_locked', user_id)
513
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', 'account_locked', user_id)
497
514
  self._record_ip_failure(ip_address, 'login')
498
515
  return {
499
516
  'success': False,
@@ -514,7 +531,7 @@ class StandaloneAuthManager:
514
531
  UPDATE users SET login_attempts = %s, locked_until = %s WHERE id = %s
515
532
  ''', (login_attempts, locked_until, user_id))
516
533
 
517
- self._log_login_attempt(username, ip_address, user_agent, 'failure', f'password_incorrect_locked_{lockout_duration}s', user_id)
534
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'password_incorrect_locked_{lockout_duration}s', user_id)
518
535
  self._record_ip_failure(ip_address, 'login')
519
536
 
520
537
  return {
@@ -528,14 +545,14 @@ class StandaloneAuthManager:
528
545
  UPDATE users SET login_attempts = %s WHERE id = %s
529
546
  ''', (login_attempts, user_id))
530
547
 
531
- self._log_login_attempt(username, ip_address, user_agent, 'failure', f'password_incorrect_attempt_{login_attempts}', user_id)
548
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'failure', f'password_incorrect_attempt_{login_attempts}', user_id)
532
549
  self._record_ip_failure(ip_address, 'login')
533
550
 
534
551
  remaining_attempts = self.auth_config['max_login_attempts'] - login_attempts
535
552
  return {
536
553
  'success': False,
537
554
  'error': 'invalid_credentials',
538
- 'message': f'用户名或密码错误,还可以尝试 {remaining_attempts} 次',
555
+ 'message': f'用户名/邮箱或密码错误,还可以尝试 {remaining_attempts} 次',
539
556
  'remaining_attempts': remaining_attempts
540
557
  }
541
558
 
@@ -545,13 +562,14 @@ class StandaloneAuthManager:
545
562
  ''', (current_time_utc, user_id))
546
563
 
547
564
  # 记录成功登录
548
- self._log_login_attempt(username, ip_address, user_agent, 'success', None, user_id)
565
+ self._log_login_attempt(username_or_email, ip_address, user_agent, 'success', None, user_id)
549
566
  self._reset_ip_failures(ip_address, 'login')
550
567
 
551
568
  return {
552
569
  'success': True,
553
570
  'user_id': user_id,
554
- 'username': user['username'],
571
+ 'username': username,
572
+ 'email': email,
555
573
  'role': role,
556
574
  'permissions': self._safe_json_parse(permissions),
557
575
  'last_login': current_time_utc.isoformat()
@@ -897,28 +915,61 @@ class StandaloneAuthManager:
897
915
  cursor.close()
898
916
  conn.close()
899
917
 
900
- def request_password_reset(self, username):
901
- """请求密码重置"""
918
+ def get_user_masked_email(self, username):
919
+ """根据用户名获取脱敏邮箱(用于忘记密码提示)"""
902
920
  conn = self.pool.connection()
903
921
  cursor = conn.cursor()
904
922
 
905
923
  try:
906
- # 查找用户
924
+ # 查找用户邮箱
907
925
  cursor.execute('''
908
- SELECT id, username, is_active
926
+ SELECT email, is_active
909
927
  FROM users WHERE username = %s
910
928
  ''', (username,))
911
929
 
930
+ user = cursor.fetchone()
931
+ if not user or not user['is_active']:
932
+ return {'success': False, 'message': '用户不存在'}
933
+
934
+ # 返回脱敏邮箱
935
+ masked_email = self._mask_email(user['email'])
936
+ return {
937
+ 'success': True,
938
+ 'masked_email': masked_email
939
+ }
940
+
941
+ except Exception as e:
942
+ return {'success': False, 'message': f'查询用户邮箱失败: {str(e)}'}
943
+ finally:
944
+ cursor.close()
945
+ conn.close()
946
+
947
+ def request_password_reset(self, username, email):
948
+ """请求密码重置 - 需要同时验证用户名和邮箱"""
949
+ conn = self.pool.connection()
950
+ cursor = conn.cursor()
951
+
952
+ try:
953
+ # 查找用户 - 必须用户名和邮箱都匹配
954
+ cursor.execute('''
955
+ SELECT id, username, email, is_active
956
+ FROM users WHERE username = %s AND email = %s
957
+ ''', (username, email))
958
+
912
959
  user = cursor.fetchone()
913
960
  if not user:
914
- # 为安全起见,即使用户不存在也返回成功消息
961
+ # 为安全起见,统一返回错误信息
915
962
  return {
916
- 'success': True,
917
- 'message': '如果该用户存在,重置链接已发送到相关联系方式'
963
+ 'success': False,
964
+ 'message': '用户名或邮箱不正确'
918
965
  }
919
966
 
920
967
  if not user['is_active']:
921
- return {'success': False, 'message': '账户已被禁用'}
968
+ # 账户被禁用,也返回用户名或邮箱不正确
969
+ return {
970
+ 'success': False,
971
+ 'message': '用户名或邮箱不正确'
972
+ }
922
973
 
923
974
  # 生成重置令牌
924
975
  reset_token = secrets.token_urlsafe(32)
@@ -931,11 +982,19 @@ class StandaloneAuthManager:
931
982
  WHERE id = %s
932
983
  ''', (reset_token, expires_at, user['id']))
933
984
 
985
+ # 邮箱脱敏处理
986
+ masked_email = self._mask_email(user['email'])
987
+
988
+ # 直接返回重置令牌给前端
934
989
  return {
935
990
  'success': True,
936
- 'message': '重置链接已生成',
937
- 'reset_token': reset_token, # 实际应用中应该通过邮件发送
938
- 'username': user['username']
991
+ 'message': f'验证成功,重置令牌已生成(邮箱:{masked_email})',
992
+ 'data': {
993
+ 'reset_token': reset_token,
994
+ 'masked_email': masked_email,
995
+ 'username': user['username'],
996
+ 'expires_at': expires_at.isoformat()
997
+ }
939
998
  }
940
999
 
941
1000
  except Exception as e:
@@ -1215,6 +1274,33 @@ class StandaloneAuthManager:
1215
1274
  return []
1216
1275
  return json_str or []
1217
1276
 
1277
+ def _mask_email(self, email):
1278
+ """邮箱脱敏处理"""
1279
+ if not email or '@' not in email:
1280
+ return email
1281
+
1282
+ local_part, domain_part = email.split('@', 1)
1283
+
1284
+ # 处理本地部分(@前面的部分)
1285
+ if len(local_part) <= 3:
1286
+ # 短邮箱名,只显示第一个字符
1287
+ masked_local = local_part[0] + '***'
1288
+ else:
1289
+ # 长邮箱名,显示前3个字符
1290
+ masked_local = local_part[:3] + '***'
1291
+
1292
+ # 处理域名部分
1293
+ if '.' in domain_part:
1294
+ domain_name, domain_ext = domain_part.rsplit('.', 1)
1295
+ if len(domain_name) <= 2:
1296
+ masked_domain = '***.' + domain_ext
1297
+ else:
1298
+ masked_domain = domain_name[:2] + '***.' + domain_ext
1299
+ else:
1300
+ masked_domain = '***'
1301
+
1302
+ return f"{masked_local}@{masked_domain}"
1303
+
1218
1304
 
1219
1305
  # Flask集成装饰器
1220
1306
  def require_auth(auth_manager):
@@ -1387,7 +1473,7 @@ def main():
1387
1473
  auth_manager = StandaloneAuthManager(db_config, auth_config)
1388
1474
 
1389
1475
  # 注册用户
1390
- result = auth_manager.register_user('admin', 'password123', 'admin', ['read', 'write', 'admin'])
1476
+ result = auth_manager.register_user('admin', 'password123', 'admin', ['read', 'write', 'admin'], 'admin@example.com')
1391
1477
  print("注册结果:", result)
1392
1478
 
1393
1479
  # 用户认证
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdbq
3
- Version: 4.0.91
3
+ Version: 4.0.93
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1,7 +1,7 @@
1
1
  mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
2
- mdbq/__version__.py,sha256=k-Ojd9XAyoffKPd5ogyvzqZF_lbLJqX_MBLGwMbk2Ew,18
2
+ mdbq/__version__.py,sha256=uqyWnqEalaBGkkETtbWm-C3KxKBrzkBLQ2EZWezDmE0,18
3
3
  mdbq/auth/__init__.py,sha256=pnPMAt63sh1B6kEvmutUuro46zVf2v2YDAG7q-jV_To,24
4
- mdbq/auth/auth_backend.py,sha256=RHWHeSjS2BMTIdtD1sV9idg2BzQ5F9AG3JS7ObFohns,57192
4
+ mdbq/auth/auth_backend.py,sha256=4fbx8PehiW0BzEeJkye6a7KQcMeD1unPr4y5CFBGANI,60691
5
5
  mdbq/auth/rate_limiter.py,sha256=e7K8pMQlZ1vm1N-c0HBH8tbAwzcmXSRiAl81JNZ369g,26192
6
6
  mdbq/js/__init__.py,sha256=hpMi3_ZKwIWkzc0LnKL-SY9AS-7PYFHq0izYTgEvxjc,30
7
7
  mdbq/js/jc.py,sha256=FOc6HOOTJwnoZLZmgmaE1SQo9rUnVhXmefhKMD2MlDA,13229
@@ -33,7 +33,7 @@ mdbq/route/routes.py,sha256=DHJg0eRNi7TKqhCHuu8ia3vdQ8cTKwrTm6mwDBtNboM,19111
33
33
  mdbq/selenium/__init__.py,sha256=AKzeEceqZyvqn2dEDoJSzDQnbuENkJSHAlbHAD0u0ZI,10
34
34
  mdbq/selenium/get_driver.py,sha256=1NTlVUE6QsyjTrVVVqTO2LOnYf578ccFWlWnvIXGtic,20903
35
35
  mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
36
- mdbq-4.0.91.dist-info/METADATA,sha256=njj-Rq_D97tP__py-GgA4nlc8D3zPvHO4D63XXqOeKg,364
37
- mdbq-4.0.91.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- mdbq-4.0.91.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
39
- mdbq-4.0.91.dist-info/RECORD,,
36
+ mdbq-4.0.93.dist-info/METADATA,sha256=zZnnDF3T05r1AkarZURpxPtEOwkuYmJ9_Hy-_0CUq7M,364
37
+ mdbq-4.0.93.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ mdbq-4.0.93.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
39
+ mdbq-4.0.93.dist-info/RECORD,,
File without changes