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