""" 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