File size: 15,698 Bytes
088dffd
02886ab
088dffd
 
d3ae963
 
998c1ee
 
1ed36f1
 
088dffd
 
d3ae963
088dffd
 
 
d3ae963
02886ab
d3ae963
 
 
 
998c1ee
 
d3ae963
998c1ee
 
d3ae963
 
 
998c1ee
 
 
 
 
 
 
 
 
 
 
 
 
d3ae963
998c1ee
 
 
 
 
 
 
 
 
 
 
d3ae963
998c1ee
 
d3ae963
998c1ee
d3ae963
 
1ed36f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3ae963
1ed36f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3ae963
02886ab
c76aa8d
 
 
 
 
 
 
 
 
 
 
 
 
 
d3ae963
 
 
 
 
 
 
 
 
02886ab
d3ae963
02886ab
d3ae963
 
 
02886ab
d3ae963
088dffd
 
 
d3ae963
1ed36f1
d3ae963
 
 
 
088dffd
 
 
 
 
 
 
1ed36f1
 
088dffd
1ed36f1
d3ae963
088dffd
02886ab
088dffd
 
 
 
 
 
 
 
 
 
 
 
 
6f7ceef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
088dffd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3ae963
 
 
 
 
 
 
 
 
 
 
 
088dffd
 
d3ae963
 
088dffd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90d7999
 
 
 
 
 
 
 
 
 
088dffd
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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# router_users.py
from fastapi import APIRouter, HTTPException, BackgroundTasks
import time
import re
import random
import os
import json
import urllib.request
import urllib.parse
import base64
import 数据库连接 as db
from notifications import add_notification
from models import UserRegister, UserLogin, UserUpdate, PasswordReset, FollowToggle, PrivacySettings, SendCodeRequest

router = APIRouter()

# ==========================================
# 验证码内存缓存与发送引擎
# ==========================================
VERIFY_CODES = {} 

def send_email_code(to_email: str, code: str, action: str):
    """利用自动化 Webhook 作为 HTTPS 到 SMTP 的代理桥梁"""
    webhook_url = os.environ.get("MAKE_WEBHOOK_URL")

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

    action_str = "注册账号" if action == "register" else "修改/找回密码"
    subject = f"ComfyUI 社区 - {action_str}验证码"
    
    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;">该验证码在 10 分钟内有效。如非本人操作,请忽略此邮件。</p>

        </div>

    </div>

    """

    data = {
        "to": to_email,
        "subject": subject,
        "html": html_content
    }

    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 的双引擎短信发送路由"""
    action_str = "注册账号" if action == "register" else "修改/找回密码"

    # ==========================================
    # 引擎 A:Twilio (无需装 SDK,走纯 HTTP 接口,适合海外或免审核测试)
    # ==========================================
    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},10分钟内有效。"
        url = f"https://api.twilio.com/2010-04-01/Accounts/{twilio_sid}/Messages.json"
        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:阿里云 (国内首选,到达率最高)
    # ==========================================
    aliyun_ak = os.environ.get("ALIYUN_AK")
    if aliyun_ak:
        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}")

# ==========================================
# 接口路由
# ==========================================
@router.post("/api/users/send-code")
async def send_verify_code(req: SendCodeRequest, bg_tasks: BackgroundTasks):
    if req.action_type == "reset":
        if not req.account:
            raise HTTPException(status_code=400, detail="找回密码需先填写当前账号")
        
        users_db = db.load_data("users.json", default_data={})
        user = users_db.get(req.account)
        if not user:
            raise HTTPException(status_code=404, detail="该账号不存在")
            
        if req.contact_type == "email" and user.get("email") != req.contact:
            raise HTTPException(status_code=400, detail="填写的邮箱与该账号绑定的邮箱不一致")
        if req.contact_type == "phone" and user.get("phone") != req.contact:
            raise HTTPException(status_code=400, detail="填写的手机号与该账号绑定的手机号不一致")

    code = str(random.randint(100000, 999999))
    cache_key = f"{req.contact}_{req.action_type}"
    
    VERIFY_CODES[cache_key] = {
        "code": code,
        "expires_at": int(time.time()) + 600
    }
    
    if req.contact_type == "email":
        bg_tasks.add_task(send_email_code, req.contact, code, req.action_type)
    elif req.contact_type == "phone":
        bg_tasks.add_task(send_sms_code, req.contact, code, req.action_type)
    else:
        raise HTTPException(status_code=400, detail="不支持的验证方式")
        
    return {"status": "success", "message": "验证码发送请求已提交"}

