Spaces:
Running
Running
新增验证码功能
Browse files- models.py +11 -1
- router_users.py +104 -3
models.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
from typing import Optional
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
class UserRegister(BaseModel):
|
| 5 |
account: str
|
| 6 |
password: str
|
|
@@ -8,6 +14,7 @@ class UserRegister(BaseModel):
|
|
| 8 |
phone: str
|
| 9 |
name: str
|
| 10 |
gender: str
|
|
|
|
| 11 |
avatarDataUrl: Optional[str] = None
|
| 12 |
age: Optional[int] = None
|
| 13 |
country: Optional[str] = None
|
|
@@ -28,8 +35,11 @@ class UserUpdate(BaseModel):
|
|
| 28 |
avatarDataUrl: Optional[str] = None
|
| 29 |
|
| 30 |
class PasswordReset(BaseModel):
|
| 31 |
-
old_password: str
|
| 32 |
new_password: str
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
class InteractionToggle(BaseModel):
|
| 35 |
item_id: str
|
|
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
from typing import Optional
|
| 3 |
|
| 4 |
+
# 【新增】:发送验证码的请求模型
|
| 5 |
+
class SendCodeRequest(BaseModel):
|
| 6 |
+
contact: str
|
| 7 |
+
contact_type: str # "email" 或 "phone"
|
| 8 |
+
action_type: str # "register" 或 "reset"
|
| 9 |
+
|
| 10 |
class UserRegister(BaseModel):
|
| 11 |
account: str
|
| 12 |
password: str
|
|
|
|
| 14 |
phone: str
|
| 15 |
name: str
|
| 16 |
gender: str
|
| 17 |
+
code: str # 【新增】:注册验证码
|
| 18 |
avatarDataUrl: Optional[str] = None
|
| 19 |
age: Optional[int] = None
|
| 20 |
country: Optional[str] = None
|
|
|
|
| 35 |
avatarDataUrl: Optional[str] = None
|
| 36 |
|
| 37 |
class PasswordReset(BaseModel):
|
| 38 |
+
old_password: Optional[str] = None # 找回密码时此项可为空
|
| 39 |
new_password: str
|
| 40 |
+
verify_contact: str # 【新增】:接收验证码的邮箱或手机号
|
| 41 |
+
verify_type: str # 【新增】:"email" 或 "phone"
|
| 42 |
+
code: str # 【新增】:重置验证码
|
| 43 |
|
| 44 |
class InteractionToggle(BaseModel):
|
| 45 |
item_id: str
|
router_users.py
CHANGED
|
@@ -2,15 +2,95 @@
|
|
| 2 |
from fastapi import APIRouter, HTTPException
|
| 3 |
import time
|
| 4 |
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import 数据库连接 as db
|
| 6 |
from notifications import add_notification
|
| 7 |
-
from models import UserRegister, UserLogin, UserUpdate, PasswordReset, FollowToggle, PrivacySettings
|
| 8 |
|
| 9 |
router = APIRouter()
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
@router.post("/api/users/register")
|
| 12 |
async def register_user(user: UserRegister):
|
| 13 |
users_db = db.load_data("users.json", default_data={})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
if len(user.account) <= 5: raise HTTPException(status_code=400, detail="账号必须大于5个字符")
|
| 15 |
if not re.match(r'^[a-zA-Z0-9_]{6,20}$', user.account): raise HTTPException(status_code=400, detail="账号仅支持大小写英文字母、数字及下划线")
|
| 16 |
if len(user.password) < 6: raise HTTPException(status_code=400, detail="密码必须大于等于6个字符")
|
|
@@ -21,7 +101,11 @@ async def register_user(user: UserRegister):
|
|
| 21 |
if existing_user.get("email") == user.email: raise HTTPException(status_code=400, detail="该邮箱已被绑定")
|
| 22 |
if existing_user.get("phone") == user.phone: raise HTTPException(status_code=400, detail="该手机号已被绑定")
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
new_user = user.dict()
|
|
|
|
| 25 |
new_user.update({"created_at": int(time.time()), "followers": [], "following": [], "privacy": {"follows": False, "likes": False, "favorites": False, "downloads": False}})
|
| 26 |
users_db[user.account] = new_user
|
| 27 |
db.save_data("users.json", users_db)
|
|
@@ -62,10 +146,27 @@ async def update_user_profile(account: str, update_data: UserUpdate):
|
|
| 62 |
async def reset_password(account: str, pwd_data: PasswordReset):
|
| 63 |
users_db = db.load_data("users.json", default_data={})
|
| 64 |
if account not in users_db: raise HTTPException(status_code=404, detail="用户不存在")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
if len(pwd_data.new_password) < 6: raise HTTPException(status_code=400, detail="新密码必须大于等于6个字符")
|
| 66 |
if not re.match(r'^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]{6,}$', pwd_data.new_password): raise HTTPException(status_code=400, detail="新密码包含不支持的特殊字符")
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
user["password"] = pwd_data.new_password
|
| 70 |
db.save_data("users.json", users_db)
|
| 71 |
return {"status": "success"}
|
|
|
|
| 2 |
from fastapi import APIRouter, HTTPException
|
| 3 |
import time
|
| 4 |
import re
|
| 5 |
+
import random
|
| 6 |
+
import os
|
| 7 |
+
import smtplib
|
| 8 |
+
from email.mime.text import MIMEText
|
| 9 |
import 数据库连接 as db
|
| 10 |
from notifications import add_notification
|
| 11 |
+
from models import UserRegister, UserLogin, UserUpdate, PasswordReset, FollowToggle, PrivacySettings, SendCodeRequest
|
| 12 |
|
| 13 |
router = APIRouter()
|
| 14 |
|
| 15 |
+
# ==========================================
|
| 16 |
+
# 【核心新增】:验证码内存缓存与发送引擎
|
| 17 |
+
# ==========================================
|
| 18 |
+
# 结构: {"contact_action": {"code": "123456", "expires_at": 1690000000}}
|
| 19 |
+
VERIFY_CODES = {}
|
| 20 |
+
|
| 21 |
+
def send_email_code(to_email: str, code: str, action: str):
|
| 22 |
+
"""底层邮件发送引擎 (需在 HF Spaces 设置环境变量)"""
|
| 23 |
+
sender = os.environ.get("SMTP_USER")
|
| 24 |
+
pwd = os.environ.get("SMTP_PASS")
|
| 25 |
+
server = os.environ.get("SMTP_SERVER", "smtp.qq.com") # 默认使用 QQ 邮箱 SMTP
|
| 26 |
+
port = int(os.environ.get("SMTP_PORT", 465))
|
| 27 |
+
|
| 28 |
+
if not sender or not pwd:
|
| 29 |
+
print("警告: 未配置 SMTP_USER 或 SMTP_PASS 环境变量,跳过真实邮件发送")
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
action_str = "注册账号" if action == "register" else "修改/找回密码"
|
| 33 |
+
msg = MIMEText(f"【ComfyUI 社区精选】您的验证码是:{code},用于{action_str}。有效期 10 分钟。如非本人操作,请忽略此邮件。")
|
| 34 |
+
msg['Subject'] = f"ComfyUI 社区 - {action_str}验证码"
|
| 35 |
+
msg['From'] = sender
|
| 36 |
+
msg['To'] = to_email
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
if port == 465:
|
| 40 |
+
with smtplib.SMTP_SSL(server, port) as smtp:
|
| 41 |
+
smtp.login(sender, pwd)
|
| 42 |
+
smtp.send_message(msg)
|
| 43 |
+
else:
|
| 44 |
+
with smtplib.SMTP(server, port) as smtp:
|
| 45 |
+
smtp.starttls()
|
| 46 |
+
smtp.login(sender, pwd)
|
| 47 |
+
smtp.send_message(msg)
|
| 48 |
+
except Exception as e:
|
| 49 |
+
print(f"邮件发送失败: {e}")
|
| 50 |
+
raise HTTPException(status_code=500, detail="邮件下发失败,请检查云端 SMTP 配置")
|
| 51 |
+
|
| 52 |
+
def send_sms_code(phone: str, code: str, action: str):
|
| 53 |
+
"""短信发送接口挂载点"""
|
| 54 |
+
# TODO: 这里接入阿里云/腾讯云短信 SDK
|
| 55 |
+
print(f"[模拟短信发送] 手机号: {phone}, 验证码: {code}, 动作: {action}")
|
| 56 |
+
pass
|
| 57 |
+
|
| 58 |
+
@router.post("/api/users/send-code")
|
| 59 |
+
async def send_verify_code(req: SendCodeRequest):
|
| 60 |
+
# 生成 6 位纯数字验证码
|
| 61 |
+
code = str(random.randint(100000, 999999))
|
| 62 |
+
cache_key = f"{req.contact}_{req.action_type}"
|
| 63 |
+
|
| 64 |
+
# 存入缓存,设置 10 分钟过期
|
| 65 |
+
VERIFY_CODES[cache_key] = {
|
| 66 |
+
"code": code,
|
| 67 |
+
"expires_at": int(time.time()) + 600
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if req.contact_type == "email":
|
| 71 |
+
send_email_code(req.contact, code, req.action_type)
|
| 72 |
+
elif req.contact_type == "phone":
|
| 73 |
+
send_sms_code(req.contact, code, req.action_type)
|
| 74 |
+
else:
|
| 75 |
+
raise HTTPException(status_code=400, detail="不支持的验证方式")
|
| 76 |
+
|
| 77 |
+
return {"status": "success", "message": "验证码已发送"}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ==========================================
|
| 81 |
+
# 原有业务接口修改 (增加验证码鉴权)
|
| 82 |
+
# ==========================================
|
| 83 |
+
|
| 84 |
@router.post("/api/users/register")
|
| 85 |
async def register_user(user: UserRegister):
|
| 86 |
users_db = db.load_data("users.json", default_data={})
|
| 87 |
+
|
| 88 |
+
# 【新增】:校验注册验证码
|
| 89 |
+
cache_key = f"{user.email}_register"
|
| 90 |
+
cached = VERIFY_CODES.get(cache_key)
|
| 91 |
+
if not cached or cached["code"] != user.code or int(time.time()) > cached["expires_at"]:
|
| 92 |
+
raise HTTPException(status_code=400, detail="验证码不正确或已过期")
|
| 93 |
+
|
| 94 |
if len(user.account) <= 5: raise HTTPException(status_code=400, detail="账号必须大于5个字符")
|
| 95 |
if not re.match(r'^[a-zA-Z0-9_]{6,20}$', user.account): raise HTTPException(status_code=400, detail="账号仅支持大小写英文字母、数字及下划线")
|
| 96 |
if len(user.password) < 6: raise HTTPException(status_code=400, detail="密码必须大于等于6个字符")
|
|
|
|
| 101 |
if existing_user.get("email") == user.email: raise HTTPException(status_code=400, detail="该邮箱已被绑定")
|
| 102 |
if existing_user.get("phone") == user.phone: raise HTTPException(status_code=400, detail="该手机号已被绑定")
|
| 103 |
|
| 104 |
+
# 验证通过,销毁验证码
|
| 105 |
+
VERIFY_CODES.pop(cache_key, None)
|
| 106 |
+
|
| 107 |
new_user = user.dict()
|
| 108 |
+
new_user.pop("code", None) # 不将验证码存入数据库
|
| 109 |
new_user.update({"created_at": int(time.time()), "followers": [], "following": [], "privacy": {"follows": False, "likes": False, "favorites": False, "downloads": False}})
|
| 110 |
users_db[user.account] = new_user
|
| 111 |
db.save_data("users.json", users_db)
|
|
|
|
| 146 |
async def reset_password(account: str, pwd_data: PasswordReset):
|
| 147 |
users_db = db.load_data("users.json", default_data={})
|
| 148 |
if account not in users_db: raise HTTPException(status_code=404, detail="用户不存在")
|
| 149 |
+
|
| 150 |
+
user = users_db[account]
|
| 151 |
+
|
| 152 |
+
# 【新增】:校验账号与提供的联系方式是否匹配,防止通过自己的邮箱改别人的密码
|
| 153 |
+
if pwd_data.verify_type == "email" and user.get("email") != pwd_data.verify_contact:
|
| 154 |
+
raise HTTPException(status_code=400, detail="填写的邮箱与该账号绑定的邮箱不匹配")
|
| 155 |
+
if pwd_data.verify_type == "phone" and user.get("phone") != pwd_data.verify_contact:
|
| 156 |
+
raise HTTPException(status_code=400, detail="填写的手机号与该账号绑定的手机号不匹配")
|
| 157 |
+
|
| 158 |
+
# 【新增】:校验重置验证码
|
| 159 |
+
cache_key = f"{pwd_data.verify_contact}_reset"
|
| 160 |
+
cached = VERIFY_CODES.get(cache_key)
|
| 161 |
+
if not cached or cached["code"] != pwd_data.code or int(time.time()) > cached["expires_at"]:
|
| 162 |
+
raise HTTPException(status_code=400, detail="验证码不正确或已过期")
|
| 163 |
+
|
| 164 |
if len(pwd_data.new_password) < 6: raise HTTPException(status_code=400, detail="新密码必须大于等于6个字符")
|
| 165 |
if not re.match(r'^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]{6,}$', pwd_data.new_password): raise HTTPException(status_code=400, detail="新密码包含不支持的特殊字符")
|
| 166 |
+
|
| 167 |
+
# 验证通过,销毁验证码
|
| 168 |
+
VERIFY_CODES.pop(cache_key, None)
|
| 169 |
+
|
| 170 |
user["password"] = pwd_data.new_password
|
| 171 |
db.save_data("users.json", users_db)
|
| 172 |
return {"status": "success"}
|