File size: 10,434 Bytes
f68778c
 
 
 
 
 
 
 
 
 
 
 
a0ab3de
f68778c
 
 
 
6309acf
 
f68778c
 
 
 
 
 
 
 
6309acf
 
 
 
a0ab3de
 
 
 
 
6309acf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0ab3de
 
 
6309acf
a0ab3de
 
 
 
 
 
 
 
 
 
6309acf
a0ab3de
 
 
 
 
 
 
 
 
6309acf
 
 
 
 
 
 
 
 
 
 
a0ab3de
f68778c
 
 
 
 
 
 
 
 
7e0f067
f68778c
 
 
 
 
 
 
 
 
 
 
 
7e0f067
 
 
 
 
 
f68778c
 
 
 
 
 
 
 
 
 
6309acf
f68778c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e0f067
f68778c
 
 
 
 
7e0f067
 
 
 
 
 
f68778c
 
 
 
 
 
 
 
 
 
 
 
6309acf
f68778c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# verify_code_engine.py
# ==========================================
# 📧 验证码发送引擎
# ==========================================
# 作用:提供验证码内存缓存与多渠道发送能力(邮件/短信)
# 关联文件:
#   - router_users_auth.py (调用此模块发送验证码)
#   - router_wallet.py (提现时也会调用验证码缓存)
# ==========================================

import os
import json
import time
import urllib.request
import urllib.parse
import base64

from fastapi import HTTPException

# ==========================================
# 验证码内存缓存 (全局共享字典)
# ==========================================
# 作用:存储所有待验证的验证码,格式为 {key: {code, expires_at}}
# 关联:router_users_auth.py 和 router_wallet.py 都会读写此字典
# 注意:服务重启后缓存会清空,生产环境建议改用 Redis
VERIFY_CODES = {}

# 🔒 发送频率限制配置
SEND_COOLDOWN = {}  # {contact: last_send_timestamp}
COOLDOWN_SECONDS = 60  # 同一联系方式60秒内最多发送1条验证码

# 🚀 P1性能优化:记录上次清理时间,避免频繁清理
_last_cleanup_time = 0
_CLEANUP_INTERVAL = 300  # 5分钟清理一次


def check_send_cooldown(contact: str):
    """

    🔒 检查发送频率限制

    

    作用:防止同一联系方式频繁发送验证码

    参数:

        - contact: 联系方式(邮箱或手机号)

    抛出:

        - HTTPException: 如果在冷却期内,返回429状态码

    """
    now = time.time()
    last_send = SEND_COOLDOWN.get(contact, 0)
    if now - last_send < COOLDOWN_SECONDS:
        remaining = int(COOLDOWN_SECONDS - (now - last_send))
        raise HTTPException(status_code=429, detail=f"请等待{remaining}秒后再发送验证码")
    SEND_COOLDOWN[contact] = now


def cleanup_expired_codes():
    """

    🚀 P1性能优化:清理过期验证码,防止内存泄漏

    🔒 同时清理过期的发送频率限制记录

    """
    global _last_cleanup_time
    now = time.time()
    
    # 限制清理频率,避免每次请求都清理
    if now - _last_cleanup_time < _CLEANUP_INTERVAL:
        return
    
    _last_cleanup_time = now
    
    # 收集过期的验证码键
    expired_keys = [
        key for key, data in VERIFY_CODES.items()
        if now > data.get("expires_at", data.get("expires", 0))
    ]
    
    # 删除过期验证码
    for key in expired_keys:
        del VERIFY_CODES[key]
    
    # 🔒 清理过期的发送频率限制记录,防止内存泄漏
    expired_cooldown_contacts = [
        contact for contact, last_send in SEND_COOLDOWN.items()
        if now - last_send >= COOLDOWN_SECONDS
    ]
    for contact in expired_cooldown_contacts:
        del SEND_COOLDOWN[contact]
    
    total_cleaned = len(expired_keys) + len(expired_cooldown_contacts)
    if total_cleaned > 0:
        print(f"🧹 已清理 {len(expired_keys)} 个过期验证码,{len(expired_cooldown_contacts)} 个过期冷却记录,当前缓存: {len(VERIFY_CODES)} 条验证码,{len(SEND_COOLDOWN)} 条冷却记录")