@router.post("/api/users/register")
async def register_user(user: UserRegister):
    users_db = db.load_data("users.json", default_data={})
    
    cache_key = f"{user.email}_register" if user.email else f"{user.phone}_register"
    cached = VERIFY_CODES.get(cache_key)
    if not cached or cached["code"] != user.code or int(time.time()) > cached["expires_at"]:
        raise HTTPException(status_code=400, detail="验证码不正确或已过期")
        
    if len(user.account) <= 5: raise HTTPException(status_code=400, detail="账号必须大于5个字符")
    if not re.match(r'^[a-zA-Z0-9_]{6,20}$', user.account): raise HTTPException(status_code=400, detail="账号仅支持大小写英文字母、数字及下划线")
    if len(user.password) < 6: raise HTTPException(status_code=400, detail="密码必须大于等于6个字符")
    if not re.match(r'^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]{6,}$', user.password): raise HTTPException(status_code=400, detail="密码包含不支持的特殊字符")
    if user.intro and len(user.intro) > 100: raise HTTPException(status_code=400, detail="个人介绍不能超过100个字符")
    if user.account in users_db: raise HTTPException(status_code=400, detail="该账号已被注册")
    for existing_user in users_db.values():
        if user.email and existing_user.get("email") == user.email: raise HTTPException(status_code=400, detail="该邮箱已被绑定")
        if user.phone and existing_user.get("phone") == user.phone: raise HTTPException(status_code=400, detail="该手机号已被绑定")
    
    VERIFY_CODES.pop(cache_key, None) 
    
    new_user = user.dict()
    new_user.pop("code", None)
    new_user.update({"created_at": int(time.time()), "followers": [], "following": [], "privacy": {"follows": False, "likes": False, "favorites": False, "downloads": False}})
    users_db[user.account] = new_user
    db.save_data("users.json", users_db)
    return {"status": "success", "message": "注册成功", "data": {k: v for k, v in new_user.items() if k != "password"}}

@router.post("/api/users/login")
async def login_user(user: UserLogin):
    users_db = db.load_data("users.json", default_data={})
    if user.account not in users_db: raise HTTPException(status_code=404, detail="账号不存在")
    user_data = users_db[user.account]
    if user_data.get("password") != user.password: raise HTTPException(status_code=401, detail="密码错误")
    return {"status": "success", "token": f"mock_token_{user.account}", "account": user.account, "name": user_data["name"], "avatar": user_data.get("avatarDataUrl", "https://via.placeholder.com/150")}

# ==========================================
# 补充:发送验证码的 API 接口
# ==========================================
@router.post("/api/users/send_code")
async def send_code_api(req: SendCodeRequest):
    # 1. 生成 6 位随机数字验证码
    code = str(random.randint(100000, 999999))
    
    # 2. 生成缓存 Key (格式: 邮箱地址_动作)
    # 例如: test@qq.com_register 或 test@qq.com_reset
    key = f"{req.contact}_{req.action_type}"
    
    # 3. 存入全局内存字典 (设置有效期为 10 分钟 = 600 秒)
    VERIFY_CODES[key] = {
        "code": code,
        "expires": time.time() + 600
    }
    
    # 4. 触发发送逻辑
    if req.contact_type == "email":
        try:
            # 调用你文件里已经写好的通过 Make.com Webhook 发邮件的函数
            send_email_code(req.contact, code, req.action_type)
            return {"status": "success", "message": "验证码已成功发送至邮箱"}
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"邮件发送失败: {str(e)}")
            
    elif req.contact_type == "phone":
        # 预留的短信通道
        return {"status": "success", "message": "验证码已成功发送至手机"}
        
    else:
        raise HTTPException(status_code=400, detail="不支持的验证方式")

