feat: 用户认证路由
This commit is contained in:
parent
945373612a
commit
9a60abcb8f
|
|
@ -0,0 +1,286 @@
|
||||||
|
"""
|
||||||
|
NAC 一键上链系统 - 用户注册/登录路由 v3.1
|
||||||
|
更新:注册时自动调用NAC钱包微服务创建原生钱包
|
||||||
|
支持本地账号注册、登录,JWT token认证
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
from pydantic import BaseModel, EmailStr, validator
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from jose import jwt, JWTError
|
||||||
|
from database import users_col, now_utc
|
||||||
|
from nac_wallet_client import create_wallet, get_wallet
|
||||||
|
|
||||||
|
logger = logging.getLogger("nac-onboarding.users")
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
# 密码哈希
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
# JWT配置
|
||||||
|
JWT_SECRET = os.getenv("JWT_SECRET", "nac-rwa-secret-key-2026-xtzh-cbpp")
|
||||||
|
JWT_ALGORITHM = "HS256"
|
||||||
|
JWT_EXPIRE_HOURS = 24 * 7 # 7天有效期
|
||||||
|
|
||||||
|
# KYC等级权限
|
||||||
|
KYC_PERMISSIONS = {
|
||||||
|
0: {"can_view": True, "can_onboard": False, "can_rwa": False},
|
||||||
|
1: {"can_view": True, "can_onboard": False, "can_rwa": False},
|
||||||
|
2: {"can_view": True, "can_onboard": True, "can_rwa": False},
|
||||||
|
3: {"can_view": True, "can_onboard": True, "can_rwa": True},
|
||||||
|
4: {"can_view": True, "can_onboard": True, "can_rwa": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Pydantic 模型 ----
|
||||||
|
|
||||||
|
class RegisterRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
full_name: str = ""
|
||||||
|
|
||||||
|
@validator("username")
|
||||||
|
def username_valid(cls, v):
|
||||||
|
v = v.strip()
|
||||||
|
if len(v) < 3:
|
||||||
|
raise ValueError("用户名至少3个字符")
|
||||||
|
if len(v) > 32:
|
||||||
|
raise ValueError("用户名最多32个字符")
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator("password")
|
||||||
|
def password_valid(cls, v):
|
||||||
|
if len(v) < 6:
|
||||||
|
raise ValueError("密码至少6个字符")
|
||||||
|
return v
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
username: str # 支持用户名或邮箱
|
||||||
|
password: str
|
||||||
|
|
||||||
|
class TokenResponse(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
user: dict
|
||||||
|
|
||||||
|
# ---- 工具函数 ----
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
def verify_password(plain: str, hashed: str) -> bool:
|
||||||
|
return pwd_context.verify(plain, hashed)
|
||||||
|
|
||||||
|
def create_token(did: str, username: str) -> str:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(hours=JWT_EXPIRE_HOURS)
|
||||||
|
payload = {
|
||||||
|
"sub": did,
|
||||||
|
"username": username,
|
||||||
|
"exp": expire,
|
||||||
|
"iat": datetime.now(timezone.utc),
|
||||||
|
}
|
||||||
|
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||||
|
|
||||||
|
def decode_token(token: str) -> dict:
|
||||||
|
try:
|
||||||
|
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||||
|
except JWTError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def make_did(username: str) -> str:
|
||||||
|
"""生成NAC格式的DID"""
|
||||||
|
safe = username.lower().replace(" ", "_")
|
||||||
|
rand = secrets.token_hex(4)
|
||||||
|
return f"did:nac:user:{safe}_{rand}"
|
||||||
|
|
||||||
|
def user_to_public(user: dict) -> dict:
|
||||||
|
"""返回用户公开信息(不含密码哈希)"""
|
||||||
|
return {
|
||||||
|
"did": user.get("did"),
|
||||||
|
"username": user.get("username"),
|
||||||
|
"email": user.get("email"),
|
||||||
|
"full_name": user.get("full_name", ""),
|
||||||
|
"display_name": user.get("full_name") or user.get("username"),
|
||||||
|
"kyc_level": user.get("kyc_level", 2),
|
||||||
|
"kyc_status": user.get("kyc_status", "approved"),
|
||||||
|
"role": user.get("role", "user"),
|
||||||
|
"nac_address": user.get("nac_address", ""),
|
||||||
|
"created_at": str(user.get("created_at", "")),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- API端点 ----
|
||||||
|
|
||||||
|
@router.post("/register", summary="用户注册")
|
||||||
|
async def register(req: RegisterRequest):
|
||||||
|
"""注册新用户账号,并自动创建NAC原生钱包"""
|
||||||
|
username = req.username.strip()
|
||||||
|
email = req.email.strip().lower()
|
||||||
|
|
||||||
|
# 检查用户名是否已存在
|
||||||
|
existing = await users_col.find_one({"username": {"$regex": f"^{username}$", "$options": "i"}})
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=400, detail="用户名已被使用")
|
||||||
|
|
||||||
|
# 检查邮箱是否已注册
|
||||||
|
existing_email = await users_col.find_one({"email": email})
|
||||||
|
if existing_email:
|
||||||
|
raise HTTPException(status_code=400, detail="该邮箱已注册")
|
||||||
|
|
||||||
|
# 创建用户
|
||||||
|
did = make_did(username)
|
||||||
|
hashed_pw = hash_password(req.password)
|
||||||
|
now = now_utc()
|
||||||
|
|
||||||
|
user_doc = {
|
||||||
|
"did": did,
|
||||||
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"full_name": req.full_name.strip() or username,
|
||||||
|
"display_name": req.full_name.strip() or username,
|
||||||
|
"password_hash": hashed_pw,
|
||||||
|
"kyc_level": 2, # 新注册用户默认KYC-2(可上链)
|
||||||
|
"kyc_status": "approved",
|
||||||
|
"role": "user",
|
||||||
|
"is_active": True,
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
"last_login": None,
|
||||||
|
"permissions": KYC_PERMISSIONS[2],
|
||||||
|
"nac_address": "", # 钱包地址(创建后填入)
|
||||||
|
"wallet_created": False, # 钱包创建状态
|
||||||
|
}
|
||||||
|
|
||||||
|
await users_col.insert_one(user_doc)
|
||||||
|
logger.info(f"新用户注册: {username} ({did})")
|
||||||
|
|
||||||
|
# ===== 自动创建NAC原生钱包 =====
|
||||||
|
wallet_info = None
|
||||||
|
mnemonic_data = None
|
||||||
|
try:
|
||||||
|
wallet_result = await create_wallet(user_id=did, user_name=username)
|
||||||
|
if wallet_result:
|
||||||
|
nac_address = wallet_result.get("address", "")
|
||||||
|
# 将钱包地址写回用户文档
|
||||||
|
await users_col.update_one(
|
||||||
|
{"did": did},
|
||||||
|
{"$set": {
|
||||||
|
"nac_address": nac_address,
|
||||||
|
"wallet_created": True,
|
||||||
|
"wallet_id": wallet_result.get("wallet_id"),
|
||||||
|
"updated_at": now_utc(),
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
wallet_info = {
|
||||||
|
"address": nac_address,
|
||||||
|
"wallet_id": wallet_result.get("wallet_id"),
|
||||||
|
"assets": [
|
||||||
|
{"symbol": "XIC", "balance": "0.00", "type": "治理币"},
|
||||||
|
{"symbol": "XTZH", "balance": "0.00", "type": "稳定币"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
# 助记词仅此一次返回,不存储
|
||||||
|
if wallet_result.get("mnemonic"):
|
||||||
|
mnemonic_data = {
|
||||||
|
"mnemonic": wallet_result["mnemonic"],
|
||||||
|
"notice": wallet_result.get("notice", "请立即备份助记词,系统不会再次显示!"),
|
||||||
|
}
|
||||||
|
logger.info(f"NAC钱包创建成功: {username} -> {nac_address}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"NAC钱包创建失败(服务不可达),用户 {username} 已注册,钱包待补创建")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"钱包创建异常: {e},用户 {username} 已注册,钱包待补创建")
|
||||||
|
|
||||||
|
# 生成token
|
||||||
|
token = create_token(did, username)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"success": True,
|
||||||
|
"message": "注册成功",
|
||||||
|
"access_token": token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": user_to_public({**user_doc, "nac_address": wallet_info["address"] if wallet_info else ""}),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 附加钱包信息(仅注册时返回一次助记词)
|
||||||
|
if wallet_info:
|
||||||
|
response["wallet"] = wallet_info
|
||||||
|
if mnemonic_data:
|
||||||
|
response["mnemonic"] = mnemonic_data
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", summary="用户登录")
|
||||||
|
async def login(req: LoginRequest):
|
||||||
|
"""用户名/邮箱 + 密码登录"""
|
||||||
|
identifier = req.username.strip()
|
||||||
|
|
||||||
|
# 查找用户(支持用户名或邮箱)
|
||||||
|
user = await users_col.find_one({
|
||||||
|
"$or": [
|
||||||
|
{"username": {"$regex": f"^{identifier}$", "$options": "i"}},
|
||||||
|
{"email": identifier.lower()},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||||||
|
|
||||||
|
# 验证密码
|
||||||
|
if not verify_password(req.password, user.get("password_hash", "")):
|
||||||
|
raise HTTPException(status_code=401, detail="用户名或密码错误")
|
||||||
|
|
||||||
|
# 检查账号状态
|
||||||
|
if not user.get("is_active", True):
|
||||||
|
raise HTTPException(status_code=403, detail="账号已被禁用")
|
||||||
|
|
||||||
|
# 如果钱包尚未创建,尝试补创建
|
||||||
|
if not user.get("wallet_created", False):
|
||||||
|
try:
|
||||||
|
wallet_result = await create_wallet(user_id=user["did"], user_name=user["username"])
|
||||||
|
if wallet_result:
|
||||||
|
await users_col.update_one(
|
||||||
|
{"did": user["did"]},
|
||||||
|
{"$set": {
|
||||||
|
"nac_address": wallet_result.get("address", ""),
|
||||||
|
"wallet_created": True,
|
||||||
|
"wallet_id": wallet_result.get("wallet_id"),
|
||||||
|
"updated_at": now_utc(),
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
user["nac_address"] = wallet_result.get("address", "")
|
||||||
|
logger.info(f"登录时补创建钱包成功: {user['username']}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"登录时补创建钱包失败: {e}")
|
||||||
|
|
||||||
|
# 更新最后登录时间
|
||||||
|
await users_col.update_one(
|
||||||
|
{"did": user["did"]},
|
||||||
|
{"$set": {"last_login": now_utc()}}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"用户登录: {user['username']} ({user['did']})")
|
||||||
|
|
||||||
|
# 生成token
|
||||||
|
token = create_token(user["did"], user["username"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "登录成功",
|
||||||
|
"access_token": token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": user_to_public(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/wallet", summary="获取当前用户的NAC钱包信息")
|
||||||
|
async def get_my_wallet(current_user: dict = Depends(lambda: None)):
|
||||||
|
"""
|
||||||
|
获取当前用户的NAC钱包信息(余额等)
|
||||||
|
注意:此端点需要Bearer token,由get_current_user依赖注入处理
|
||||||
|
"""
|
||||||
|
# 此端点由onboarding.py中的get_current_user处理
|
||||||
|
pass
|
||||||
Loading…
Reference in New Issue