def send_email_code(to_email: str, code: str, action: str):
    """

    📧 邮件验证码发送函数

    

    作用:通过 Make.com Webhook 触发邮件发送(无需 SMTP 服务器)

    参数:

        - to_email: 目标邮箱地址

        - code: 6位数字验证码

        - action: 动作类型 ("register" 注册 / "reset" 重置密码 / "withdraw" 提现)

    关联:

        - 环境变量 MAKE_WEBHOOK_URL (在 HF Space Settings 中配置)

        - router_users_auth.py 的 send_verify_code() 异步调用此函数

    """
    # 从环境变量读取 Make.com Webhook 地址
    webhook_url = os.environ.get("MAKE_WEBHOOK_URL")

    if not webhook_url:
        print("警告: 未配置 MAKE_WEBHOOK_URL,跳过邮件发送")
        return

    # 根据动作类型生成不同的邮件标题
    if action == "register":
        action_str = "注册账号"
    elif action == "withdraw":
        action_str = "提现操作"
    else:
        action_str = "修改/找回密码"
    subject = f"ComfyUI 社区 - {action_str}验证码"
    
    # 构建 HTML 格式的邮件正文(美化样式)
    html_content = f"""

    <div style="background:#f9f9f9; padding:20px; font-family:sans-serif;">

        <div style="background:#fff; padding:20px; border-radius:8px; max-width:500px; margin:0 auto; box-shadow:0 2px 10px rgba(0,0,0,0.05);">

            <h2 style="color:#4CAF50; margin-top:0;">ComfyUI 社区精选</h2>

            <p>您好,</p>

            <p>您正在请求<strong>{action_str}</strong>,您的验证码是:</p>

            <div style="font-size:24px; font-weight:bold; color:#2196F3; background:#e3f2fd; padding:15px; text-align:center; border-radius:6px; letter-spacing: 5px;">{code}</div>

            <p style="color:#888; font-size:12px; margin-top:20px;">该验证码在 5 分钟内有效。如非本人操作,请忽略此邮件。</p>

        </div>

    </div>

    """

    # 组装 Webhook 请求载荷
    data = {
        "to": to_email,
        "subject": subject,
        "html": html_content
    }

    # 发送 HTTP POST 请求触发 Webhook
    req = urllib.request.Request(
        webhook_url, 
        data=json.dumps(data).encode('utf-8'), 
        headers={'Content-Type': 'application/json'}
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as response:
            print(f"✅ 成功触发 Webhook 发送验证码 {code}{to_email}")
    except Exception as e:
        print(f"❌ Webhook 触发失败: {e}")


def send_sms_code(phone: str, code: str, action: str):
    """

    📱 短信验证码发送函数(双引擎:Twilio + 阿里云)

    

    作用:支持海外用户(Twilio)和国内用户(阿里云)的短信发送

    参数:

        - phone: 手机号(需带国际区号,如 +86)

        - code: 6位数字验证码

        - action: 动作类型 ("register" 注册 / "reset" 重置密码 / "withdraw" 提现)

    关联:

        - 环境变量 TWILIO_SID, TWILIO_TOKEN, TWILIO_FROM (Twilio配置)

        - 环境变量 ALIYUN_AK, ALIYUN_SK, ALIYUN_SIGN_NAME (阿里云配置)

        - router_users_auth.py 的 send_verify_code() 异步调用此函数

    """
    if action == "register":
        action_str = "注册账号"
    elif action == "withdraw":
        action_str = "提现操作"
    else:
        action_str = "修改/找回密码"

    # ==========================================
    # 引擎 A:Twilio (海外优先,无需SDK,纯HTTP接口)
    # ==========================================
    # 作用:为海外用户或测试环境提供短信能力
    # 关联:需在 HF Space Settings 配置 TWILIO_SID, TWILIO_TOKEN, TWILIO_FROM
    twilio_sid = os.environ.get("TWILIO_SID")
    if twilio_sid:
        token = os.environ.get("TWILIO_TOKEN")
        from_phone = os.environ.get("TWILIO_FROM")
        
        # 构建短信内容
        body = f"【ComfyUI社区】您正在请求{action_str},验证码是:{code},5分钟内有效。"
        url = f"https://api.twilio.com/2010-04-01/Accounts/{twilio_sid}/Messages.json"
        
        # Twilio 使用 Basic Auth 认证
        auth = base64.b64encode(f"{twilio_sid}:{token}".encode('utf-8')).decode('utf-8')
        data = urllib.parse.urlencode({'To': phone, 'From': from_phone, 'Body': body}).encode('utf-8')
        
        req = urllib.request.Request(url, data=data)
        req.add_header("Authorization", f"Basic {auth}")
        try:
            with urllib.request.urlopen(req, timeout=10) as response:
                print(f"✅ Twilio 短信已成功下发至 {phone}")
        except Exception as e:
            print(f"❌ Twilio 发送失败: {e}")
        return

    # ==========================================
    # 引擎 B:阿里云 (国内首选,到达率最高)
    # ==========================================
    # 作用:为国内用户提供短信能力,需要已备案的签名和模板
    # 关联:需在 HF Space Settings 配置 ALIYUN_AK, ALIYUN_SK 等
    aliyun_ak = os.environ.get("ALIYUN_AK")
    if aliyun_ak:
        # 阿里云SDK需要单独安装,这里做动态导入
        try:
            from alibabacloud_dysmsapi20170525.client import Client as DysmsapiClient
            from alibabacloud_tea_openapi import models as open_api_models
            from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
        except ImportError:
            print("❌ 缺少阿里云 SDK,请在 requirements.txt 中添加 alibabacloud_dysmsapi20170525")
            return

        sk = os.environ.get("ALIYUN_SK")
        sign_name = os.environ.get("ALIYUN_SIGN_NAME")  # 短信签名,如 "阿里云"
        # 根据动作类型选择不同的模板
        tpl_code = os.environ.get("ALIYUN_TPL_REGISTER") if action == "register" else os.environ.get("ALIYUN_TPL_RESET")

        # 初始化阿里云客户端
        config = open_api_models.Config(access_key_id=aliyun_ak, access_key_secret=sk)
        config.endpoint = 'dysmsapi.aliyuncs.com'
        client = DysmsapiClient(config)

        # 构建发送请求
        send_req = dysmsapi_models.SendSmsRequest(
            phone_numbers=phone,
            sign_name=sign_name,
            template_code=tpl_code,
            template_param=json.dumps({"code": code})  # 模板参数
        )
        try:
            response = client.send_sms(send_req)
            if response.body.code == "OK":
                print(f"✅ 阿里云短信已成功下发至 {phone}")
            else:
                print(f"❌ 阿里云下发失败: {response.body.message}")
        except Exception as e:
            print(f"❌ 阿里云请求异常: {e}")
        return

    # ==========================================
    # 降级模式:控制台打印模拟
    # ==========================================
    # 作用:当没有配置任何短信服务时,仅在控制台打印验证码(仅限开发测试)
    print(f"⚠️ 未配置短信秘钥,模拟下发 -> 手机号: {phone}, 验证码: {code}")