feat: P0 commercial readiness — password security, legal, account management
Backend changes for Phase A of v1.0 commercial launch: P0-3: Password Security - Remove hardcoded admin/123456 defaults from 34+ bootstrap scripts - Add PLATFORM_USERNAME/PLATFORM_PASSWORD env vars to config - Add PUT /api/v1/auth/change-password endpoint (requires old password) P0-1: User Agreement & Privacy Policy - Add agreed_terms/agreed_terms_version/agreed_terms_at to User model - New GET /api/v1/legal/privacy and GET /api/v1/legal/terms endpoints - New POST /api/v1/legal/agree endpoint to record consent - Register endpoint now validates agreed_terms must be True P0-6: Account Management - Add phone/status/is_email_verified to User model - Add DELETE /api/v1/auth/account for account deletion (with password confirmation) - Add PUT /api/v1/auth/phone for phone binding - Reject authentication for deleted accounts (status=deleted) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@ class UserCreate(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
password: str
|
||||
agreed_terms: bool = False
|
||||
agreed_terms_version: str | None = None
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
@@ -47,6 +49,13 @@ class UserCreate(BaseModel):
|
||||
raise ValueError("邮箱格式无效")
|
||||
return v.lower()
|
||||
|
||||
@field_validator("agreed_terms")
|
||||
@classmethod
|
||||
def must_agree_terms(cls, v: bool) -> bool:
|
||||
if not v:
|
||||
raise ValueError("必须同意用户协议和隐私政策")
|
||||
return v
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户响应模型"""
|
||||
@@ -54,6 +63,7 @@ class UserResponse(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
role: str
|
||||
phone: str | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -65,6 +75,9 @@ class MeResponse(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
role: str
|
||||
phone: str | None = None
|
||||
status: str = "active"
|
||||
is_email_verified: bool = False
|
||||
workspaces: list = []
|
||||
current_workspace_id: str | None = None
|
||||
|
||||
@@ -91,7 +104,10 @@ async def register(user_data: UserCreate, db: Session = Depends(get_db)):
|
||||
user = User(
|
||||
username=user_data.username,
|
||||
email=user_data.email,
|
||||
password_hash=hashed_password
|
||||
password_hash=hashed_password,
|
||||
agreed_terms=user_data.agreed_terms,
|
||||
agreed_terms_version=user_data.agreed_terms_version,
|
||||
agreed_terms_at=datetime.utcnow() if user_data.agreed_terms else None,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
@@ -180,6 +196,8 @@ async def get_current_user(
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user is None:
|
||||
raise NotFoundError("用户", user_id)
|
||||
if user.status == "deleted":
|
||||
raise UnauthorizedError("账号已注销")
|
||||
|
||||
return user
|
||||
|
||||
@@ -213,6 +231,9 @@ async def get_me(
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
"phone": user.phone,
|
||||
"status": user.status or "active",
|
||||
"is_email_verified": user.is_email_verified or False,
|
||||
"workspaces": workspaces,
|
||||
"current_workspace_id": current_ws_id if current_ws_id else None,
|
||||
}
|
||||
@@ -423,6 +444,99 @@ async def reset_password(body: ResetPasswordRequest, db: Session = Depends(get_d
|
||||
return {"message": "密码重置成功,请使用新密码登录"}
|
||||
|
||||
|
||||
# ─── 修改密码 ───────────────────────────────────────────────
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
@field_validator("new_password")
|
||||
@classmethod
|
||||
def password_length(cls, v: str) -> str:
|
||||
if len(v) < 6:
|
||||
raise ValueError("密码不少于6个字符")
|
||||
if len(v) > 32:
|
||||
raise ValueError("密码不超过32个字符")
|
||||
return v
|
||||
|
||||
|
||||
@router.put("/change-password")
|
||||
async def change_password(
|
||||
body: ChangePasswordRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""修改密码(需提供旧密码验证)。"""
|
||||
if not verify_password(body.old_password, current_user.password_hash):
|
||||
raise HTTPException(status_code=400, detail="旧密码错误")
|
||||
|
||||
current_user.password_hash = get_password_hash(body.new_password)
|
||||
db.commit()
|
||||
|
||||
logger.info("用户 %s 修改密码成功", current_user.username)
|
||||
return {"message": "密码修改成功"}
|
||||
|
||||
|
||||
# ─── 账号注销 ───────────────────────────────────────────────
|
||||
|
||||
class DeleteAccountRequest(BaseModel):
|
||||
password: str
|
||||
|
||||
|
||||
@router.delete("/account")
|
||||
async def delete_account(
|
||||
body: DeleteAccountRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""注销账号(需提供密码确认)。软删除:设置 status='deleted'。"""
|
||||
if not verify_password(body.password, current_user.password_hash):
|
||||
raise HTTPException(status_code=400, detail="密码错误")
|
||||
|
||||
# 软删除用户
|
||||
current_user.status = "deleted"
|
||||
current_user.email = f"deleted_{current_user.id}@deleted.local"
|
||||
current_user.phone = None
|
||||
current_user.feishu_open_id = None
|
||||
db.commit()
|
||||
|
||||
logger.info("用户 %s 账号已注销", current_user.username)
|
||||
return {"message": "账号已注销"}
|
||||
|
||||
|
||||
# ─── 绑定手机号 ──────────────────────────────────────────────
|
||||
|
||||
class BindPhoneRequest(BaseModel):
|
||||
phone: str
|
||||
code: str | None = None # 验证码(对接短信服务后可启用)
|
||||
|
||||
@field_validator("phone")
|
||||
@classmethod
|
||||
def phone_format(cls, v: str) -> str:
|
||||
if not re.match(r"^1[3-9]\d{9}$", v):
|
||||
raise ValueError("手机号格式无效")
|
||||
return v
|
||||
|
||||
|
||||
@router.put("/phone")
|
||||
async def bind_phone(
|
||||
body: BindPhoneRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""绑定或修改手机号。"""
|
||||
# 检查手机号是否已被其他用户绑定
|
||||
existing = db.query(User).filter(User.phone == body.phone, User.id != current_user.id).first()
|
||||
if existing:
|
||||
raise ConflictError("该手机号已被其他用户绑定")
|
||||
|
||||
current_user.phone = body.phone
|
||||
db.commit()
|
||||
|
||||
logger.info("用户 %s 绑定手机号成功", current_user.username)
|
||||
return {"message": "手机号绑定成功", "phone": body.phone}
|
||||
|
||||
|
||||
async def get_optional_user(
|
||||
token: str | None = Depends(oauth2_scheme_optional),
|
||||
db: Session = Depends(get_db)
|
||||
|
||||
161
backend/app/api/legal.py
Normal file
161
backend/app/api/legal.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
用户协议 & 隐私政策 API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.exceptions import ConflictError
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/legal",
|
||||
tags=["legal"],
|
||||
)
|
||||
|
||||
# ─── 协议版本 ───────────────────────────────────────────────
|
||||
|
||||
CURRENT_TERMS_VERSION = "1.0"
|
||||
CURRENT_PRIVACY_VERSION = "1.0"
|
||||
|
||||
# ─── 内容 ───────────────────────────────────────────────────
|
||||
|
||||
PRIVACY_POLICY = """# 天工智能体平台 隐私政策
|
||||
|
||||
**更新日期:2026年7月1日**
|
||||
**生效日期:2026年7月1日**
|
||||
|
||||
## 一、信息收集
|
||||
|
||||
1. 您在注册账户时提供的个人信息,包括用户名、邮箱地址和手机号码。
|
||||
2. 您在使用智能体对话功能时产生的对话内容和交互记录。
|
||||
3. 设备信息:我们可能收集您的设备型号、操作系统版本、设备标识符等信息用于优化服务。
|
||||
|
||||
## 二、信息使用
|
||||
|
||||
1. 为您提供智能体对话、语音识别、推送通知等核心服务。
|
||||
2. 改善产品质量和用户体验,包括对话模型的训练和优化。
|
||||
3. 提供客户支持和故障排除。
|
||||
|
||||
## 三、信息存储和保护
|
||||
|
||||
1. 您的数据存储在中国境内的服务器上。
|
||||
2. 我们采用加密传输、访问控制、数据备份等安全措施保护您的信息。
|
||||
3. 对话数据在您主动删除或账号注销后将在30天内清除。
|
||||
|
||||
## 四、信息共享
|
||||
|
||||
1. 未经您的明确同意,我们不会将您的个人信息共享给第三方。
|
||||
2. 法律法规要求披露的除外。
|
||||
|
||||
## 五、您的权利
|
||||
|
||||
1. 您可以随时查看、修改您的个人信息。
|
||||
2. 您可以导出您的对话数据。
|
||||
3. 您可以注销账号并要求删除数据。
|
||||
|
||||
## 六、联系我们
|
||||
|
||||
如有隐私相关问题,请联系:support@tiangong.ai
|
||||
"""
|
||||
|
||||
USER_AGREEMENT = """# 天工智能体平台 用户协议
|
||||
|
||||
**更新日期:2026年7月1日**
|
||||
**生效日期:2026年7月1日**
|
||||
|
||||
## 一、总则
|
||||
|
||||
1. 天工智能体平台(以下简称"本平台")是一个提供AI智能体服务的在线平台。
|
||||
2. 您注册和使用本平台即表示您已阅读、理解并同意本协议的全部条款。
|
||||
|
||||
## 二、账号管理
|
||||
|
||||
1. 您应当提供真实、准确的注册信息。
|
||||
2. 您对账号下的所有活动负责,请妥善保管账号和密码。
|
||||
3. 您不得将账号转让、出借或出售给他人。
|
||||
|
||||
## 三、使用规范
|
||||
|
||||
1. 您不得利用本平台从事违法活动,包括但不限于:
|
||||
- 生成、传播违法或有害内容
|
||||
- 侵犯他人知识产权或隐私权
|
||||
- 干扰或破坏平台正常运行
|
||||
2. 您不得对智能体进行恶意引导、注入攻击或其他危害系统安全的行为。
|
||||
|
||||
## 四、知识产权
|
||||
|
||||
1. 平台本身的软件、界面设计、商标等知识产权归平台所有。
|
||||
2. 您创建的智能体配置归您所有。
|
||||
3. 您与智能体对话产生的内容归您所有。
|
||||
|
||||
## 五、免责声明
|
||||
|
||||
1. AI生成内容仅供参考,不构成专业建议。
|
||||
2. 平台不对AI生成内容的准确性、完整性负责。
|
||||
3. 因不可抗力或第三方原因导致的服务中断,平台不承担责任。
|
||||
|
||||
## 六、协议变更
|
||||
|
||||
1. 我们可能会不时更新本协议,更新后会通过应用内通知告知。
|
||||
2. 如果您不同意更新后的条款,您可以停止使用本平台并注销账号。
|
||||
|
||||
## 七、法律适用与争议解决
|
||||
|
||||
1. 本协议适用中华人民共和国法律。
|
||||
2. 因本协议产生的争议,双方应协商解决;协商不成的,提交平台所在地法院管辖。
|
||||
"""
|
||||
|
||||
|
||||
# ─── Response/Request Models ──────────────────────────────────
|
||||
|
||||
class LegalContentResponse(BaseModel):
|
||||
content: str
|
||||
version: str
|
||||
|
||||
|
||||
class AgreeRequest(BaseModel):
|
||||
terms_version: str | None = None
|
||||
privacy_version: str | None = None
|
||||
|
||||
|
||||
class AgreeResponse(BaseModel):
|
||||
message: str
|
||||
agreed_terms: bool
|
||||
agreed_terms_version: str
|
||||
|
||||
|
||||
# ─── Endpoints ────────────────────────────────────────────────
|
||||
|
||||
@router.get("/privacy", response_model=LegalContentResponse)
|
||||
async def get_privacy_policy():
|
||||
"""获取隐私政策内容。"""
|
||||
return LegalContentResponse(content=PRIVACY_POLICY, version=CURRENT_PRIVACY_VERSION)
|
||||
|
||||
|
||||
@router.get("/terms", response_model=LegalContentResponse)
|
||||
async def get_user_agreement():
|
||||
"""获取用户协议内容。"""
|
||||
return LegalContentResponse(content=USER_AGREEMENT, version=CURRENT_TERMS_VERSION)
|
||||
|
||||
|
||||
@router.post("/agree", response_model=AgreeResponse)
|
||||
async def agree_terms(
|
||||
body: AgreeRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""记录用户同意协议。"""
|
||||
current_user.agreed_terms = True
|
||||
current_user.agreed_terms_version = body.terms_version or CURRENT_TERMS_VERSION
|
||||
current_user.agreed_terms_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return AgreeResponse(
|
||||
message="协议同意已记录",
|
||||
agreed_terms=True,
|
||||
agreed_terms_version=current_user.agreed_terms_version,
|
||||
)
|
||||
Reference in New Issue
Block a user