@router.get("/api/users/{account}")
async def get_user_profile(account: str):
    users_db = db.load_data("users.json", default_data={})
    if account not in users_db: raise HTTPException(status_code=404, detail="用户不存在")
    user_data = users_db[account]
    items_db = db.load_data("items.json", default_data=[])
    user_items = [item for item in items_db if item.get("author") == account]
    user_data["receivedLikes"] = sum(item.get("likes", 0) for item in user_items)
    user_data["receivedFavorites"] = sum(item.get("favorites", 0) for item in user_items)
    user_data["receivedUses"] = sum(item.get("uses", 0) for item in user_items)
    return {"status": "success", "data": {k: v for k, v in user_data.items() if k != "password"}}

@router.put("/api/users/{account}")
async def update_user_profile(account: str, update_data: UserUpdate):
    users_db = db.load_data("users.json", default_data={})
    if account not in users_db: raise HTTPException(status_code=404, detail="用户不存在")
    if update_data.intro and len(update_data.intro) > 100: raise HTTPException(status_code=400, detail="个人介绍不能超过100个字符")
    user = users_db[account]
    for k, v in update_data.dict(exclude_unset=True).items():
        if v is not None: user[k] = v
    db.save_data("users.json", users_db)
    return {"status": "success", "data": {k: v for k, v in user.items() if k != "password"}}

@router.post("/api/users/{account}/reset-password")
async def reset_password(account: str, pwd_data: PasswordReset):
    users_db = db.load_data("users.json", default_data={})
    if account not in users_db: raise HTTPException(status_code=404, detail="用户不存在")
    user = users_db[account]
    
    if pwd_data.verify_type == "email" and user.get("email") != pwd_data.verify_contact:
        raise HTTPException(status_code=400, detail="填写的邮箱与该账号绑定的邮箱不匹配")
    if pwd_data.verify_type == "phone" and user.get("phone") != pwd_data.verify_contact:
        raise HTTPException(status_code=400, detail="填写的手机号与该账号绑定的手机号不匹配")

    cache_key = f"{pwd_data.verify_contact}_reset"
    cached = VERIFY_CODES.get(cache_key)
    if not cached or cached["code"] != pwd_data.code or int(time.time()) > cached["expires_at"]:
        raise HTTPException(status_code=400, detail="验证码不正确或已过期")

    if len(pwd_data.new_password) < 6: raise HTTPException(status_code=400, detail="新密码必须大于等于6个字符")
    if not re.match(r'^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]{6,}$', pwd_data.new_password): raise HTTPException(status_code=400, detail="新密码包含不支持的特殊字符")
    
    VERIFY_CODES.pop(cache_key, None)
    user["password"] = pwd_data.new_password
    db.save_data("users.json", users_db)
    return {"status": "success"}

@router.post("/api/users/follow")
async def toggle_follow(follow: FollowToggle):
    users_db = db.load_data("users.json", default_data={})
    if follow.target_account not in users_db or follow.user_id not in users_db: raise HTTPException(status_code=404, detail="用户不存在")
    target_followers = users_db[follow.target_account].setdefault("followers", [])
    current_following = users_db[follow.user_id].setdefault("following", [])
    if follow.is_active:
        if follow.user_id not in target_followers: 
            target_followers.append(follow.user_id)
            add_notification(follow.target_account, {"type": "follow", "from_user": follow.user_id})
        if follow.target_account not in current_following: current_following.append(follow.target_account)
    else:
        if follow.user_id in target_followers: target_followers.remove(follow.user_id)
        if follow.target_account in current_following: current_following.remove(follow.target_account)
    db.save_data("users.json", users_db)
    return {"status": "success"}

@router.put("/api/users/{account}/privacy")
async def update_privacy(account: str, privacy: PrivacySettings):
    users_db = db.load_data("users.json", default_data={})
    if account not in users_db: 
        raise HTTPException(status_code=404, detail="用户不存在")
    
    users_db[account]["privacy"] = privacy.dict()
    db.save_data("users.json", users_db)
    return {"status": "success"}