rainycode 1.0.9__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.
Files changed (43) hide show
  1. {rainycode-1.0.9 → rainycode-1.1.1}/PKG-INFO +1 -1
  2. rainycode-1.1.1/common_base/consts.py +9 -0
  3. {rainycode-1.0.9 → rainycode-1.1.1}/common_depends/auth_depend.py +3 -2
  4. {rainycode-1.0.9 → rainycode-1.1.1}/common_models/base_model.py +2 -2
  5. rainycode-1.1.1/common_servers/api/auth_api.py +85 -0
  6. rainycode-1.1.1/common_servers/api/wechat_api.py +98 -0
  7. {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/schemas/auth_schema.py +5 -5
  8. rainycode-1.1.1/common_servers/service/auth_svc.py +550 -0
  9. {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/bcrypt_util.py +2 -4
  10. {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/ip_util.py +4 -4
  11. {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/wechat_util.py +23 -1
  12. {rainycode-1.0.9 → rainycode-1.1.1}/core/base_config.py +4 -1
  13. {rainycode-1.0.9 → rainycode-1.1.1}/core/databases/aiodb.py +1 -4
  14. {rainycode-1.0.9 → rainycode-1.1.1}/core/databases/aioredis.py +1 -1
  15. {rainycode-1.0.9 → rainycode-1.1.1}/core/start.py +2 -2
  16. {rainycode-1.0.9 → rainycode-1.1.1}/pyproject.toml +1 -1
  17. {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/PKG-INFO +1 -1
  18. {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/SOURCES.txt +6 -9
  19. {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/top_level.txt +1 -1
  20. rainycode-1.0.9/common_base/consts.py +0 -3
  21. rainycode-1.0.9/common_servers/api/auth_api.py +0 -50
  22. rainycode-1.0.9/common_servers/api/wechat_api.py +0 -48
  23. rainycode-1.0.9/common_servers/service/auth_svc.py +0 -131
  24. rainycode-1.0.9/common_servers/strategies/base_strategy.py +0 -9
  25. rainycode-1.0.9/common_servers/strategies/password_strategy.py +0 -34
  26. rainycode-1.0.9/common_servers/strategies/wechat_stragegy.py +0 -141
  27. {rainycode-1.0.9 → rainycode-1.1.1}/common_base/aiorequests.py +0 -0
  28. {rainycode-1.0.9 → rainycode-1.1.1}/common_base/exception.py +0 -0
  29. {rainycode-1.0.9 → rainycode-1.1.1}/common_base/logging.py +0 -0
  30. {rainycode-1.0.9 → rainycode-1.1.1}/common_base/response.py +0 -0
  31. {rainycode-1.0.9 → rainycode-1.1.1}/common_models/__init__.py +0 -0
  32. {rainycode-1.0.9 → rainycode-1.1.1}/common_models/user_model.py +0 -0
  33. {rainycode-1.0.9 → rainycode-1.1.1}/common_models/wechat_model.py +0 -0
  34. {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/router.py +0 -0
  35. {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/schemas/__init__.py +0 -0
  36. {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/service/wechat_svc.py +0 -0
  37. {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/captcha_util.py +0 -0
  38. {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/jwt_util.py +0 -0
  39. {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/snowflake_util.py +0 -0
  40. {rainycode-1.0.9 → rainycode-1.1.1}/core/middleware/http_middleware.py +0 -0
  41. {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/dependency_links.txt +0 -0
  42. {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/requires.txt +0 -0
  43. {rainycode-1.0.9 → rainycode-1.1.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainycode
3
- Version: 1.0.9
3
+ Version: 1.1.1
4
4
  Summary: FastAPI base modules
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: build==1.4.0
@@ -0,0 +1,9 @@
1
+ REDIS_PREFIX_REFRESH_TOKEN: str = "auth:refresh:"
2
+ REDIS_PREFIX_BLOCKLIST: str = "auth:blocklist:"
3
+ REDIS_PREFIX_USER_INFO: str = "user:info:"
4
+ REDIS_PREFIX_OAUTH_STATE: str = "oauth:state:"
5
+ REDIS_PREFIX_WECHAT_CODE: str = "wechat:code:"
6
+ REDIS_PREFIX_WECHAT_OAUTH_CODE: str = "wechat:oauth:code:"
7
+ REDIS_PREFIX_TEMP_CODE: str = "oauth:temp_code:"
8
+ REDIS_PREFIX_LOGIN_LOCK_USER: str = "auth:login_lock:user:"
9
+ REDIS_PREFIX_LOGIN_LOCK_IP: str = "auth:login_lock:ip:"
@@ -4,7 +4,7 @@ from fastapi.security import OAuth2PasswordBearer, SecurityScopes
4
4
  from common_base.consts import REDIS_PREFIX_BLOCKLIST, REDIS_PREFIX_USER_INFO
5
5
  from common_base.exception import CustomException
6
6
  from common_models.user_model import User
7
- from common_utlis.jwt_util import JwtUtil
7
+ from common_utils.jwt_util import JwtUtil
8
8
  from core.databases.aioredis import AioRedis
9
9
 
10
10
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth_center/login")
@@ -53,6 +53,7 @@ async def get_current_user(request: Request, token: str, security_scopes: Securi
53
53
 
54
54
  # 设置请求上下文
55
55
  request.state.user = user
56
+ request.state.token = token
56
57
  return user
57
58
 
58
59
 
@@ -71,5 +72,5 @@ async def login_or_none(request: Request, security_scopes: SecurityScopes, token
71
72
 
72
73
  try:
73
74
  return await get_current_user(request, token, security_scopes)
74
- except Exception as e:
75
+ except CustomException:
75
76
  return None
@@ -5,7 +5,7 @@ from typing import Optional, Any
5
5
  from tortoise import fields, BaseDBAsyncClient
6
6
  from tortoise.expressions import Q
7
7
  from tortoise.queryset import BulkCreateQuery, QuerySet, QuerySetSingle
8
- from common_utlis.snowflake_util import get_snowflake_id
8
+ from common_utils.snowflake_util import get_snowflake_id
9
9
  from core.databases.aiodb import MySQLUtils
10
10
 
11
11
 
@@ -86,7 +86,7 @@ class BaseModel(MySQLUtils):
86
86
  force_update: bool = False,
87
87
  **kwargs: Any
88
88
  ) -> None:
89
- if not getattr(self, 'id', 0) and kwargs.get('uuid', False) and 'id' in self._meta.fields:
89
+ if not getattr(self, 'id', 0) and kwargs.get('id_uuid', False) and 'id' in self._meta.fields:
90
90
  setattr(self, 'id', str(uuid.uuid4()).replace('-', ''))
91
91
  return await super().save(using_db=using_db, update_fields=update_fields, force_create=force_create,
92
92
  force_update=force_update)
@@ -0,0 +1,85 @@
1
+ from common_models.user_model import User
2
+ from fastapi import APIRouter, Security, Body, Depends, Query, Request
3
+ from common_base.response import SuccessReturn
4
+ from common_depends.auth_depend import login_require, oauth2_scheme
5
+ from common_servers.schemas.auth_schema import *
6
+ from common_servers.service.auth_svc import AuthService
7
+
8
+ router = APIRouter(prefix="/auth")
9
+
10
+ @router.post("/register", summary="用户注册")
11
+ async def register(user_in: UserRegister):
12
+ user = await AuthService.register(user_in)
13
+ return SuccessReturn(
14
+ data={"id": str(user.id), "username": user.username}, msg="注册成功"
15
+ )
16
+
17
+
18
+ @router.post("/login", summary="用户密码登录")
19
+ async def login(user_in: UserLogin, request: Request):
20
+ client_ip = request.client.host if request.client else "unknown"
21
+ token_data = await AuthService.login(user_in, client_ip)
22
+ return SuccessReturn(data=token_data, msg="登录成功")
23
+
24
+
25
+ @router.post("/refresh", response_model=TokenResponse, summary="刷新令牌")
26
+ async def refresh_token(refresh_token: str = Body(..., embed=True)):
27
+ token_data = await AuthService.refresh_token(refresh_token)
28
+ return SuccessReturn(data=token_data, msg="刷新成功")
29
+
30
+
31
+ @router.post("/logout", summary="登出")
32
+ async def logout(
33
+ request: Request,
34
+ refresh_token: str = Body(..., embed=True),
35
+ _: User = Security(login_require),
36
+ ):
37
+ token = request.state.token
38
+ await AuthService.logout(token, refresh_token)
39
+ return SuccessReturn(msg="登出成功")
40
+
41
+
42
+ @router.get("/me", response_model=UserInfo, summary="获取当前用户信息")
43
+ async def get_me(user: User = Security(login_require)):
44
+ return SuccessReturn(data=user)
45
+
46
+
47
+ @router.post("/wechat/mini", response_model=TokenResponse, summary="微信小程序登录")
48
+ async def login_mini(wechat_in: WechatLogin):
49
+ """
50
+ 微信小程序登录
51
+
52
+ - 静默登录: 传 code + app_id
53
+ - 一键登录(获取手机号): 额外传 encrypted_data + iv
54
+ """
55
+ token_data = await AuthService.login_wechat_mini(wechat_in)
56
+ return SuccessReturn(data=token_data, msg="登录成功")
57
+
58
+
59
+ @router.get("/wechat/oauth", summary="获取微信网页授权链接 - 回调(/oauth/callback)")
60
+ async def get_wechat_oauth_url(
61
+ app_id: str = Query(..., description="微信 AppID"),
62
+ redirect_uri: str = Query(..., description="授权成功后跳转地址"),
63
+ state: str | None = Query(default=None, description="自定义状态参数"),
64
+ ):
65
+ """
66
+ 获取微信网页授权链接(snsapi_base 静默授权)
67
+
68
+ - 首次请求返回 snsapi_base 授权链接
69
+ - 如果用户已注册,回调后直接返回 token
70
+ - 如果用户未注册,自动重定向到 snsapi_userinfo 授权
71
+ """
72
+ authorize_url = await AuthService.get_wechat_oauth_url(app_id, redirect_uri, state)
73
+ return SuccessReturn(data={"authorize_url": authorize_url})
74
+
75
+
76
+ @router.get("/wechat/token", summary="通过临时授权码获取 token")
77
+ async def get_token_by_temp_code(code: str = Query(..., description="临时授权码")):
78
+ """
79
+ OAuth 回调后,前端使用临时码换取 token
80
+
81
+ - 临时码有效期:120 秒
82
+ - 一次性使用,获取后立即失效
83
+ """
84
+ token_data = await AuthService.get_token_by_temp_code(code)
85
+ return SuccessReturn(data=token_data, msg="获取成功")
@@ -0,0 +1,98 @@
1
+ import json
2
+ from urllib.parse import urlencode
3
+ from fastapi import APIRouter, Request, Query, Response
4
+ from fastapi.responses import RedirectResponse
5
+ from common_base.consts import REDIS_PREFIX_OAUTH_STATE
6
+ from common_base.exception import CustomException
7
+ from common_servers.service.auth_svc import AuthService
8
+ from common_servers.service.wechat_svc import WechatService
9
+ from core.databases.aioredis import AioRedis
10
+
11
+ router = APIRouter(prefix="/wechat")
12
+
13
+
14
+ @router.get("/callback/{app_pk}", summary="微信回调验证")
15
+ async def verify_callback(
16
+ app_pk: int,
17
+ signature: str = Query(..., description="微信加密签名"),
18
+ timestamp: str = Query(..., description="时间戳"),
19
+ nonce: str = Query(..., description="随机数"),
20
+ echostr: str = Query(..., description="随机字符串"),
21
+ ):
22
+ """
23
+ 微信服务器配置验证接口
24
+ """
25
+ is_valid = await WechatService.verify_signature(app_pk, signature, timestamp, nonce)
26
+ if is_valid:
27
+ # 必须直接返回 echostr 字符串,不能包含 json 格式
28
+ return Response(content=echostr, media_type="text/plain")
29
+ return Response(content="fail", media_type="text/plain")
30
+
31
+
32
+ @router.post("/callback/{app_pk}", summary="微信消息回调")
33
+ async def handle_callback(
34
+ app_pk: int,
35
+ request: Request,
36
+ signature: str = Query(..., description="微信加密签名"),
37
+ timestamp: str = Query(..., description="时间戳"),
38
+ nonce: str = Query(..., description="随机数"),
39
+ openid: str = Query(None, description="用户OpenID"),
40
+ encrypt_type: str = Query(None, description="加密类型"),
41
+ msg_signature: str = Query(None, description="消息体签名"),
42
+ ):
43
+ """
44
+ 接收微信推送的消息和事件
45
+ """
46
+ # 获取原始 XML 数据
47
+ body = await request.body()
48
+ xml_data = body.decode("utf-8")
49
+
50
+ # 目前仅实现了明文模式,加密模式需要结合 msg_secret 解密
51
+ # 如果 encrypt_type == "aes",需要解密 (暂未实现,TODO)
52
+ result = await WechatService.handle_message(
53
+ app_pk, xml_data, signature, timestamp, nonce
54
+ )
55
+ return Response(content=result, media_type="application/xml")
56
+
57
+
58
+ @router.get("/oauth/callback", summary="微信网页授权回调")
59
+ async def wechat_oauth_callback(
60
+ code: str | None = Query(default=None, description="微信授权码"),
61
+ state: str | None = Query(default=None, description="状态参数"),
62
+ error: str | None = Query(default=None, description="错误码"),
63
+ error_description: str | None = Query(default=None, description="错误描述"),
64
+ ):
65
+ """
66
+ 处理微信网页授权回调
67
+
68
+ - 用户已存在:重定向到前端(带 token)
69
+ - 用户不存在:自动重定向到 snsapi_userinfo 授权
70
+ - 用户拒绝授权:重定向到前端并附带错误信息
71
+ """
72
+ # 处理用户拒绝授权的情况
73
+ if error:
74
+ # 获取 redirect_uri
75
+ cache_key = f"{REDIS_PREFIX_OAUTH_STATE}{state}"
76
+ cache_value = await AioRedis.get(cache_key)
77
+ if cache_value:
78
+ try:
79
+ cached_data = json.loads(cache_value)
80
+ redirect_uri = cached_data.get("redirect_uri")
81
+ except (json.JSONDecodeError, TypeError):
82
+ redirect_uri = None
83
+
84
+ if redirect_uri:
85
+ await AioRedis.delete(cache_key)
86
+ params = {
87
+ "error": error,
88
+ "error_description": error_description or "用户拒绝授权",
89
+ }
90
+ error_url = f"{redirect_uri}?{urlencode(params)}"
91
+ return RedirectResponse(url=error_url, status_code=302)
92
+
93
+ raise CustomException(msg="授权失败")
94
+
95
+ if not code:
96
+ raise CustomException(msg="缺少授权码")
97
+
98
+ return await AuthService.handle_oauth_callback(code, state)
@@ -36,8 +36,8 @@ class UserInfo(BaseModel):
36
36
  from_attributes = True
37
37
 
38
38
  class WechatLogin(BaseModel):
39
- """微信登录模型"""
40
- code: str = Field(..., description="微信授权Code")
41
- app_id: str = Field(..., description="微信AppID (如 wx...)")
42
- encrypted_data: str | None = Field(default=None, description="加密数据(小程序一键登录用)")
43
- iv: str | None = Field(default=None, description="加密算法的初始向量")
39
+ """微信小程序登录模型"""
40
+ code: str = Field(..., description="微信登录Code (wx.login获取)")
41
+ app_id: str = Field(..., description="小程序AppID")
42
+ encrypted_data: str | None = Field(default=None, description="加密数据(一键登录手机号)")
43
+ iv: str | None = Field(default=None, description="加密算法初始向量")