329 lines
15 KiB
Python
329 lines
15 KiB
Python
# sunderer_app/api/custom_user_signup.py
|
|
import frappe
|
|
from frappe import _ # 虽然这里不直接用 _(),但保持导入
|
|
from frappe.utils import validate_email_address, random_string, get_url, generate_hash
|
|
import json # 导入 json 模块
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
def custom_sign_up(email, full_name, password=None, send_email=True, **kwargs):
|
|
"""
|
|
自定义用户注册 API,模仿 Frappe 的 sign_up 但发送自定义邮件链接
|
|
|
|
Args:
|
|
email (str): 用户邮箱
|
|
full_name (str): 用户全名
|
|
password (str, optional): 用户密码 (如果需要立即设置)
|
|
send_email (bool, optional): 是否发送激活邮件. Defaults to True.
|
|
**kwargs: 其他可能传递给 User DocType 的字段
|
|
|
|
Returns:
|
|
dict: 包含操作结果的字典
|
|
"""
|
|
try:
|
|
# --- 1. 验证输入 ---
|
|
email = validate_email_address(email, throw=True) # 验证邮箱格式
|
|
|
|
if frappe.db.exists("User", {"email": email}):
|
|
# 不再使用 frappe.throw,而是直接返回错误信息
|
|
return {"status": "error", "message": _("该邮箱地址已被注册。")}
|
|
|
|
if not full_name:
|
|
return {"status": "error", "message": _("请输入全名。")}
|
|
|
|
# --- 2. 创建 User 文档 ---
|
|
user = frappe.get_doc({
|
|
"doctype": "User",
|
|
"email": email,
|
|
"first_name": full_name.split(" ")[0], # 简单拆分姓
|
|
"last_name": " ".join(full_name.split(" ")[1:]) if len(full_name.split(" ")) > 1 else "", # 其余部分为名
|
|
"enabled": 1 if password else 0, # 如果提供了密码,可以立即启用;否则等待激活
|
|
"new_password": password, # 如果提供了密码,设置它
|
|
"send_welcome_email": 0, # 我们自己发送邮件
|
|
"user_type": "Website User", # 通常注册的是网站用户
|
|
# **kwargs: 添加其他通过 kwargs 传递的字段
|
|
**kwargs
|
|
})
|
|
|
|
# --- 3. 插入用户文档 ---
|
|
user.insert(ignore_permissions=True)
|
|
|
|
# --- 4. 生成激活密钥 ---
|
|
# 如果没有提供密码,或者总是需要激活邮件,则生成密钥
|
|
if not password:
|
|
# Use frappe.generate_hash instead of get_password_reset_key
|
|
reset_key = frappe.generate_hash(length=100) # Generate a secure key
|
|
else:
|
|
reset_key = None # 如果密码已设置,可能不需要密钥,或用于其他目的
|
|
|
|
# --- 5. 保存用户文档 (如果需要更新密钥) ---
|
|
if reset_key:
|
|
user.reload() # 重新加载文档以确保它是最新的
|
|
user.reset_password_key = reset_key
|
|
user.save(ignore_permissions=True)
|
|
|
|
# --- 6. 发送自定义激活邮件 ---
|
|
if send_email and reset_key:
|
|
send_custom_activation_email(user.email, reset_key)
|
|
|
|
# --- 7. 返回结果 ---
|
|
if password:
|
|
return {"status": "success", "message": _("用户注册成功,您现在可以登录。")}
|
|
else:
|
|
return {"status": "success", "message": _("激活邮件已发送,请查收邮箱。")}
|
|
|
|
except frappe.DuplicateEntryError:
|
|
# 这个异常可能在 user.insert() 时由数据库唯一性约束触发
|
|
return {"status": "error", "message": _("该邮箱地址已被注册。")}
|
|
except Exception as e:
|
|
# 捕获所有其他意外错误
|
|
frappe.log_error(f"注册过程中发生未知错误,邮箱: {email}, 错误: {str(e)}")
|
|
return {"status": "error", "message": _("注册过程中发生未知错误,请稍后再试。")}
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
def send_custom_activation_email(user_email, reset_key):
|
|
"""
|
|
发送包含自定义激活链接的邮件
|
|
"""
|
|
# 构造自定义激活 URL
|
|
activation_url = f"{get_url()}/activate-account?key={reset_key}" # Use get_url helper
|
|
|
|
# 使用中文主题和内容
|
|
subject = _("决裂者终端 - 激活您的账户") # 或者 _("决裂者终端 - 设置密码")
|
|
message = f"""
|
|
<p>您好!</p>
|
|
<p>请点击下方链接设置密码并激活您的账户:</p>
|
|
<p><a href="{activation_url}">{activation_url}</a></p>
|
|
<p>如果您未发起此请求,请忽略此邮件。</p>
|
|
"""
|
|
|
|
frappe.sendmail(
|
|
recipients=[user_email],
|
|
subject=subject,
|
|
message=message,
|
|
delayed=False # Send immediately
|
|
)
|
|
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
def trigger_account_setup_email(email):
|
|
|
|
try:
|
|
# --- 1. 验证邮箱格式 (可选,但推荐) ---
|
|
import re
|
|
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
if not re.match(pattern, email):
|
|
return {"exc": "请输入有效的邮箱地址!"}
|
|
|
|
# --- 2. 检查邮箱是否已存在于系统中 ---
|
|
if frappe.db.exists("User", {"email": email}):
|
|
return {"exc": "该邮箱地址已被注册。"}
|
|
|
|
# --- 3. 生成唯一的、有时效性的设置密钥 ---
|
|
setup_key = frappe.generate_hash(length=100) # 生成一个足够长的随机密钥
|
|
|
|
# --- 4. 创建初始的 User 记录 (禁用) ---
|
|
# 由于没有全名,我们暂时使用邮箱前缀作为名字,或者可以要求前端提供
|
|
username_parts = email.split('@')
|
|
first_name = username_parts[0] # 使用邮箱前缀作为名字
|
|
|
|
user_doc = frappe.get_doc({
|
|
"doctype": "User",
|
|
"email": email,
|
|
"first_name": first_name,
|
|
"last_name": "", # 可以留空或根据需要填充
|
|
"enabled": 0, # 初始状态为禁用
|
|
"send_welcome_email": 0, # 我们自己发送邮件
|
|
"user_type": "Website User",
|
|
# 可以根据需要添加其他默认字段
|
|
})
|
|
user_doc.insert(ignore_permissions=True) # 插入用户,此时是禁用的
|
|
print(f"Created initial disabled User: {user_doc.name}") # 调试日志
|
|
|
|
# --- 5. 创建 Signup Key DocType 记录 ---
|
|
signup_key_doc = frappe.new_doc("Signup Key")
|
|
signup_key_doc.key = setup_key # 使用生成的 setup_key 作为 key 字段
|
|
signup_key_doc.email = email # 存储关联的邮箱
|
|
signup_key_doc.used = 0 # 标记为未使用
|
|
# 如果 Signup Key DocType 有 expires_on 字段,可以设置过期时间
|
|
# signup_key_doc.expires_on = add_to_date(now_datetime(), hours=1) # 1小时后过期
|
|
signup_key_doc.insert(ignore_permissions=True)
|
|
print(f"Created Signup Key: {signup_key_doc.name} for email: {email}") # 调试日志
|
|
|
|
# --- 6. 构造 IncreaseAcc 链接 ---
|
|
site_url = get_url()
|
|
setup_url = f"{site_url}/IncreaseAcc?key={setup_key}"
|
|
|
|
# --- 7. 发送包含链接的邮件 ---
|
|
send_setup_link_email(email, setup_url)
|
|
|
|
# --- 8. 返回成功信息 ---
|
|
return {"message": "注册链接已发送至您的邮箱,请查收。"}
|
|
|
|
except frappe.DuplicateEntryError as e:
|
|
# 如果 User 或 Signup Key 插入时违反唯一性约束
|
|
frappe.log_error(f"Trigger account setup failed due to duplicate entry, email: {email}, error: {str(e)}")
|
|
return {"exc": "用户或密钥已存在,请稍后再试。"}
|
|
except Exception as e:
|
|
# 捕获所有其他意外错误
|
|
frappe.log_error(f"触发账户设置邮件失败,邮箱: {email}, 错误: {str(e)}")
|
|
import traceback
|
|
frappe.log_error(f"触发账户设置邮件错误堆栈:\n{traceback.format_exc()}")
|
|
# 如果 User 创建失败,尝试回滚 Signup Key (可选)
|
|
# frappe.delete_doc_if_exists("Signup Key", setup_key_name, ignore_permissions=True)
|
|
return {"exc": f"发送失败: {str(e)}"}
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
def send_setup_link_email(user_email, setup_url):
|
|
"""
|
|
发送包含账户设置链接的邮件
|
|
"""
|
|
# 使用中文主题和内容
|
|
subject = "决裂者终端 - 完成账户注册"
|
|
|
|
message = f"""
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
<h2 style="color: #00AFFF;">决裂者终端</h2>
|
|
<p>您好!</p>
|
|
<p>请点击下方链接完成您的账户注册:</p>
|
|
<p><a href="{setup_url}" style="display: inline-block; padding: 15px 30px; background-color: #00AFFF; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;">完成注册</a></p>
|
|
<p><em>此链接将在 1 小时内有效。</em></p>
|
|
<p>如果您未发起此请求,请忽略此邮件。</p>
|
|
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ccc;">
|
|
<p style="color: #666; font-size: 12px;">© 2026 Sunderer Games. All rights reserved.</p>
|
|
</div>
|
|
"""
|
|
|
|
frappe.sendmail(
|
|
recipients=[user_email],
|
|
subject=subject,
|
|
message=message,
|
|
delayed=False # 立即发送
|
|
)
|
|
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
@frappe.whitelist(allow_guest=True)
|
|
def complete_account(key, password):
|
|
print(f"[DEBUG] 开始执行 complete_account,传入的 key: {key[:10]}...")
|
|
try:
|
|
signup_key_doc_list = frappe.get_all("Signup Key", fields=["name", "email"], filters={"key": key, "used": 0})
|
|
print(f"[DEBUG] 查询到的 signup_key_doc_list: {signup_key_doc_list}")
|
|
if not signup_key_doc_list:
|
|
print("[DEBUG] 未找到有效的未使用密钥,抛出错误。")
|
|
frappe.throw("无效或已过期的密钥。", frappe.ValidationError)
|
|
return
|
|
|
|
signup_key_name = signup_key_doc_list[0].name
|
|
email = signup_key_doc_list[0].email
|
|
print(f"[DEBUG] 获取到的邮箱: {email}")
|
|
|
|
# --- 获取用户文档 ---
|
|
try:
|
|
user_doc = frappe.get_doc("User", email)
|
|
print(f"[DEBUG] 找到用户文档: {user_doc.name}, 启用状态: {user_doc.enabled}")
|
|
except frappe.DoesNotExistError:
|
|
print(f"[ERROR] 用户 {email} 不存在。")
|
|
frappe.throw(f"用户 {email} 不存在。")
|
|
return
|
|
|
|
# --- 新增:手动进行密码强度校验 ---
|
|
# 为密码强度检查准备用户信息
|
|
user_info = {
|
|
"email": user_doc.email or "",
|
|
"first_name": user_doc.first_name or "",
|
|
"last_name": user_doc.last_name or "",
|
|
"username": user_doc.name or "" # 如果 User 文档有 username 字段,也可以包含
|
|
}
|
|
|
|
# 调用 Frappe 的密码强度检查函数
|
|
# 注意:这个函数名可能因版本而异,但通常在 frappe.utils.password_strength 中
|
|
# 我们会尝试不同的函数名
|
|
try:
|
|
from frappe.utils.password_strength import get_password_strength
|
|
strength_result = get_password_strength(password, user_info)
|
|
except ImportError:
|
|
# 如果无法导入 get_password_strength,尝试其他方式
|
|
# 这里我们假设它在 frappe.utils 中,或者使用一个简单的自定义检查
|
|
strength_result = {"score": 0, "feedback": {"warning": "", "suggestions": ["密码至少需要8位字符,包含大小写字母和数字"]}}
|
|
# 或者,您可以在这里实现更复杂的自定义强度检查逻辑
|
|
|
|
score = strength_result.get('score', 0)
|
|
feedback = strength_result.get('feedback', {})
|
|
|
|
# 定义最低分数要求(Frappe 默认是2)
|
|
min_score = 2
|
|
|
|
if score < min_score:
|
|
suggestions = feedback.get('suggestions', [])
|
|
warning = feedback.get('warning', "")
|
|
error_message_parts = ["密码强度不足,请设置一个更强的密码。"]
|
|
if warning:
|
|
error_message_parts.append(f"警告: {warning}")
|
|
if suggestions:
|
|
error_message_parts.append(f"建议: {', '.join(suggestions)}")
|
|
# 使用 frappe.throw 返回结构化的错误信息,便于前端解析
|
|
frappe.throw({
|
|
"message": " ".join(error_message_parts),
|
|
"type": "password_strength",
|
|
"details": {
|
|
"score": score,
|
|
"min_score": min_score,
|
|
"suggestions": suggestions,
|
|
"warning": warning
|
|
}
|
|
})
|
|
# --- 新增结束:手动进行密码强度校验 ---
|
|
|
|
# --- 关键部分:更新密码和角色 ---
|
|
try:
|
|
user_doc.new_password = password
|
|
user_doc.enabled = 1 # 确保用户处于启用状态
|
|
|
|
# --- 添加或修改角色 ---
|
|
default_roles = ["Sunderer User"] # 根据你的需求调整
|
|
|
|
for role_name in default_roles:
|
|
if frappe.db.exists("Role", role_name):
|
|
if not any(d.role == role_name for d in user_doc.roles):
|
|
user_doc.append("roles", {"role": role_name})
|
|
print(f"[DEBUG] 已将角色 '{role_name}' 添加到用户 {email}。")
|
|
else:
|
|
print(f"[WARNING] 角色 '{role_name}' 不存在。跳过。")
|
|
|
|
# --- 角色添加结束 ---
|
|
|
|
# 在手动检查通过后,再执行 save(),此时应该不会再因为密码强度问题失败
|
|
user_doc.save(ignore_permissions=True)
|
|
print(f"[DEBUG] 成功更新用户 {email} 的密码、角色和启用状态。")
|
|
except Exception as pwd_error:
|
|
frappe.log_error(f"在完成账户 {email} 时更新用户详细信息(密码/角色)失败: {str(pwd_error)}")
|
|
print(f"[ERROR] 更新用户 {email} 详细信息失败: {str(pwd_error)}")
|
|
frappe.throw(f"设置用户账户失败: {str(pwd_error)}")
|
|
return
|
|
|
|
# --- 仅当用户信息更新成功后,才将密钥标记为已使用 ---
|
|
try:
|
|
frappe.db.set_value("Signup Key", signup_key_name, "used", 1)
|
|
print(f"[DEBUG] 已将 Signup Key {signup_key_name} 标记为已使用。")
|
|
except Exception as db_error:
|
|
frappe.log_error(f"用户更新后,将 Signup Key {signup_key_name} 标记为已使用失败: {str(db_error)}")
|
|
print(f"[ERROR] 将 Signup Key 标记为已使用失败: {str(db_error)}")
|
|
frappe.msgprint("密码已设置,但请就确认链接状态联系技术支持。")
|
|
|
|
frappe.db.commit()
|
|
print(f"[DEBUG] 已提交 key: {key[:10]}... 的事务。")
|
|
|
|
return {"message": "账户已激活,密码设置成功!"}
|
|
|
|
except frappe.AuthenticationError as e:
|
|
frappe.log_error(f"complete_account 中发生身份验证错误: {str(e)}")
|
|
print(f"[ERROR] 身份验证错误: {str(e)}")
|
|
frappe.throw(f"身份验证错误: {str(e)}")
|
|
except Exception as e:
|
|
frappe.log_error(f"激活账户时发生错误,key {key[:10]}...: {str(e)}")
|
|
print(f"[ERROR] 激活账户时发生错误,key {key[:10]}...: {str(e)}")
|
|
frappe.throw("激活您的账户时发生错误。请稍后重试或联系技术支持。") |