登录注册体系的源码级硬切换
This commit is contained in:
@@ -324,14 +324,6 @@ class ImportExportController extends ControllerBase {
|
||||
'status' => empty($missing_tables) ? 'ok' : 'error',
|
||||
];
|
||||
|
||||
// Email verification backfill
|
||||
$backfill_done = get_option('aksrt_email_verification_backfill_done', false);
|
||||
$checks['email_backfill'] = [
|
||||
'label' => '邮箱验证迁移',
|
||||
'value' => $backfill_done ? '已完成' : '未完成',
|
||||
'status' => $backfill_done ? 'ok' : 'info',
|
||||
];
|
||||
|
||||
// Option count
|
||||
$option_count = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'aksrt_%'"
|
||||
|
||||
@@ -32,7 +32,7 @@ class SchemaPermissions {
|
||||
['key' => 'mode', 'label' => '触发模式', 'type' => 'radio', 'default' => 'always', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['label' => '始终显示', 'value' => 'always'],
|
||||
]],
|
||||
['key' => 'enabled_scenarios', 'label' => '启用场景', 'type' => 'checkbox', 'default' => ['comment', 'login', 'register', 'bind', 'bind_phone', 'lostpassword', 'change_password', 'change_email', 'verify_email', 'download', 'signin'], 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['key' => 'enabled_scenarios', 'label' => '启用场景', 'type' => 'checkbox', 'default' => ['comment', 'login', 'register', 'bind', 'bind_phone', 'lostpassword', 'change_password', 'change_email', 'download', 'signin'], 'dependsOn' => ['key' => 'enabled', 'value' => true], 'options' => [
|
||||
['label' => '发表评论', 'value' => 'comment'],
|
||||
['label' => '用户登录', 'value' => 'login'],
|
||||
['label' => '用户注册', 'value' => 'register'],
|
||||
@@ -41,7 +41,6 @@ class SchemaPermissions {
|
||||
['label' => '找回密码', 'value' => 'lostpassword'],
|
||||
['label' => '修改密码', 'value' => 'change_password'],
|
||||
['label' => '更改邮箱', 'value' => 'change_email'],
|
||||
['label' => '邮箱验证', 'value' => 'verify_email'],
|
||||
['label' => '资源下载', 'value' => 'download'],
|
||||
['label' => '每日签到', 'value' => 'signin'],
|
||||
['label' => '前台投稿', 'value' => 'front_submit'],
|
||||
|
||||
@@ -115,30 +115,29 @@ class SchemaUsers {
|
||||
'groups' => [
|
||||
['key' => 'register', 'title' => '注册设置', 'fields' => [
|
||||
['key' => 'close_sign', 'label' => '一键禁用登录注册', 'type' => 'switch', 'default' => false, 'desc' => '开启后前台禁用全部注册/登录入口(含社交登录),不影响后台登录'],
|
||||
['key' => 'user_close_signup', 'label' => '关闭注册', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'close_signin', 'label' => '禁用账密登录', 'type' => 'switch', 'default' => false, 'desc' => '仅允许社交账号登录'],
|
||||
['key' => 'user_agreement_s', 'label' => '用户协议', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'close_signup', 'label' => '关闭注册', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'close_signin', 'label' => '禁用账号登录/注册', 'type' => 'switch', 'default' => false, 'desc' => '关闭账号密码和验证码登录注册,仅允许社交账号登录'],
|
||||
['key' => 'user_agreement_s', 'label' => '用户协议', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_agreement_page', 'label' => '协议页面', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_privacy_s', 'label' => '隐私政策', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_privacy_s', 'label' => '隐私政策', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_privacy_page', 'label' => '隐私页面', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'user_agreement_check', 'label' => '强制勾选协议', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'no_repetition_name', 'label' => '禁止重复昵称', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_nickname_out', 'label' => '昵称黑名单关键词', 'type' => 'textarea', 'default' => "赌博,博彩,代发,代理,刷单,博彩,六合彩,admin,test,管理员,客服", 'desc' => '一行一个关键词,注册昵称包含关键词将被拒绝'],
|
||||
['key' => 'user_email_limit_type', 'label' => '邮箱限制类型', 'type' => 'radio', 'default' => 'black', 'options' => [
|
||||
['label' => '黑名单(拒绝以下后缀)', 'value' => 'black'],
|
||||
['label' => '白名单(仅允许以下后缀)', 'value' => 'white'],
|
||||
['key' => 'user_agreement_check', 'label' => '强制勾选协议', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'no_repetition_name', 'label' => '禁止重复昵称', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_nickname_out', 'label' => '昵称黑名单关键词', 'type' => 'textarea', 'default' => '赌博,博彩,彩票,性爱,色情,做爱,爱爱,淫秽,傻b,妈的,妈b,admin,test'],
|
||||
['key' => 'user_email_limit_type', 'label' => '邮箱限制类型', 'type' => 'radio', 'default' => 'not', 'options' => [
|
||||
['label' => '黑名单', 'value' => 'not'],
|
||||
['label' => '白名单', 'value' => 'allow'],
|
||||
]],
|
||||
['key' => 'user_email_out', 'label' => '邮箱限制后缀', 'type' => 'textarea', 'default' => "@tempmail.com\n@throwaway.email\n@guerrillamail.com", 'desc' => '一行一个邮箱后缀,如 @tempmail.com'],
|
||||
['key' => 'user_email_out', 'label' => '邮箱限制关键词', 'type' => 'textarea', 'default' => ''],
|
||||
]],
|
||||
['key' => 'login', 'title' => '登录设置', 'fields' => [
|
||||
['key' => 'user_close_login', 'label' => '关闭登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_sign_type', 'label' => '登录注册样式', 'type' => 'radio', 'default' => 'page', 'options' => [
|
||||
['key' => 'user_sign_type', 'label' => '登录注册样式', 'type' => 'radio', 'default' => 'modal', 'options' => [
|
||||
['label' => '弹窗', 'value' => 'modal'],
|
||||
['label' => '跳转页面', 'value' => 'page'],
|
||||
]],
|
||||
['key' => 'sign_modal_auto_s', 'label' => '自动弹出登录窗', 'type' => 'switch', 'default' => false, 'desc' => '仅在 user_sign_type=modal 时生效'],
|
||||
['key' => 'sign_modal_auto_waiting_time', 'label' => '弹窗等待秒数', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 60, 'desc' => '页面加载多少秒后弹出登录窗'],
|
||||
['key' => 'sign_modal_auto_expires', 'label' => '弹窗周期(小时)', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 720, 'desc' => '关闭弹窗后多少小时内不再自动弹出,0=每次刷新都弹'],
|
||||
['key' => 'sign_modal_auto_waiting_time', 'label' => '弹窗等待秒数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 100, 'desc' => '页面加载多少秒后弹出登录窗'],
|
||||
['key' => 'sign_modal_auto_expires', 'label' => '弹窗周期(小时)', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 2000, 'desc' => '关闭弹窗后多少小时内不再自动弹出,0=每次刷新都弹'],
|
||||
['key' => 'replace_wp_login', 'label' => '替换WP登录页', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'admin_sign_key', 'label' => '应急登录 Key', 'type' => 'input', 'default' => '', 'desc' => '留空为禁用。管理员可通过 /wp-login.php?aksrt_bypass=<key> 绕过前台替换'],
|
||||
['key' => 'user_signin_phone_s', 'label' => '手机号登录', 'type' => 'switch', 'default' => false, 'desc' => '允许用手机号作为用户名登录'],
|
||||
@@ -148,20 +147,20 @@ class SchemaUsers {
|
||||
['label' => '手机验证码', 'value' => 'phone'],
|
||||
['label' => '邮箱或手机', 'value' => 'email_phone'],
|
||||
]],
|
||||
['key' => 'user_signin_nopas_active', 'label' => '免密显示模式', 'type' => 'radio', 'default' => 'nopas_first', 'options' => [
|
||||
['label' => '优先免密', 'value' => 'nopas_first'],
|
||||
['label' => '优先密码', 'value' => 'password_first'],
|
||||
['label' => '仅免密', 'value' => 'nopas_only'],
|
||||
['key' => 'user_signin_nopas_active', 'label' => '免密显示模式', 'type' => 'radio', 'default' => 'nopas', 'options' => [
|
||||
['label' => '优先免密', 'value' => 'nopas'],
|
||||
['label' => '优先密码', 'value' => 'pas'],
|
||||
['label' => '仅免密', 'value' => 'only_nopas'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'signup_captcha', 'title' => '注册验证', 'fields' => [
|
||||
['key' => 'user_signup_captch', 'label' => '注册需验证', 'type' => 'switch', 'default' => true, 'desc' => '注册时需完成邮箱或手机验证'],
|
||||
['key' => 'user_signup_captch', 'label' => '注册需验证', 'type' => 'switch', 'default' => false, 'desc' => '验证码通过后才创建并登录用户'],
|
||||
['key' => 'captch_type', 'label' => '验证方式', 'type' => 'radio', 'default' => 'email', 'options' => [
|
||||
['label' => '邮箱', 'value' => 'email'],
|
||||
['label' => '手机', 'value' => 'phone'],
|
||||
['label' => '邮箱或手机', 'value' => 'email_phone'],
|
||||
]],
|
||||
['key' => 'user_signup_no_repas', 'label' => '无需重复密码', 'type' => 'switch', 'default' => true, 'desc' => '注册时不要求二次确认密码'],
|
||||
['key' => 'user_signup_no_repas', 'label' => '无需重复密码', 'type' => 'switch', 'default' => true, 'desc' => '注册时不要求二次确认密码', 'dependsOn' => ['key' => 'user_signup_captch', 'value' => true]],
|
||||
['key' => 'user_repas_captch_type', 'label' => '找回密码验证方式', 'type' => 'radio', 'default' => 'email', 'options' => [
|
||||
['label' => '邮箱', 'value' => 'email'],
|
||||
['label' => '手机', 'value' => 'phone'],
|
||||
@@ -176,8 +175,8 @@ class SchemaUsers {
|
||||
['label' => '提醒绑定邮箱和手机', 'value' => 'email_phone'],
|
||||
['label' => '关闭', 'value' => 'close'],
|
||||
]],
|
||||
['key' => 'bind_reminder_text', 'label' => '提醒文案', 'type' => 'input', 'default' => '请绑定手机号以正常使用'],
|
||||
['key' => 'bind_reminder_expires', 'label' => '提醒周期(小时)', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 720, 'desc' => '用户关闭提醒后多少小时内不再提醒'],
|
||||
['key' => 'bind_reminder_text', 'label' => '提醒文案', 'type' => 'textarea', 'default' => '为了您的账户安全,请务必完成账户绑定'],
|
||||
['key' => 'bind_reminder_expires', 'label' => '提醒周期(小时)', 'type' => 'number', 'default' => 24, 'min' => 0, 'max' => 2000],
|
||||
['key' => 'mandatory_bind', 'label' => '强制绑定', 'type' => 'radio', 'default' => 'close', 'options' => [
|
||||
['label' => '强制绑定邮箱', 'value' => 'email'],
|
||||
['label' => '强制绑定手机', 'value' => 'phone'],
|
||||
@@ -187,37 +186,34 @@ class SchemaUsers {
|
||||
['key' => 'email_set_captch', 'label' => '绑定邮箱需验证', 'type' => 'switch', 'default' => true, 'desc' => '绑定邮箱时需发送验证码确认'],
|
||||
]],
|
||||
['key' => 'referral_inline', 'title' => '推广码注册', 'fields' => [
|
||||
['key' => 'referral_registration_enabled', 'label' => '开放推广码注册', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'referral_register_field', 'label' => '注册页推广码字段', 'type' => 'select', 'default' => 'optional', 'options' => [
|
||||
['label' => '隐藏', 'value' => 'hidden'],
|
||||
['label' => '选填', 'value' => 'optional'],
|
||||
['label' => '必填', 'value' => 'required'],
|
||||
['key' => 'invit_code_s', 'label' => '推广码注册模式', 'type' => 'radio', 'default' => 'close', 'options' => [
|
||||
['label' => '关闭', 'value' => 'close'],
|
||||
['label' => '选填', 'value' => 'open'],
|
||||
['label' => '必填', 'value' => 'must'],
|
||||
]],
|
||||
['key' => 'referral_reject_invalid', 'label' => '拒绝无效推广码', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'referral_code_length', 'label' => 'Ref 码长度', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 12],
|
||||
['key' => 'referral_window_days', 'label' => '首次触达窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
|
||||
['key' => 'referral_open_desc', 'label' => '选填提示', 'type' => 'input', 'default' => '推广码为选填,没有推广码也可以注册'],
|
||||
['key' => 'referral_required_desc', 'label' => '必填提示', 'type' => 'input', 'default' => '请输入推广码'],
|
||||
['key' => 'mandatory_bind_text', 'label' => '强制绑定文案', 'type' => 'textarea', 'default' => '为了您的账户安全,请先完成账户绑定'],
|
||||
['key' => 'invit_code_open_desc', 'label' => '选填提示', 'type' => 'input', 'default' => '如您受到邀请,可填写推广码。部分推广码会有额外积分赠送'],
|
||||
['key' => 'invit_code_must_desc', 'label' => '必填提示', 'type' => 'textarea', 'default' => '<div>本站需使用推广码注册</div><div>获取推广码请与客服联系</div>'],
|
||||
]],
|
||||
['key' => 'sign_page_ui', 'title' => '页面 UI 配置', 'fields' => [
|
||||
['key' => 'sign_page_background', 'label' => '页面背景图', 'type' => 'gallery', 'default' => [], 'desc' => '登录注册页背景图,多图随机'],
|
||||
['key' => 'sign_theme_mode', 'label' => '主题模式', 'type' => 'radio', 'default' => 'default', 'options' => [
|
||||
['label' => '默认', 'value' => 'default'],
|
||||
['label' => '亮色', 'value' => 'light'],
|
||||
['label' => '暗色', 'value' => 'dark'],
|
||||
['key' => 'sign_theme_mode', 'label' => '主题模式', 'type' => 'radio', 'default' => 'no', 'options' => [
|
||||
['label' => '默认', 'value' => 'no'],
|
||||
['label' => '亮色', 'value' => 'white-theme'],
|
||||
['label' => '暗色', 'value' => 'dark-theme'],
|
||||
]],
|
||||
['key' => 'sign_show_header', 'label' => '显示顶部导航', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sign_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'center', 'options' => [
|
||||
['key' => 'sign_card_position', 'label' => '卡片位置', 'type' => 'radio', 'default' => 'right', 'options' => [
|
||||
['label' => '靠左', 'value' => 'left'],
|
||||
['label' => '居中', 'value' => 'center'],
|
||||
['label' => '靠右', 'value' => 'right'],
|
||||
]],
|
||||
['key' => 'sign_show_logo', 'label' => '显示 LOGO', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sign_show_logo', 'label' => '显示 LOGO', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'sign_page_footer', 'label' => '页脚 HTML', 'type' => 'textarea', 'default' => '', 'desc' => '登录注册页自定义页脚内容,支持 HTML'],
|
||||
]],
|
||||
['key' => 'modal_ui', 'title' => '弹窗 UI 配置', 'fields' => [
|
||||
['key' => 'sign_modal_background', 'label' => '弹窗背景图', 'type' => 'gallery', 'default' => [], 'desc' => '弹窗左侧背景图,多图随机'],
|
||||
['key' => 'sign_modal_show_logo', 'label' => '弹窗显示 LOGO', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'sign_modal_show_logo', 'label' => '弹窗显示 LOGO', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'sign_logo', 'title' => '登录框 LOGO', 'fields' => [
|
||||
['key' => 'user_logo', 'label' => '日间 LOGO', 'type' => 'gallery', 'default' => [], 'desc' => '登录框专用日间 LOGO,留空使用站点全局 LOGO'],
|
||||
|
||||
@@ -17,20 +17,6 @@ class ToolsController extends ControllerBase {
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
// Health: email verification status
|
||||
register_rest_route(self::API_NAMESPACE, '/health/email-verification', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_health_email_verification'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Health: REST block toggle
|
||||
register_rest_route(self::API_NAMESPACE, '/health/rest-block', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_set_rest_block'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Email rollback: list users with recent email changes
|
||||
register_rest_route(self::API_NAMESPACE, '/email-rollback/users', [
|
||||
'methods' => 'GET',
|
||||
@@ -213,35 +199,6 @@ class ToolsController extends ControllerBase {
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_health_email_verification(): \WP_REST_Response {
|
||||
$feature_enabled = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::registration_requires_email_verification();
|
||||
$pending_users = 0;
|
||||
|
||||
if (class_exists('\AKSRT\Auth\EmailVerification')) {
|
||||
global $wpdb;
|
||||
$pending_users = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_email_verified' AND CAST(meta_value AS UNSIGNED) = 0"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success(['data' => [
|
||||
'feature_enabled' => $feature_enabled,
|
||||
'pending_users' => $pending_users,
|
||||
'rest_blocking_active' => !empty(get_option('aksrt_rest_block_email_verification', false)),
|
||||
]]);
|
||||
}
|
||||
|
||||
public function rest_set_rest_block(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$enabled = !empty($params['enabled']);
|
||||
update_option('aksrt_rest_block_email_verification', $enabled);
|
||||
|
||||
return $this->success([
|
||||
'message' => $enabled ? __('已启用 REST 邮箱验证阻断', 'aksrt') : __('已关闭 REST 邮箱验证阻断', 'aksrt'),
|
||||
'data' => ['enabled' => $enabled],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_email_rollback_list(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\EmailRollback')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20]]);
|
||||
|
||||
@@ -207,27 +207,36 @@ class UserController extends ControllerBase {
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// ── Modal Auth (弹窗登录/注册) ──
|
||||
|
||||
// Modal login
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/modal-login', [
|
||||
// Shared public authentication contract for modal and /sign/.
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/code', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'modal_login'],
|
||||
'callback' => [$this, 'auth_code'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Modal register
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/modal-register', [
|
||||
register_rest_route(self::API_NAMESPACE, '/user/phone-code', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'user_phone_code'],
|
||||
'permission_callback' => static function () { return is_user_logged_in(); },
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/user/phone', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'user_bind_phone'],
|
||||
'permission_callback' => static function () { return is_user_logged_in(); },
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/login', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'modal_register'],
|
||||
'callback' => [$this, 'auth_login'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Current user info (for AJAX refresh)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/me', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_current_user_info'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/register', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_register'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/reset-password', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_reset_password'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Identity verification provider settings (admin only)
|
||||
@@ -277,41 +286,6 @@ class UserController extends ControllerBase {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// SMS auth: send verification code (public, rate-limited)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/send-sms-code', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_send_sms_code'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: register by phone (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/phone-register', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_phone_register'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: login by phone + password (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/phone-login', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_phone_login'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: login by phone + SMS code (public)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/phone-code-login', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_phone_code_login'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// SMS auth: bind phone to current user (logged-in)
|
||||
register_rest_route(self::API_NAMESPACE, '/auth/bind-phone', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'auth_bind_phone'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
]);
|
||||
|
||||
// ── User Restrictions / Ban ──
|
||||
|
||||
// List restrictions for a user (admin)
|
||||
@@ -401,22 +375,7 @@ class UserController extends ControllerBase {
|
||||
|
||||
if ($request->get_method() === 'GET') {
|
||||
$data = \AKSRT\Settings\Settings::get_all();
|
||||
$referral = class_exists('\AKSRT\Settings\CreditsRuntime')
|
||||
? \AKSRT\Settings\CreditsRuntime::get_referral_registration_settings()
|
||||
: [];
|
||||
$shared = class_exists('\AKSRT\Settings\CreditsRuntime')
|
||||
? \AKSRT\Settings\CreditsRuntime::get_referral_settings()
|
||||
: [];
|
||||
|
||||
return $this->success(['data' => [
|
||||
'registration_email_verification_enabled' => \AKSRT\Settings\Settings::registration_requires_email_verification(),
|
||||
'registration_email_verification_method' => \AKSRT\Settings\Settings::registration_verification_method(),
|
||||
'referral_registration_enabled' => $referral['enabled'] ?? true,
|
||||
'referral_code_length' => $shared['code_length'] ?? 12,
|
||||
'referral_window_days' => $shared['window_days'] ?? 30,
|
||||
'referral_register_field' => $referral['register_field'] ?? 'optional',
|
||||
'referral_reject_invalid' => $referral['reject_invalid'] ?? true,
|
||||
] + array_intersect_key($data, $allowed)]);
|
||||
return $this->success(['data' => array_intersect_key($data, $allowed)]);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
@@ -424,30 +383,6 @@ class UserController extends ControllerBase {
|
||||
$sanitized = $this->sanitize_fields($params, $allowed);
|
||||
$settings = array_merge($settings, $sanitized);
|
||||
|
||||
if (array_key_exists('referral_registration_enabled', $params)) {
|
||||
$settings['referral_registration_enabled'] = !empty($params['referral_registration_enabled']);
|
||||
}
|
||||
if (array_key_exists('referral_code_length', $params)) {
|
||||
$settings['referral_code_length'] = max(4, min(12, (int) $params['referral_code_length']));
|
||||
}
|
||||
if (array_key_exists('referral_window_days', $params)) {
|
||||
$settings['referral_window_days'] = max(1, min(365, (int) $params['referral_window_days']));
|
||||
}
|
||||
if (array_key_exists('referral_register_field', $params)) {
|
||||
$field = sanitize_key((string) $params['referral_register_field']);
|
||||
$settings['referral_register_field'] = in_array($field, ['hidden', 'optional', 'required'], true) ? $field : 'optional';
|
||||
}
|
||||
if (array_key_exists('referral_reject_invalid', $params)) {
|
||||
$settings['referral_reject_invalid'] = !empty($params['referral_reject_invalid']);
|
||||
}
|
||||
if (array_key_exists('registration_email_verification_enabled', $params)) {
|
||||
$settings['registration_email_verification_enabled'] = !empty($params['registration_email_verification_enabled']);
|
||||
}
|
||||
if (array_key_exists('registration_email_verification_method', $params)) {
|
||||
$method = (string) $params['registration_email_verification_method'];
|
||||
$settings['registration_email_verification_method'] = in_array($method, ['link', 'code'], true) ? $method : 'link';
|
||||
}
|
||||
|
||||
update_option('aksrt_settings', $settings);
|
||||
\AKSRT\Settings\Settings::flush_settings_cache();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Shared public authentication REST operations.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -9,286 +11,385 @@ defined('ABSPATH') || exit;
|
||||
|
||||
trait UserControllerAuthPhoneOperations {
|
||||
|
||||
/**
|
||||
* POST /auth/modal-login — 弹窗登录
|
||||
*/
|
||||
public function modal_login(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$payload = is_array($params) ? $params : [];
|
||||
$login = sanitize_text_field((string) ($payload['log'] ?? $payload['login'] ?? ''));
|
||||
$password = (string) ($payload['pwd'] ?? $payload['password'] ?? '');
|
||||
private function auth_payload(\WP_REST_Request $request): array {
|
||||
$params = $this->get_json_params($request);
|
||||
return is_array($params) ? $params : [];
|
||||
}
|
||||
|
||||
if ($login === '' || $password === '') {
|
||||
return $this->error('请输入用户名和密码');
|
||||
}
|
||||
private function auth_verify_rest_nonce(\WP_REST_Request $request, array $payload): bool {
|
||||
$nonce = sanitize_text_field((string) ($request->get_header('X-WP-Nonce') ?: ($payload['_wpnonce'] ?? '')));
|
||||
return $nonce !== '' && (bool) wp_verify_nonce($nonce, 'wp_rest');
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用', 500);
|
||||
}
|
||||
private function auth_rate_key(string $action, string $subject = ''): string {
|
||||
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'));
|
||||
return 'aksrt_auth_rate_' . md5($action . '|' . strtolower($subject) . '|' . $ip);
|
||||
}
|
||||
|
||||
$result = \AKSRT\Auth\Service::login_user([
|
||||
...$payload,
|
||||
'log' => $login,
|
||||
'pwd' => $password,
|
||||
'rememberme' => !empty($payload['rememberme']) ? (string) $payload['rememberme'] : 'forever',
|
||||
], true, true);
|
||||
private function auth_rate_limited(string $action, string $subject = '', int $limit = 8, int $ttl = 600): bool {
|
||||
$key = $this->auth_rate_key($action, $subject);
|
||||
$count = (int) get_transient($key);
|
||||
if ($count >= $limit) {
|
||||
return true;
|
||||
}
|
||||
set_transient($key, $count + 1, $ttl);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($result['success'])) {
|
||||
$error = $result['error'] ?? '登录失败';
|
||||
$verify_url = $result['verify_url'] ?? '';
|
||||
$response = ['success' => false, 'message' => $error];
|
||||
if ($verify_url) {
|
||||
$response['verify_url'] = $verify_url;
|
||||
}
|
||||
return new \WP_REST_Response($response, 401);
|
||||
}
|
||||
private function auth_code_key(string $scene, string $target): string {
|
||||
return 'aksrt_auth_code_' . md5($scene . '|' . strtolower(trim($target)));
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
return $this->success([
|
||||
'message' => __('登录成功', 'aksrt'),
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
'user' => [
|
||||
'id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user->ID, ['size' => 48])
|
||||
: get_avatar_url($user->ID, ['size' => 48]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
private function auth_store_email_code(string $scene, string $target, string $code): void {
|
||||
set_transient($this->auth_code_key($scene, $target), [
|
||||
'hash' => wp_hash_password($code),
|
||||
'attempts' => 0,
|
||||
], 10 * MINUTE_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/modal-register — 弹窗注册
|
||||
*/
|
||||
public function modal_register(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$payload = is_array($params) ? $params : [];
|
||||
$username = sanitize_user((string) ($payload['user_login'] ?? $payload['username'] ?? ''), true);
|
||||
$email = sanitize_email((string) ($payload['user_email'] ?? $payload['email'] ?? ''));
|
||||
$password = (string) ($payload['user_pass'] ?? $payload['password'] ?? '');
|
||||
$confirm = (string) ($payload['user_pass_confirm'] ?? $payload['password_confirm'] ?? $payload['confirm_password'] ?? $password);
|
||||
$ref = sanitize_key((string) ($payload['ref'] ?? ''));
|
||||
$register_nonce = sanitize_text_field((string) ($payload['_register_nonce'] ?? ''));
|
||||
$agree_terms = !empty($payload['agree_terms']) ? '1' : '';
|
||||
private function auth_consume_code(string $scene, string $target, string $code, string $channel): bool {
|
||||
if ($channel === 'phone') {
|
||||
if (!class_exists('\AKSRT\Sms\SMS')) {
|
||||
return false;
|
||||
}
|
||||
$result = \AKSRT\Sms\SMS::get_instance()->verify_code($target, $code);
|
||||
return !empty($result['success']);
|
||||
}
|
||||
|
||||
if ($username === '' || $email === '' || $password === '') {
|
||||
return $this->error('请填写所有必填项');
|
||||
}
|
||||
$key = $this->auth_code_key($scene, $target);
|
||||
$stored = get_transient($key);
|
||||
if (!is_array($stored) || empty($stored['hash'])) {
|
||||
return false;
|
||||
}
|
||||
$attempts = (int) ($stored['attempts'] ?? 0) + 1;
|
||||
if ($attempts > 6 || !wp_check_password($code, (string) $stored['hash'])) {
|
||||
$stored['attempts'] = $attempts;
|
||||
set_transient($key, $stored, 10 * MINUTE_IN_SECONDS);
|
||||
return false;
|
||||
}
|
||||
delete_transient($key);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_email($email)) {
|
||||
return $this->error('邮箱格式不正确');
|
||||
}
|
||||
private function auth_channel(string $target, string $requested = ''): string {
|
||||
if ($requested === 'phone' || preg_match('/^1[3-9]\d{9}$/', $target)) {
|
||||
return 'phone';
|
||||
}
|
||||
return 'email';
|
||||
}
|
||||
|
||||
if (strlen($password) < 6) {
|
||||
return $this->error('密码至少 6 位');
|
||||
}
|
||||
private function auth_channel_allowed(string $configured, string $channel): bool {
|
||||
return $configured === 'email_phone' || $configured === $channel;
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用', 500);
|
||||
}
|
||||
private function auth_safe_redirect(string $requested, string $fallback = ''): string {
|
||||
$fallback = $fallback !== '' ? $fallback : home_url('/');
|
||||
return wp_validate_redirect($requested, $fallback);
|
||||
}
|
||||
|
||||
$payload['user_login'] = $username;
|
||||
$payload['user_email'] = $email;
|
||||
$payload['user_pass'] = $password;
|
||||
$payload['user_pass_confirm'] = $confirm;
|
||||
$payload['ref'] = $ref;
|
||||
$payload['_register_nonce'] = $register_nonce;
|
||||
$payload['agree_terms'] = $agree_terms;
|
||||
private function auth_registration_error(string $error): string {
|
||||
if (str_contains($error, '已被') || str_contains($error, '已注册') || str_contains($error, '已使用')) {
|
||||
return __('注册信息不可用,请检查后重试', 'aksrt');
|
||||
}
|
||||
return $error;
|
||||
}
|
||||
|
||||
$result = \AKSRT\Auth\Service::register_user($payload);
|
||||
private function auth_find_user(string $target, string $channel) {
|
||||
if ($channel === 'phone') {
|
||||
$users = get_users([
|
||||
'meta_key' => 'aksrt_phone',
|
||||
'meta_value' => $target,
|
||||
'number' => 1,
|
||||
]);
|
||||
return $users[0] ?? null;
|
||||
}
|
||||
return is_email($target)
|
||||
? get_user_by('email', sanitize_email($target))
|
||||
: get_user_by('login', sanitize_user($target, true));
|
||||
}
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['error'] ?? '注册失败');
|
||||
}
|
||||
public function auth_code(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) {
|
||||
return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403);
|
||||
}
|
||||
|
||||
$verification_required = !empty($result['verification_required']);
|
||||
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
|
||||
$registered_user = null;
|
||||
$current_user = wp_get_current_user();
|
||||
if (
|
||||
$current_user instanceof \WP_User
|
||||
&& $current_user->ID > 0
|
||||
&& !empty($result['user_id'])
|
||||
&& (int) $result['user_id'] === (int) $current_user->ID
|
||||
) {
|
||||
$registered_user = $current_user;
|
||||
}
|
||||
$user = $current_user;
|
||||
$needs_verify = $verification_required;
|
||||
$scene = sanitize_key((string) ($payload['scene'] ?? ''));
|
||||
$target = trim(sanitize_text_field((string) ($payload['target'] ?? '')));
|
||||
$channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? '')));
|
||||
$obk = sanitize_text_field((string) ($payload['obk'] ?? ''));
|
||||
$oauth_bind = $obk !== '' && class_exists('\AKSRT\Auth\Service') && is_array(\AKSRT\Auth\Service::get_oauth_bind_context($obk));
|
||||
if (!in_array($scene, ['login', 'register', 'resetpassword'], true)) {
|
||||
return $this->error(__('请求类型无效', 'aksrt'));
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
if (\AKSRT\Settings\Settings::close_sign()) return $this->error(__('登录注册已关闭', 'aksrt'), 403);
|
||||
if (($scene === 'login' || $scene === 'resetpassword') && \AKSRT\Settings\Settings::close_signin() && !$oauth_bind) return $this->error(__('当前登录方式已关闭', 'aksrt'), 403);
|
||||
if ($scene === 'register' && !\AKSRT\Settings\Settings::registration_enabled() && !$oauth_bind) return $this->error(__('当前已关闭新用户注册', 'aksrt'), 403);
|
||||
$type = $scene === 'login' ? \AKSRT\Settings\Settings::passwordless_login_type() : ($scene === 'register' ? \AKSRT\Settings\Settings::signup_verification_type() : \AKSRT\Settings\Settings::reset_password_verification_type());
|
||||
$enabled = $scene === 'login' ? \AKSRT\Settings\Settings::passwordless_login_enabled() : ($scene === 'register' ? \AKSRT\Settings\Settings::signup_verification_enabled() : true);
|
||||
if (!$enabled || ($type !== 'email_phone' && $type !== $channel)) return $this->error(__('当前验证方式不可用', 'aksrt'), 403);
|
||||
}
|
||||
if (($channel === 'email' && !is_email($target)) || ($channel === 'phone' && !preg_match('/^1[3-9]\d{9}$/', $target))) {
|
||||
return $this->error(__('请输入有效的邮箱或手机号', 'aksrt'));
|
||||
}
|
||||
if ($this->auth_rate_limited('code:' . $scene, $target, 5, HOUR_IN_SECONDS)) {
|
||||
return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429);
|
||||
}
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_scene = $scene === 'resetpassword' ? 'lostpassword' : $scene;
|
||||
$captcha = aksrt_require_captcha($captcha_scene, $payload);
|
||||
if ($captcha !== true) {
|
||||
return $this->error((string) $captcha);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => $needs_verify ? __('注册成功,请验证邮箱', 'aksrt') : __('注册成功', 'aksrt'),
|
||||
'needs_verification' => $needs_verify,
|
||||
'verify_url' => $result['verify_url'] ?? '',
|
||||
'redirect' => $verification_required ? ($result['verify_url'] ?? '') : $login_url,
|
||||
'verification_required' => $verification_required,
|
||||
'login_url' => $login_url,
|
||||
'user_login' => $result['user_login'] ?? $username,
|
||||
'user_email' => $email,
|
||||
'notice' => (string) ($result['notice'] ?? ''),
|
||||
'user' => $registered_user ? [
|
||||
'id' => $registered_user->ID,
|
||||
'display_name' => $registered_user->display_name,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($registered_user->ID, ['size' => 48])
|
||||
: get_avatar_url($registered_user->ID, ['size' => 48]),
|
||||
] : ($user->ID ? [
|
||||
'id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user->ID, ['size' => 48])
|
||||
: get_avatar_url($user->ID, ['size' => 48]),
|
||||
] : null),
|
||||
]);
|
||||
}
|
||||
$generic = __('如果信息有效,验证码将发送到该账号', 'aksrt');
|
||||
if ($scene !== 'register' && !$this->auth_find_user($target, $channel)) {
|
||||
return $this->success(['message' => $generic, 'countdown' => 60]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /auth/me — 当前用户信息(用于 AJAX 刷新)
|
||||
*/
|
||||
public function get_current_user_info(): \WP_REST_Response {
|
||||
$user = wp_get_current_user();
|
||||
if (!$user->ID) {
|
||||
return $this->error('未登录', 401);
|
||||
}
|
||||
if ($channel === 'phone') {
|
||||
if (!class_exists('\AKSRT\Sms\SMS')) {
|
||||
return $this->error(__('短信服务不可用', 'aksrt'), 503);
|
||||
}
|
||||
$result = \AKSRT\Sms\SMS::get_instance()->send_code($target);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error((string) ($result['message'] ?? __('验证码发送失败', 'aksrt')));
|
||||
}
|
||||
return $this->success(['message' => $generic, 'countdown' => (int) ($result['remaining_time'] ?? 60)]);
|
||||
}
|
||||
|
||||
$credits = 0;
|
||||
if (class_exists('\AKSRT\Credits\Credits')) {
|
||||
$credits = \AKSRT\Credits\Credits::balance($user->ID);
|
||||
}
|
||||
$code = (string) random_int(100000, 999999);
|
||||
$this->auth_store_email_code($scene, $target, $code);
|
||||
$subject = sprintf(__('[%s] 登录注册验证码', 'aksrt'), get_bloginfo('name'));
|
||||
$message = sprintf(__('您的验证码是:%s,10 分钟内有效。', 'aksrt'), $code);
|
||||
if (!wp_mail($target, $subject, $message)) {
|
||||
delete_transient($this->auth_code_key($scene, $target));
|
||||
if ($scene !== 'register') {
|
||||
return $this->success(['message' => $generic, 'countdown' => 60]);
|
||||
}
|
||||
return $this->error(__('验证码发送失败,请稍后再试', 'aksrt'), 500);
|
||||
}
|
||||
return $this->success(['message' => $generic, 'countdown' => 60]);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'id' => $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'user_email' => $user->user_email,
|
||||
'avatar_url' => function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user->ID, ['size' => 48])
|
||||
: get_avatar_url($user->ID, ['size' => 48]),
|
||||
'is_admin' => current_user_can('manage_options'),
|
||||
'credits' => $credits,
|
||||
],
|
||||
]);
|
||||
}
|
||||
public function auth_login(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) {
|
||||
return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403);
|
||||
}
|
||||
$target = trim(sanitize_text_field((string) ($payload['target'] ?? $payload['log'] ?? '')));
|
||||
$obk = sanitize_text_field((string) ($payload['obk'] ?? ''));
|
||||
$oauth_data = $obk !== '' && class_exists('\AKSRT\Auth\Service')
|
||||
? \AKSRT\Auth\Service::get_oauth_bind_context($obk)
|
||||
: null;
|
||||
if ($obk !== '' && !$oauth_data) {
|
||||
return $this->error(__('绑定会话已过期,请重新使用第三方登录', 'aksrt'), 410);
|
||||
}
|
||||
if (
|
||||
class_exists('\AKSRT\Settings\Settings')
|
||||
&& (\AKSRT\Settings\Settings::close_sign() || (!$oauth_data && \AKSRT\Settings\Settings::close_signin()))
|
||||
) {
|
||||
return $this->error(__('当前登录方式已关闭', 'aksrt'), 403);
|
||||
}
|
||||
if ($target === '' || (preg_match('/^1[3-9]\d{9}$/', $target) && empty($payload['code']) && class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::signin_phone_enabled())) {
|
||||
return $this->error(__('账号或密码错误', 'aksrt'), 401);
|
||||
}
|
||||
if ($this->auth_rate_limited('login', $target)) {
|
||||
return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429);
|
||||
}
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha($oauth_data ? 'bind' : 'login', $payload);
|
||||
if ($captcha !== true) {
|
||||
return $this->error((string) $captcha);
|
||||
}
|
||||
}
|
||||
$redirect = $this->auth_safe_redirect((string) ($payload['redirect_to'] ?? ''), home_url('/'));
|
||||
|
||||
/**
|
||||
* POST /auth/send-sms-code — 发送短信验证码
|
||||
*/
|
||||
public function auth_send_sms_code(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$phone = sanitize_text_field((string) ($params['phone'] ?? ''));
|
||||
$scene = sanitize_key((string) ($params['scene'] ?? 'register'));
|
||||
if (!array_key_exists($scene, function_exists('aksrt_captcha_scenes') ? aksrt_captcha_scenes() : [])) {
|
||||
$scene = 'register';
|
||||
}
|
||||
if (!empty($payload['code'])) {
|
||||
if (!class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::passwordless_login_enabled()) {
|
||||
return $this->error(__('当前未开启免密登录', 'aksrt'));
|
||||
}
|
||||
$channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? '')));
|
||||
$configured = \AKSRT\Settings\Settings::passwordless_login_type();
|
||||
if (!$this->auth_channel_allowed($configured, $channel)) {
|
||||
return $this->error(__('当前验证方式不可用', 'aksrt'), 403);
|
||||
}
|
||||
$user = $this->auth_find_user($target, $channel);
|
||||
if (!$user || !$this->auth_consume_code('login', $target, sanitize_text_field((string) $payload['code']), $channel)) {
|
||||
return $this->error(__('账号或验证码错误', 'aksrt'), 401);
|
||||
}
|
||||
wp_set_current_user((int) $user->ID);
|
||||
wp_set_auth_cookie((int) $user->ID, !empty($payload['rememberme']));
|
||||
do_action('wp_login', $user->user_login, $user);
|
||||
if ($oauth_data) {
|
||||
\AKSRT\Auth\Service::finalize_oauth_binding((int) $user->ID, $oauth_data, $obk);
|
||||
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : $redirect;
|
||||
}
|
||||
return $this->success(['message' => $oauth_data ? __('账号绑定成功', 'aksrt') : __('登录成功', 'aksrt'), 'redirect' => $redirect]);
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha($scene, is_array($params) ? $params : []);
|
||||
if ($captcha_result !== true) {
|
||||
return $this->error((string) $captcha_result, 400);
|
||||
}
|
||||
}
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error(__('认证服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
$payload['log'] = $target;
|
||||
$payload['pwd'] = (string) ($payload['password'] ?? $payload['pwd'] ?? '');
|
||||
$payload['redirect_to'] = $redirect;
|
||||
$result = \AKSRT\Auth\Service::login_user($payload, $oauth_data ? 'bind' : 'login');
|
||||
if (empty($result['success'])) {
|
||||
return $this->error(__('账号或密码错误', 'aksrt'), 401);
|
||||
}
|
||||
if ($oauth_data) {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return $this->error(__('账号或密码错误', 'aksrt'), 401);
|
||||
}
|
||||
\AKSRT\Auth\Service::finalize_oauth_binding($user_id, $oauth_data, $obk);
|
||||
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : $redirect;
|
||||
}
|
||||
return $this->success(['message' => $oauth_data ? __('账号绑定成功', 'aksrt') : __('登录成功', 'aksrt'), 'redirect' => $redirect]);
|
||||
}
|
||||
|
||||
if (!class_exists('\AKSRT\Sms\SMS')) {
|
||||
return $this->error('短信服务不可用');
|
||||
}
|
||||
public function auth_register(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) {
|
||||
return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403);
|
||||
}
|
||||
$obk = sanitize_text_field((string) ($payload['obk'] ?? ''));
|
||||
$oauth_data = $obk !== '' && class_exists('\AKSRT\Auth\Service')
|
||||
? \AKSRT\Auth\Service::get_oauth_bind_context($obk)
|
||||
: null;
|
||||
if ($obk !== '' && !$oauth_data) {
|
||||
return $this->error(__('绑定会话已过期,请重新使用第三方登录', 'aksrt'), 410);
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\Settings') && (\AKSRT\Settings\Settings::close_sign() || (!$oauth_data && !\AKSRT\Settings\Settings::registration_enabled()))) {
|
||||
return $this->error(__('当前已关闭新用户注册', 'aksrt'), 403);
|
||||
}
|
||||
if ($this->auth_rate_limited('register')) {
|
||||
return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429);
|
||||
}
|
||||
$target = trim(sanitize_text_field((string) ($payload['target'] ?? $payload['user_email'] ?? $payload['phone'] ?? '')));
|
||||
$channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? '')));
|
||||
$signup_verification = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::signup_verification_enabled();
|
||||
$configured = $signup_verification ? \AKSRT\Settings\Settings::signup_verification_type() : 'email';
|
||||
if (!$this->auth_channel_allowed($configured, $channel)) {
|
||||
return $this->error(__('当前注册方式不可用', 'aksrt'), 403);
|
||||
}
|
||||
if (($channel === 'email' && !is_email($target)) || ($channel === 'phone' && !preg_match('/^1[3-9]\d{9}$/', $target))) {
|
||||
return $this->error(__('请输入有效的邮箱或手机号', 'aksrt'));
|
||||
}
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha('register', $payload);
|
||||
if ($captcha !== true) {
|
||||
return $this->error((string) $captcha);
|
||||
}
|
||||
}
|
||||
if ($signup_verification) {
|
||||
if (!$this->auth_consume_code('register', $target, sanitize_text_field((string) ($payload['code'] ?? '')), $channel)) {
|
||||
return $this->error(__('验证码错误或已过期', 'aksrt'));
|
||||
}
|
||||
$payload['_auth_code_verified'] = true;
|
||||
}
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error(__('认证服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
if ($channel === 'phone') {
|
||||
$payload['phone'] = $target;
|
||||
$result = \AKSRT\Auth\Service::register_by_phone($payload);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($this->auth_registration_error((string) ($result['error'] ?? __('注册失败', 'aksrt'))));
|
||||
}
|
||||
$user_id = (int) ($result['user_id'] ?? 0);
|
||||
if ($oauth_data && $user_id > 0) {
|
||||
\AKSRT\Auth\Service::finalize_oauth_binding($user_id, $oauth_data, $obk);
|
||||
}
|
||||
return $this->success(['message' => $oauth_data ? __('注册并绑定成功', 'aksrt') : __('注册成功', 'aksrt'), 'redirect' => $result['redirect'] ?? home_url('/'), 'notice' => (string) ($result['notice'] ?? '')]);
|
||||
}
|
||||
$payload['user_email'] = sanitize_email($target);
|
||||
$payload['user_pass'] = (string) ($payload['password'] ?? '');
|
||||
$payload['user_pass_confirm'] = (string) ($payload['password_confirm'] ?? $payload['password'] ?? '');
|
||||
$result = \AKSRT\Auth\Service::register_user($payload);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($this->auth_registration_error((string) ($result['error'] ?? __('注册失败', 'aksrt'))));
|
||||
}
|
||||
$user_id = (int) ($result['user_id'] ?? 0);
|
||||
if ($oauth_data && $user_id > 0) {
|
||||
\AKSRT\Auth\Service::finalize_oauth_binding($user_id, $oauth_data, $obk);
|
||||
}
|
||||
return $this->success([
|
||||
'message' => $oauth_data ? __('注册并绑定成功', 'aksrt') : __('注册成功', 'aksrt'),
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
'notice' => (string) ($result['notice'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
$result = \AKSRT\Sms\SMS::get_instance()->send_code($phone);
|
||||
public function auth_reset_password(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) {
|
||||
return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403);
|
||||
}
|
||||
$target = trim(sanitize_text_field((string) ($payload['target'] ?? '')));
|
||||
$channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? '')));
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
if (\AKSRT\Settings\Settings::close_sign() || \AKSRT\Settings\Settings::close_signin() || !$this->auth_channel_allowed(\AKSRT\Settings\Settings::reset_password_verification_type(), $channel)) {
|
||||
return $this->error(__('当前验证方式不可用', 'aksrt'), 403);
|
||||
}
|
||||
}
|
||||
if (($channel === 'email' && !is_email($target)) || ($channel === 'phone' && !preg_match('/^1[3-9]\d{9}$/', $target))) {
|
||||
return $this->error(__('请输入有效的邮箱或手机号', 'aksrt'));
|
||||
}
|
||||
if ($this->auth_rate_limited('reset-password', $target, 8, HOUR_IN_SECONDS)) {
|
||||
return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429);
|
||||
}
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha('lostpassword', $payload);
|
||||
if ($captcha !== true) {
|
||||
return $this->error((string) $captcha);
|
||||
}
|
||||
}
|
||||
$user = $this->auth_find_user($target, $channel);
|
||||
$password = (string) ($payload['password'] ?? '');
|
||||
$confirm = (string) ($payload['password_confirm'] ?? '');
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error(__('认证服务不可用', 'aksrt'), 500);
|
||||
}
|
||||
$password_check = \AKSRT\Auth\Service::validate_password($password, $confirm);
|
||||
if ($password_check !== true) {
|
||||
return $this->error((string) $password_check);
|
||||
}
|
||||
if (!$user || !$this->auth_consume_code('resetpassword', $target, sanitize_text_field((string) ($payload['code'] ?? '')), $channel)) {
|
||||
return $this->error(__('账号或验证码错误', 'aksrt'));
|
||||
}
|
||||
wp_set_password($password, (int) $user->ID);
|
||||
return $this->success(['message' => __('密码已重置,请重新登录', 'aksrt'), 'redirect' => function_exists('aksrt_get_sign_page_url') ? aksrt_get_sign_page_url('signin') : wp_login_url()]);
|
||||
}
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'message' => $result['message'],
|
||||
'remaining_time' => $result['remaining_time'] ?? 60,
|
||||
]);
|
||||
}
|
||||
public function user_phone_code(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) {
|
||||
return $this->error(__('安全验证失败', 'aksrt'), 403);
|
||||
}
|
||||
$phone = sanitize_text_field((string) ($payload['phone'] ?? ''));
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return $this->error(__('手机号格式不正确', 'aksrt'));
|
||||
}
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha = aksrt_require_captcha('bind_phone', $payload);
|
||||
if ($captcha !== true) return $this->error((string) $captcha);
|
||||
}
|
||||
if (!class_exists('\AKSRT\Sms\SMS')) return $this->error(__('短信服务不可用', 'aksrt'), 503);
|
||||
$result = \AKSRT\Sms\SMS::get_instance()->send_code($phone);
|
||||
return !empty($result['success'])
|
||||
? $this->success(['message' => __('验证码已发送', 'aksrt'), 'countdown' => (int) ($result['remaining_time'] ?? 60)])
|
||||
: $this->error((string) ($result['message'] ?? __('发送失败', 'aksrt')));
|
||||
}
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $result['message'],
|
||||
];
|
||||
if (isset($result['remaining_time'])) {
|
||||
$response['data'] = ['remaining_time' => $result['remaining_time']];
|
||||
}
|
||||
return new \WP_REST_Response($response, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/phone-register — 手机号注册
|
||||
*/
|
||||
public function auth_phone_register(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::register_by_phone($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'message' => $result['message'] ?? '注册成功',
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
'user_id' => $result['user_id'] ?? 0,
|
||||
'notice' => (string) ($result['notice'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '注册失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/phone-login — 手机号 + 密码登录
|
||||
*/
|
||||
public function auth_phone_login(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::login_by_phone_password($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '登录失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/phone-code-login — 手机号 + 验证码免密登录
|
||||
*/
|
||||
public function auth_phone_code_login(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::login_by_phone_code($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success([
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '登录失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /auth/bind-phone — 绑定手机号
|
||||
*/
|
||||
public function auth_bind_phone(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Auth\Service')) {
|
||||
return $this->error('认证服务不可用');
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$result = \AKSRT\Auth\Service::bind_phone($params);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
return $this->success(['message' => $result['message']]);
|
||||
}
|
||||
|
||||
return $this->error($result['error'] ?? '绑定失败');
|
||||
}
|
||||
public function user_bind_phone(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$payload = $this->auth_payload($request);
|
||||
if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403);
|
||||
if (!class_exists('\AKSRT\Auth\Service')) return $this->error(__('认证服务不可用', 'aksrt'), 500);
|
||||
$result = \AKSRT\Auth\Service::bind_phone($payload);
|
||||
return !empty($result['success']) ? $this->success(['message' => $result['message'] ?? __('绑定成功', 'aksrt')]) : $this->error((string) ($result['error'] ?? __('绑定失败', 'aksrt')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +167,7 @@ class EmailRollback {
|
||||
if (count($log) > 10) $log = array_slice($log, -10);
|
||||
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
|
||||
|
||||
if (class_exists('AKSRT\\Auth\\Service')) {
|
||||
Service::log_event('admin.email_rollback', [
|
||||
'uid' => $user_id,
|
||||
'by' => $by_admin_id ?: get_current_user_id(),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => sprintf(__('已将用户邮箱回滚为 %s', 'aksrt'), $previous),
|
||||
];
|
||||
|
||||
@@ -1,523 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Auth;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class EmailVerification {
|
||||
|
||||
public static function user_requires_email_verification($user): bool {
|
||||
if (!$user instanceof \WP_User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\AKSRT\Settings\Settings::registration_requires_email_verification()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !self::is_email_verified((int) $user->ID);
|
||||
}
|
||||
|
||||
public static function is_email_verified(int $user_id): bool {
|
||||
return (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
|
||||
}
|
||||
|
||||
public static function mark_email_verified(int $user_id): void {
|
||||
$was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
|
||||
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
|
||||
self::clear_email_verification($user_id);
|
||||
// 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。
|
||||
// 监听方负责自己幂等(Ref 关系使用原子奖励状态处理重复事件)。
|
||||
if (!$was_verified) {
|
||||
do_action('aksrt_email_verified', $user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public static function clear_email_verification(int $user_id): void {
|
||||
delete_user_meta($user_id, Service::EMAIL_METHOD_META);
|
||||
delete_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META);
|
||||
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
|
||||
delete_user_meta($user_id, Service::EMAIL_EXPIRES_META);
|
||||
delete_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META);
|
||||
delete_user_meta($user_id, Service::EMAIL_TARGET_META);
|
||||
delete_user_meta($user_id, Service::EMAIL_OBK_META);
|
||||
self::reset_email_code_attempt_trackers($user_id);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 公开 API:管理员后台 / 内部业务调用
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 是否已验证邮箱(公开版本,给后台 UI / 用户列表渲染用)。
|
||||
*/
|
||||
public static function user_is_email_verified(int $user_id): bool {
|
||||
return self::is_email_verified($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取一个用户当前的验证状态摘要,用于后台资料页展示。
|
||||
*
|
||||
* @return array{verified:bool, method:string, target_email:string, expires_at:int, requested_at:int, last_admin_log:array}
|
||||
*/
|
||||
public static function get_user_verification_state(int $user_id): array {
|
||||
return [
|
||||
'verified' => self::is_email_verified($user_id),
|
||||
'method' => (string) get_user_meta($user_id, Service::EMAIL_METHOD_META, true),
|
||||
'target_email' => (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true),
|
||||
'expires_at' => (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true),
|
||||
'requested_at' => (int) get_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, true),
|
||||
'last_admin_log' => (array) (get_user_meta($user_id, 'aksrt_email_verification_admin_log', true) ?: []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员:把指定用户标记为已验证(清除所有进行中的验证 meta)。
|
||||
*/
|
||||
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
self::mark_email_verified($user_id);
|
||||
self::record_admin_action($user_id, 'mark_verified', $by_admin_id);
|
||||
Service::log_event('admin.action', ['action' => 'mark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
|
||||
return ['success' => true, 'message' => __('已标记为已验证', 'aksrt')];
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员:撤销已验证状态(用于测试 / 回滚 / 应对邮箱泄漏)。
|
||||
*/
|
||||
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0);
|
||||
self::clear_email_verification($user_id);
|
||||
self::record_admin_action($user_id, 'unmark_verified', $by_admin_id);
|
||||
Service::log_event('admin.action', ['action' => 'unmark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
|
||||
return ['success' => true, 'message' => __('已撤销验证状态', 'aksrt')];
|
||||
}
|
||||
/**
|
||||
* 管理员:立即发送 / 重发验证邮件(绕过 60s 节流)。
|
||||
*/
|
||||
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
|
||||
}
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('用户不存在', 'aksrt')];
|
||||
}
|
||||
|
||||
$method = Service::current_registration_verification_method();
|
||||
$email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true);
|
||||
if (!in_array($method, ['link', 'code'], true)) {
|
||||
$method = \AKSRT\Settings\Settings::registration_verification_method();
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
$email = (string) $user->user_email;
|
||||
}
|
||||
|
||||
$result = self::begin_email_verification($user, $method, [
|
||||
'email' => $email,
|
||||
'obk' => (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true),
|
||||
'bypass_throttle' => true,
|
||||
'force' => true,
|
||||
]);
|
||||
|
||||
// 标记为待验证(确保 admin 主动发邮件后用户被视为未验证)
|
||||
if (!empty($result['success'])) {
|
||||
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0);
|
||||
self::record_admin_action($user_id, 'send_verification', $by_admin_id);
|
||||
Service::log_event('admin.action', ['action' => 'send_verification', 'uid' => $user_id, 'by' => $by_admin_id]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把已存在的用户视为受信任(grandfather):写入 aksrt_email_verified=1。
|
||||
*
|
||||
* 仅写入 meta 不存在的用户;已经显式为 0 的(正在走验证流程的新注册)保持不动。
|
||||
*
|
||||
* 用户量 > 50000 时改为异步分批;否则内联完成。返回处理总数。
|
||||
*
|
||||
* @param bool $force 强制再次执行(即便 backfill_done 标记已存在),调试用
|
||||
*/
|
||||
public static function backfill_legacy_users_as_verified(bool $force = false): int {
|
||||
if (!$force && get_option('aksrt_email_verification_backfill_done')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
if (function_exists('count_users')) {
|
||||
$counts = count_users();
|
||||
$total = (int) ($counts['total_users'] ?? 0);
|
||||
}
|
||||
|
||||
// 大表保护:超过 5w 用户改为后台 cron 分批
|
||||
if ($total > 50000) {
|
||||
if (!wp_next_scheduled('aksrt_email_verification_backfill_batch')) {
|
||||
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [0]);
|
||||
}
|
||||
update_option('aksrt_email_verification_backfill_log', [
|
||||
'mode' => 'async',
|
||||
'total' => $total,
|
||||
'scheduled' => time(),
|
||||
'by' => get_current_user_id(),
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 内联分批,每批 500
|
||||
$updated = 0;
|
||||
$paged = 1;
|
||||
do {
|
||||
$users = get_users([
|
||||
'number' => 500,
|
||||
'paged' => $paged,
|
||||
'fields' => ['ID'],
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => Service::EMAIL_VERIFIED_META,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if (empty($users)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($users as $u) {
|
||||
update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1);
|
||||
$updated++;
|
||||
}
|
||||
|
||||
$paged++;
|
||||
// 让 MySQL 喘口气
|
||||
if (function_exists('usleep')) {
|
||||
usleep(50000);
|
||||
}
|
||||
} while (count($users) === 500 && $paged < 200); // 上限 10w 兜底防呆
|
||||
|
||||
update_option('aksrt_email_verification_backfill_done', time());
|
||||
update_option('aksrt_email_verification_backfill_log', [
|
||||
'mode' => 'inline',
|
||||
'count' => $updated,
|
||||
'ran_at' => time(),
|
||||
'by' => get_current_user_id(),
|
||||
]);
|
||||
Service::log_event('admin.backfill', ['count' => $updated, 'by' => get_current_user_id()]);
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步分批回填:cron 钩子。每批 500,自动调度下一批直到完成。
|
||||
*/
|
||||
public static function run_async_backfill_batch(int $offset = 0): void {
|
||||
$users = get_users([
|
||||
'number' => 500,
|
||||
'offset' => max(0, $offset),
|
||||
'fields' => ['ID'],
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => Service::EMAIL_VERIFIED_META,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if (empty($users)) {
|
||||
update_option('aksrt_email_verification_backfill_done', time());
|
||||
$log = (array) get_option('aksrt_email_verification_backfill_log', []);
|
||||
$log['finished_at'] = time();
|
||||
update_option('aksrt_email_verification_backfill_log', $log);
|
||||
Service::log_event('admin.backfill.finished', []);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($users as $u) {
|
||||
update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1);
|
||||
}
|
||||
|
||||
// 调度下一批
|
||||
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [$offset + 500]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录管理员操作日志(每个用户最多 10 条 ring buffer)。
|
||||
*/
|
||||
public static function record_admin_action(int $user_id, string $action, int $by_admin_id): void {
|
||||
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
|
||||
if (!is_array($log)) {
|
||||
$log = [];
|
||||
}
|
||||
$log[] = [
|
||||
't' => time(),
|
||||
'action' => $action,
|
||||
'by' => $by_admin_id ?: get_current_user_id(),
|
||||
];
|
||||
if (count($log) > 10) {
|
||||
$log = array_slice($log, -10);
|
||||
}
|
||||
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
|
||||
}
|
||||
// ============================================================
|
||||
// 验证码失败计数 / IP 节流
|
||||
// ============================================================
|
||||
|
||||
public static function email_code_ip_throttle_key(int $user_id): string {
|
||||
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
if ($user_id <= 0 || $ip === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'aksrt_email_code_try_' . $user_id . '_' . md5($ip);
|
||||
}
|
||||
|
||||
public static function reset_email_code_attempt_trackers(int $user_id): void {
|
||||
delete_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META);
|
||||
$ip_key = self::email_code_ip_throttle_key($user_id);
|
||||
if ($ip_key !== '') {
|
||||
delete_transient($ip_key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function fail_email_code_attempt(int $user_id): array {
|
||||
$attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true);
|
||||
$attempts++;
|
||||
update_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, $attempts);
|
||||
|
||||
$ip_attempts = 0;
|
||||
$ip_key = self::email_code_ip_throttle_key($user_id);
|
||||
if ($ip_key !== '') {
|
||||
$ip_attempts = (int) get_transient($ip_key);
|
||||
$ip_attempts++;
|
||||
set_transient($ip_key, $ip_attempts, Service::EMAIL_CODE_IP_TTL);
|
||||
}
|
||||
|
||||
if ($attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS || $ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
|
||||
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
|
||||
update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0);
|
||||
Service::log_event('verify.code.locked', ['uid' => $user_id, 'attempts' => $attempts]);
|
||||
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
|
||||
}
|
||||
|
||||
Service::log_event('verify.code.fail', ['uid' => $user_id, 'attempts' => $attempts]);
|
||||
return ['success' => false, 'error' => __('邮箱验证码错误或已过期,请重新输入。', 'aksrt')];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 验证流程:发起 / 校验 / 完成
|
||||
// ============================================================
|
||||
|
||||
public static function begin_email_verification(\WP_User $user, string $method, array $args = []): array {
|
||||
$method = in_array($method, ['link', 'code'], true) ? $method : 'link';
|
||||
$email = sanitize_email((string) ($args['email'] ?? $user->user_email));
|
||||
$obk = sanitize_text_field((string) ($args['obk'] ?? ''));
|
||||
$bypass_throttle = !empty($args['bypass_throttle']);
|
||||
if ($email === '' || !is_email($email)) {
|
||||
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,无法发送验证邮件', 'aksrt')];
|
||||
}
|
||||
|
||||
// 节流:仅在非管理员路径生效(admin_send_verification 会传 bypass_throttle=true)
|
||||
if (!$bypass_throttle) {
|
||||
$last_requested = (int) get_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, true);
|
||||
if ($last_requested > 0 && (time() - $last_requested) < 60 && empty($args['force'])) {
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
$expires_at = time() + (\AKSRT\Settings\Settings::registration_verification_ttl_minutes() * MINUTE_IN_SECONDS);
|
||||
update_user_meta($user->ID, Service::EMAIL_VERIFIED_META, 0);
|
||||
update_user_meta($user->ID, Service::EMAIL_METHOD_META, $method);
|
||||
update_user_meta($user->ID, Service::EMAIL_TARGET_META, $email);
|
||||
update_user_meta($user->ID, Service::EMAIL_EXPIRES_META, $expires_at);
|
||||
update_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, time());
|
||||
if ($obk !== '') {
|
||||
update_user_meta($user->ID, Service::EMAIL_OBK_META, $obk);
|
||||
}
|
||||
|
||||
delete_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META);
|
||||
delete_user_meta($user->ID, Service::EMAIL_CODE_HASH_META);
|
||||
|
||||
if ($method === 'code') {
|
||||
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
update_user_meta($user->ID, Service::EMAIL_CODE_HASH_META, wp_hash_password($code));
|
||||
// 新验证码 = 重置失败计数 / IP 节流,避免被旧计数立即锁死
|
||||
self::reset_email_code_attempt_trackers((int) $user->ID);
|
||||
$sent = self::send_registration_verification_code_email($user, $email, $code);
|
||||
} else {
|
||||
$token = wp_generate_password(48, false, false);
|
||||
update_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META, hash('sha256', $token));
|
||||
$sent = self::send_registration_verification_link_email($user, $email, $token);
|
||||
}
|
||||
|
||||
if (!$sent) {
|
||||
self::clear_email_verification((int) $user->ID);
|
||||
Service::log_event('email.send.fail', ['uid' => (int) $user->ID, 'method' => $method]);
|
||||
return ['success' => false, 'error' => __('验证邮件发送失败,请稍后重试', 'aksrt')];
|
||||
}
|
||||
|
||||
Service::log_event('email.send.ok', ['uid' => (int) $user->ID, 'method' => $method]);
|
||||
|
||||
$message = $method === 'code'
|
||||
? __('注册成功,验证码已发送到你的邮箱,请完成验证后继续。', 'aksrt')
|
||||
: __('注册成功,验证邮件已发送到你的邮箱,请点击邮件中的链接完成验证。', 'aksrt');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
public static function verify_email_token(int $user_id, string $token): array {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
|
||||
}
|
||||
|
||||
$stored_hash = (string) get_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META, true);
|
||||
$expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true);
|
||||
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, hash('sha256', $token))) {
|
||||
return ['success' => false, 'error' => __('验证链接无效或已过期,请重新发送验证邮件。', 'aksrt')];
|
||||
}
|
||||
|
||||
return self::complete_email_verification($user);
|
||||
}
|
||||
|
||||
public static function verify_email_code(int $user_id, string $code): array {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
|
||||
}
|
||||
|
||||
// 先做 IP 节流:相同 IP 在 EMAIL_CODE_IP_TTL 内累计失败到上限即直接拒绝
|
||||
$ip_key = self::email_code_ip_throttle_key($user_id);
|
||||
if ($ip_key !== '') {
|
||||
$ip_attempts = (int) get_transient($ip_key);
|
||||
if ($ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
|
||||
Service::log_event('verify.code.ip_throttled', ['uid' => $user_id, 'attempts' => $ip_attempts]);
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
// 用户级失败计数:达到上限后该验证码立即作废
|
||||
$user_attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true);
|
||||
if ($user_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
|
||||
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
|
||||
update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0);
|
||||
Service::log_event('verify.code.locked.read', ['uid' => $user_id]);
|
||||
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
|
||||
}
|
||||
|
||||
$stored_hash = (string) get_user_meta($user_id, Service::EMAIL_CODE_HASH_META, true);
|
||||
$expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true);
|
||||
if ($stored_hash === '' || $expires_at <= time() || !wp_check_password($code, $stored_hash)) {
|
||||
return self::fail_email_code_attempt($user_id);
|
||||
}
|
||||
|
||||
self::reset_email_code_attempt_trackers($user_id);
|
||||
return self::complete_email_verification($user);
|
||||
}
|
||||
|
||||
public static function complete_email_verification(\WP_User $user): array {
|
||||
$user_id = (int) $user->ID;
|
||||
$obk = (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true);
|
||||
$oauth = $obk !== '' ? Service::get_oauth_bind_context($obk) : null;
|
||||
$email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true);
|
||||
$was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
|
||||
|
||||
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
|
||||
self::clear_email_verification($user_id);
|
||||
Service::log_event('verify.success', ['uid' => $user_id]);
|
||||
// 状态跳变时触发 hook,Ref 推广奖励等模块据此发奖
|
||||
if (!$was_verified) {
|
||||
do_action('aksrt_email_verified', $user_id);
|
||||
}
|
||||
if ($email !== '' && is_email($email)) {
|
||||
self::send_registration_verified_notice_email($user, $email);
|
||||
}
|
||||
|
||||
if ($oauth && is_array($oauth)) {
|
||||
Service::finalize_oauth_registration($user_id, $oauth, $obk);
|
||||
} elseif (\AKSRT\Settings\Settings::registration_auto_login_after_verify()) {
|
||||
wp_set_current_user($user_id);
|
||||
wp_set_auth_cookie($user_id, true);
|
||||
}
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
|
||||
if (!\AKSRT\Settings\Settings::registration_auto_login_after_verify()) {
|
||||
$redirect = aksrt_get_login_url();
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __('邮箱验证成功,即将进入你的账号。', 'aksrt'),
|
||||
'redirect' => $redirect,
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 通知邮件
|
||||
// ============================================================
|
||||
|
||||
public static function send_registration_verification_link_email(\WP_User $user, string $email, string $token): bool {
|
||||
$url = Service::get_email_verification_url((int) $user->ID, ['token' => $token]);
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('验证你的邮箱地址', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n欢迎注册 %s。\n\n请点击以下链接完成邮箱验证:\n\n%s\n\n验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
|
||||
$user->user_login, $site_name, $url
|
||||
);
|
||||
|
||||
return \AKSRT\Settings\Settings::send_event_email(
|
||||
'reg_verify_link', $email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $url]
|
||||
);
|
||||
}
|
||||
|
||||
public static function send_registration_verification_code_email(\WP_User $user, string $email, string $code): bool {
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('邮箱验证码', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n欢迎注册 %s。\n\n你的邮箱验证码为:\n\n【%s】\n\n验证码验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
|
||||
$user->user_login, $site_name, $code
|
||||
);
|
||||
|
||||
return \AKSRT\Settings\Settings::send_event_email(
|
||||
'reg_verify_code', $email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name, '{code}' => $code]
|
||||
);
|
||||
}
|
||||
|
||||
public static function send_registration_verified_notice_email(\WP_User $user, string $email): bool {
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('邮箱验证成功', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n你在 %s 的账号邮箱已经验证成功。\n\n如果这不是你的操作,请立即联系站点管理员。", 'aksrt'),
|
||||
$user->user_login, $site_name
|
||||
);
|
||||
|
||||
return \AKSRT\Settings\Settings::send_event_email(
|
||||
'reg_success', $email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name]
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-34
@@ -37,19 +37,14 @@ final class Router {
|
||||
|
||||
public function register_query_vars(array $vars): array {
|
||||
$vars[] = 'aksrt_sign';
|
||||
$vars[] = 'aksrt_auth_tab';
|
||||
$vars[] = 'aksrt_logout';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
private function auth_route_map(): array {
|
||||
return [
|
||||
'login' => 'index.php?aksrt_sign=1&aksrt_auth_tab=login',
|
||||
'register' => 'index.php?aksrt_sign=1&aksrt_auth_tab=register',
|
||||
'bind-account' => 'index.php?aksrt_sign=1&aksrt_auth_tab=bind',
|
||||
'sign' => 'index.php?aksrt_sign=1',
|
||||
'logout' => 'index.php?aksrt_logout=1',
|
||||
'reset-password' => 'index.php?aksrt_sign=1&aksrt_auth_tab=reset-password',
|
||||
'verify-email' => 'index.php?aksrt_sign=1&aksrt_auth_tab=verify-email',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -63,8 +58,7 @@ final class Router {
|
||||
if (get_query_var('aksrt_sign')) {
|
||||
// §4.1 Global sign gate: when close_sign is active, block non-logged-in access to sign page
|
||||
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign() && !is_user_logged_in()) {
|
||||
$tab = (string) get_query_var('aksrt_auth_tab');
|
||||
if (!in_array($tab, ['reset-password', 'verify-email', 'bind-account'], true) && empty($_REQUEST['interim-login'])) {
|
||||
if (empty($_REQUEST['interim-login'])) {
|
||||
if (!headers_sent()) {
|
||||
wp_safe_redirect(home_url('/'), 302);
|
||||
exit;
|
||||
@@ -72,10 +66,7 @@ final class Router {
|
||||
}
|
||||
}
|
||||
|
||||
$auth_template = function_exists('aksrt_get_auth_template_file')
|
||||
? aksrt_get_auth_template_file((string) get_query_var('aksrt_auth_tab'), true)
|
||||
: 'templates/pages/page-sign.php';
|
||||
$tpl = aksrt_locate_template_file($auth_template);
|
||||
$tpl = aksrt_locate_template_file('templates/pages/page-sign.php');
|
||||
if ($tpl) {
|
||||
return $tpl;
|
||||
}
|
||||
@@ -85,7 +76,7 @@ final class Router {
|
||||
}
|
||||
|
||||
public function maybe_flush_rewrite_rules(): void {
|
||||
$rewrite_version = AKSRT_VERSION . ':sign-router-v2-logout';
|
||||
$rewrite_version = AKSRT_VERSION . ':sign-router-v3-single-route';
|
||||
if (get_option('aksrt_auth_routes_flushed') !== $rewrite_version) {
|
||||
flush_rewrite_rules(false);
|
||||
update_option('aksrt_auth_routes_flushed', $rewrite_version);
|
||||
@@ -125,7 +116,7 @@ final class Router {
|
||||
}
|
||||
|
||||
public function redirect_native_login(): void {
|
||||
if (wp_doing_ajax()) {
|
||||
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::replace_wp_login()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,31 +130,26 @@ final class Router {
|
||||
|
||||
$action = isset($_REQUEST['action']) ? sanitize_key((string) $_REQUEST['action']) : 'login';
|
||||
$redirect_to = isset($_REQUEST['redirect_to']) ? wp_unslash((string) $_REQUEST['redirect_to']) : '';
|
||||
$interim = !empty($_REQUEST['interim-login']);
|
||||
$reauth = !empty($_REQUEST['reauth']);
|
||||
$is_admin_target = $redirect_to !== '' && strpos($redirect_to, admin_url()) === 0;
|
||||
|
||||
if (is_user_logged_in() && current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($is_admin_target || $interim || $reauth || $action === 'logout') {
|
||||
return;
|
||||
if ($action === 'logout') {
|
||||
wp_safe_redirect(aksrt_get_logout_url($redirect_to ?: home_url('/')), 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'register':
|
||||
$target = aksrt_get_register_url();
|
||||
$target = aksrt_get_sign_page_url('signup');
|
||||
break;
|
||||
case 'lostpassword':
|
||||
$target = aksrt_get_reset_password_url();
|
||||
$target = aksrt_get_sign_page_url('resetpassword');
|
||||
break;
|
||||
case 'rp':
|
||||
case 'resetpass':
|
||||
$target = add_query_arg('legacy', '1', aksrt_get_reset_password_url());
|
||||
$target = aksrt_get_sign_page_url('resetpassword');
|
||||
break;
|
||||
default:
|
||||
$target = aksrt_get_login_url();
|
||||
$target = aksrt_get_sign_page_url('signin');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -215,16 +201,9 @@ final class Router {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect logout via rewrite query var, GET flag, or pretty path.
|
||||
* Detect the unique public logout path.
|
||||
*/
|
||||
private function is_logout_request(): bool {
|
||||
if ((string) get_query_var('aksrt_logout') !== '' && (string) get_query_var('aksrt_logout') !== '0') {
|
||||
return true;
|
||||
}
|
||||
if (isset($_GET['aksrt_logout']) && (string) $_GET['aksrt_logout'] !== '' && (string) $_GET['aksrt_logout'] !== '0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
|
||||
if ($request_uri === '') {
|
||||
return false;
|
||||
|
||||
+18
-250
@@ -12,233 +12,7 @@ final class Service {
|
||||
use ServicePart1b;
|
||||
|
||||
|
||||
private const RESET_TOKEN_HASH_META = 'aksrt_password_reset_token_hash';
|
||||
private const RESET_TOKEN_EXPIRES_META = 'aksrt_password_reset_expires';
|
||||
private const RESET_TOKEN_REQUESTED_AT = 'aksrt_password_reset_requested_at';
|
||||
private const RESET_TOKEN_TTL = 3600;
|
||||
public const EMAIL_VERIFIED_META = 'aksrt_email_verified';
|
||||
/**
|
||||
* 用户当时被发送的验证方式('link' | 'code')。
|
||||
*
|
||||
* 注意:这是 *审计字段*,仅用于后台展示「上一次给该用户发的是哪种验证邮件」。
|
||||
* 路由分支(验证页应该显示链接提示还是验证码输入框、自助/管理员重发使用哪种方式)
|
||||
* 一律读取全局配置 self::current_registration_verification_method(),
|
||||
* 这样管理员把站点从 code 切换到 link 后,所有在途用户都立即跟随新方式。
|
||||
* 不要根据这个 meta 决定页面模式或重发方式。
|
||||
*/
|
||||
public const EMAIL_METHOD_META = 'aksrt_email_verification_method';
|
||||
public const EMAIL_TOKEN_HASH_META = 'aksrt_email_verification_token_hash';
|
||||
public const EMAIL_CODE_HASH_META = 'aksrt_email_verification_code_hash';
|
||||
public const EMAIL_EXPIRES_META = 'aksrt_email_verification_expires';
|
||||
public const EMAIL_REQUESTED_AT_META = 'aksrt_email_verification_requested_at';
|
||||
public const EMAIL_TARGET_META = 'aksrt_email_verification_email';
|
||||
public const EMAIL_OBK_META = 'aksrt_email_verification_obk';
|
||||
private const EMAIL_ACCESS_KEY_META = 'aksrt_email_verification_access_key';
|
||||
public const EMAIL_CODE_ATTEMPTS_META = 'aksrt_email_verification_code_attempts';
|
||||
public const EMAIL_CODE_MAX_ATTEMPTS = 5;
|
||||
public const EMAIL_CODE_IP_TTL = 900;
|
||||
// 自助重发的 IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次。
|
||||
private const RESEND_IP_MAX = 6;
|
||||
private const RESEND_IP_WINDOW = 600;
|
||||
|
||||
/**
|
||||
* 启动钩子:注册全局 authenticate 拦截器(阶段 4)
|
||||
* 调用方:bootstrap.php 在所有类加载完成后调用一次。
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_filter('authenticate', [self::class, 'enforce_email_verification_on_authenticate'], 30, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局登录拦截器:堵住 wp-login.php / REST / XML-RPC / Application Password 等所有
|
||||
* 不经过 Service::login_user() 的认证路径。
|
||||
*
|
||||
* 多重短路保护,按"成本最低"顺序排列:
|
||||
* 1. 紧急降级常量 AKSRT_NO_EMAIL_GATE
|
||||
* 2. $user 不是 \WP_User(认证失败 / 未开始)
|
||||
* 3. \WP_Error(前置错误,原样返回)
|
||||
* 4. 功能未启用
|
||||
* 5. 管理员豁免(manage_options)
|
||||
* 6. cron / 安装 / 升级豁免
|
||||
* 7. REST/XML-RPC 阶段化(option aksrt_email_verification_rest_block_at 控制)
|
||||
* 8. 自定义豁免过滤器
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return mixed
|
||||
*/
|
||||
public static function enforce_email_verification_on_authenticate($user, $username = '', $password = '') {
|
||||
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
|
||||
return $user;
|
||||
}
|
||||
if (!($user instanceof \WP_User)) {
|
||||
return $user;
|
||||
}
|
||||
if (is_wp_error($user)) {
|
||||
return $user;
|
||||
}
|
||||
if (!class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::registration_requires_email_verification()) {
|
||||
return $user;
|
||||
}
|
||||
if (user_can($user, 'manage_options')) {
|
||||
return $user;
|
||||
}
|
||||
if (defined('DOING_CRON') && DOING_CRON) {
|
||||
return $user;
|
||||
}
|
||||
if (defined('WP_INSTALLING') && WP_INSTALLING) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$is_rest = (defined('REST_REQUEST') && REST_REQUEST);
|
||||
$is_xmlrpc = (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST);
|
||||
$rest_block_at = (int) get_option('aksrt_email_verification_rest_block_at', 0);
|
||||
|
||||
// 阶段化:option=0 时 REST/XML-RPC 仅记录不拦截(Day 0~3 灰度期)
|
||||
if (($is_rest || $is_xmlrpc) && $rest_block_at <= 0) {
|
||||
if (!EmailVerification::is_email_verified((int) $user->ID)) {
|
||||
self::log_event('auth.rest_unverified', [
|
||||
'uid' => (int) $user->ID,
|
||||
'rest' => $is_rest,
|
||||
'xmlrpc' => $is_xmlrpc,
|
||||
]);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
// 已验证 → 直接放行
|
||||
if (EmailVerification::is_email_verified((int) $user->ID)) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// 自定义豁免:filter 返回 false 即放行
|
||||
$should_block = apply_filters('aksrt_email_verification_should_block', true, $user, [
|
||||
'rest' => $is_rest,
|
||||
'xmlrpc' => $is_xmlrpc,
|
||||
]);
|
||||
if (!$should_block) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
self::log_event('auth.blocked', [
|
||||
'uid' => (int) $user->ID,
|
||||
'rest' => $is_rest,
|
||||
'xmlrpc' => $is_xmlrpc,
|
||||
]);
|
||||
|
||||
return new \WP_Error(
|
||||
'aksrt_email_unverified',
|
||||
__('账号尚未完成邮箱验证,请先完成邮箱验证后再登录。', 'aksrt'),
|
||||
[
|
||||
'verify_url' => self::get_email_verification_url((int) $user->ID),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量事件日志(ring buffer,最近 200 条),用于健康检查路由统计。
|
||||
* 不写入文件,仅写 transient(24h TTL),不影响性能。
|
||||
*/
|
||||
public static function log_event(string $code, array $context = []): void {
|
||||
$key = 'aksrt_email_verify_log';
|
||||
$logs = get_transient($key);
|
||||
if (!is_array($logs)) {
|
||||
$logs = [];
|
||||
}
|
||||
$logs[] = [
|
||||
't' => time(),
|
||||
'c' => $code,
|
||||
'd' => $context,
|
||||
];
|
||||
// 保留最近 200 条
|
||||
if (count($logs) > 200) {
|
||||
$logs = array_slice($logs, -200);
|
||||
}
|
||||
set_transient($key, $logs, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取最近 24h 的事件日志聚合。
|
||||
*/
|
||||
public static function get_email_verification_url(int $user_id, array $args = []): string {
|
||||
if ($user_id <= 0 || !function_exists('aksrt_get_verify_email_url')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$access_key = self::get_or_issue_email_access_key($user_id);
|
||||
if ($access_key !== '') {
|
||||
$args['vk'] = $access_key;
|
||||
}
|
||||
|
||||
return aksrt_get_verify_email_url($user_id, $args);
|
||||
}
|
||||
|
||||
public static function current_registration_verification_method(): string {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return 'link';
|
||||
}
|
||||
|
||||
$method = \AKSRT\Settings\Settings::registration_verification_method();
|
||||
return in_array($method, ['link', 'code'], true) ? $method : 'link';
|
||||
}
|
||||
|
||||
private static function get_or_issue_email_access_key(int $user_id, bool $rotate = false): string {
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$current = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true);
|
||||
if (!$rotate && $current !== '') {
|
||||
return $current;
|
||||
}
|
||||
|
||||
$key = wp_generate_password(48, false, false);
|
||||
update_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, $key);
|
||||
return $key;
|
||||
}
|
||||
|
||||
private static function has_valid_email_access_key(int $user_id, string $access_key): bool {
|
||||
if ($user_id <= 0 || $access_key === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stored = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true);
|
||||
return $stored !== '' && hash_equals($stored, $access_key);
|
||||
}
|
||||
|
||||
private static function masked_email(string $email): string {
|
||||
if ($email === '' || !is_email($email) || strpos($email, '@') === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$local_visible = function_exists('mb_substr') ? mb_substr($local, 0, 2, 'UTF-8') : substr($local, 0, 2);
|
||||
$local_masked = $local_visible . str_repeat('*', max(1, strlen($local) - strlen($local_visible)));
|
||||
|
||||
$domain_parts = explode('.', $domain);
|
||||
$domain_head = $domain_parts[0] ?? '';
|
||||
$domain_visible = substr($domain_head, 0, 1);
|
||||
$domain_parts[0] = $domain_visible . str_repeat('*', max(1, strlen($domain_head) - strlen($domain_visible)));
|
||||
|
||||
return $local_masked . '@' . implode('.', $domain_parts);
|
||||
}
|
||||
|
||||
public static function get_recent_log_summary(): array {
|
||||
$logs = get_transient('aksrt_email_verify_log');
|
||||
if (!is_array($logs)) {
|
||||
return [];
|
||||
}
|
||||
$cutoff = time() - DAY_IN_SECONDS;
|
||||
$summary = [];
|
||||
foreach ($logs as $entry) {
|
||||
if (!is_array($entry) || !isset($entry['t'], $entry['c'])) continue;
|
||||
if ((int) $entry['t'] < $cutoff) continue;
|
||||
$code = (string) $entry['c'];
|
||||
$summary[$code] = ($summary[$code] ?? 0) + 1;
|
||||
}
|
||||
return $summary;
|
||||
}
|
||||
|
||||
public static function get_oauth_bind_context(string $obk): ?array {
|
||||
$obk = sanitize_text_field($obk);
|
||||
@@ -264,6 +38,20 @@ final class Service {
|
||||
|
||||
public static function resolve_email_to_username(string $login): string {
|
||||
$login = trim($login);
|
||||
if (
|
||||
preg_match('/^1[3-9]\d{9}$/', $login)
|
||||
&& class_exists('\AKSRT\Settings\Settings')
|
||||
&& \AKSRT\Settings\Settings::signin_phone_enabled()
|
||||
) {
|
||||
$users = get_users([
|
||||
'meta_key' => 'aksrt_phone',
|
||||
'meta_value' => $login,
|
||||
'number' => 1,
|
||||
]);
|
||||
if (!empty($users[0]) && $users[0] instanceof \WP_User) {
|
||||
return $users[0]->user_login;
|
||||
}
|
||||
}
|
||||
if (is_email($login)) {
|
||||
$user = get_user_by('email', $login);
|
||||
if ($user && !is_wp_error($user)) {
|
||||
@@ -319,23 +107,11 @@ final class Service {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function login_user(array $post, bool $is_ajax = false, bool $skip_nonce = false): array {
|
||||
public static function login_user(array $post, string $captcha_scene = 'login'): array {
|
||||
$resolved_login = self::resolve_email_to_username((string) ($post['log'] ?? ''));
|
||||
|
||||
if (!$skip_nonce) {
|
||||
if ($is_ajax) {
|
||||
if (!wp_verify_nonce($post['_ajax_nonce'] ?? '', 'aksrt_ajax_login')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
} else {
|
||||
if (!wp_verify_nonce($post['_login_nonce'] ?? '', 'aksrt_login')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('login', $post);
|
||||
$captcha_result = aksrt_require_captcha($captcha_scene, $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
@@ -362,20 +138,12 @@ final class Service {
|
||||
// REST API 上下文中 wp_signon 可能无法正确设置 Cookie
|
||||
// 手动调用 wp_set_auth_cookie 确保 Cookie 被设置
|
||||
if (defined('REST_REQUEST') && REST_REQUEST) {
|
||||
wp_set_auth_cookie($user->ID, true);
|
||||
wp_set_auth_cookie($user->ID, !empty($post['rememberme']));
|
||||
wp_set_current_user($user->ID);
|
||||
}
|
||||
|
||||
if (self::user_requires_email_verification($user)) {
|
||||
wp_logout();
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => __('账号尚未完成邮箱验证,请先验证邮箱后再登录', 'aksrt'),
|
||||
'verify_url' => self::get_email_verification_url((int) $user->ID),
|
||||
];
|
||||
}
|
||||
|
||||
$redirect = !empty($post['redirect_to']) ? wp_unslash((string) $post['redirect_to']) : home_url('/');
|
||||
$redirect = wp_validate_redirect($redirect, home_url('/'));
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
|
||||
+63
-343
@@ -10,17 +10,24 @@ defined('ABSPATH') || exit;
|
||||
trait ServicePart1a {
|
||||
|
||||
|
||||
public static function register_user(array $post, array $context = []): array {
|
||||
public static function register_user(array $post): array {
|
||||
$user_login = sanitize_user(wp_unslash($post['user_login'] ?? ''), true);
|
||||
$user_email = sanitize_email(wp_unslash($post['user_email'] ?? ''));
|
||||
$password = (string) ($post['user_pass'] ?? '');
|
||||
$confirm = (string) ($post['user_pass_confirm'] ?? '');
|
||||
|
||||
if (!wp_verify_nonce($post['_register_nonce'] ?? '', 'aksrt_register')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
if (
|
||||
class_exists('\AKSRT\Settings\Settings')
|
||||
&& \AKSRT\Settings\Settings::signup_verification_enabled()
|
||||
&& empty($post['_auth_code_verified'])
|
||||
) {
|
||||
return ['success' => false, 'error' => __('请先完成邮箱或手机验证', 'aksrt')];
|
||||
}
|
||||
|
||||
if (empty($post['agree_terms'])) {
|
||||
$agreement = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::user_agreement_settings()
|
||||
: ['enabled' => false, 'privacy' => false, 'required' => false];
|
||||
if ((!empty($agreement['enabled']) || !empty($agreement['privacy'])) && !empty($agreement['required']) && empty($post['agree_terms'])) {
|
||||
return ['success' => false, 'error' => __('请先同意服务条款', 'aksrt')];
|
||||
}
|
||||
|
||||
@@ -31,6 +38,13 @@ trait ServicePart1a {
|
||||
if (mb_strlen($user_login) < 3 || mb_strlen($user_login) > 60) {
|
||||
return ['success' => false, 'error' => __('用户名长度需在 3-60 个字符之间', 'aksrt')];
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
foreach (\AKSRT\Settings\Settings::nickname_blacklist() as $keyword) {
|
||||
if ($keyword !== '' && mb_stripos($user_login, $keyword) !== false) {
|
||||
return ['success' => false, 'error' => __('用户名包含禁止使用的关键词', 'aksrt')];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (username_exists($user_login)) {
|
||||
return ['success' => false, 'error' => __('该用户名已被使用', 'aksrt')];
|
||||
@@ -39,17 +53,37 @@ trait ServicePart1a {
|
||||
if (empty($user_email) || !is_email($user_email)) {
|
||||
return ['success' => false, 'error' => __('请输入有效的邮箱地址', 'aksrt')];
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$email_limit = \AKSRT\Settings\Settings::email_limit();
|
||||
$matches = false;
|
||||
foreach ((array) ($email_limit['domains'] ?? []) as $keyword) {
|
||||
if ($keyword !== '' && stripos($user_email, (string) $keyword) !== false) {
|
||||
$matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((($email_limit['type'] ?? 'not') === 'not' && $matches) || (($email_limit['type'] ?? 'not') === 'allow' && !empty($email_limit['domains']) && !$matches)) {
|
||||
return ['success' => false, 'error' => __('该邮箱不符合本站注册规则', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
if (email_exists($user_email)) {
|
||||
return ['success' => false, 'error' => __('该邮箱已被其他账号使用', 'aksrt')];
|
||||
}
|
||||
|
||||
if (
|
||||
class_exists('\AKSRT\Settings\Settings')
|
||||
&& \AKSRT\Settings\Settings::signup_verification_enabled()
|
||||
&& \AKSRT\Settings\Settings::signup_omit_password_confirmation()
|
||||
) {
|
||||
$confirm = $password;
|
||||
}
|
||||
$password_check = self::validate_password($password, $confirm);
|
||||
if ($password_check !== true) {
|
||||
return ['success' => false, 'error' => $password_check];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
if (empty($post['_auth_code_verified']) && function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('register', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
@@ -88,6 +122,12 @@ trait ServicePart1a {
|
||||
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
if ($submitted_ref !== '' && empty($ref_context['valid']) && !empty($registration['reject_invalid'])) {
|
||||
if ($ref_service) {
|
||||
$ref_service->clear_registration_context();
|
||||
}
|
||||
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
|
||||
}
|
||||
|
||||
$user_id = wp_create_user($user_login, $password, $user_email);
|
||||
if (is_wp_error($user_id)) {
|
||||
@@ -103,115 +143,20 @@ trait ServicePart1a {
|
||||
return ['success' => false, 'error' => __('注册成功,但读取用户信息失败,请联系管理员', 'aksrt')];
|
||||
}
|
||||
|
||||
$oauth_context = isset($context['oauth_bind_data']) && is_array($context['oauth_bind_data']) ? $context['oauth_bind_data'] : [];
|
||||
$oauth_verified = self::oauth_email_is_verified($oauth_context);
|
||||
$requires_verify = \AKSRT\Settings\Settings::registration_requires_email_verification() && !$oauth_verified;
|
||||
|
||||
if (!$requires_verify) {
|
||||
self::mark_email_verified($user_id);
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => (int) $user_id,
|
||||
'user' => $user,
|
||||
'user_login' => $user_login,
|
||||
'notice' => $ref_notice,
|
||||
];
|
||||
}
|
||||
|
||||
$method = \AKSRT\Settings\Settings::registration_verification_method();
|
||||
$begin = self::begin_email_verification(
|
||||
$user,
|
||||
$method,
|
||||
[
|
||||
'obk' => sanitize_text_field((string) ($context['obk'] ?? '')),
|
||||
]
|
||||
);
|
||||
|
||||
if (empty($begin['success'])) {
|
||||
self::delete_user_account($user_id);
|
||||
return $begin;
|
||||
}
|
||||
|
||||
self::mark_email_verified($user_id);
|
||||
wp_set_auth_cookie((int) $user_id, true);
|
||||
do_action('wp_login', $user->user_login, $user);
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => (int) $user_id,
|
||||
'user' => $user,
|
||||
'user_login' => $user_login,
|
||||
'verification_required' => true,
|
||||
'verification_method' => $method,
|
||||
'verify_url' => self::get_email_verification_url((int) $user_id, ['mode' => $method]),
|
||||
'message' => $begin['message'] ?? '',
|
||||
'notice' => $ref_notice,
|
||||
'success' => true,
|
||||
'user_id' => (int) $user_id,
|
||||
'user' => $user,
|
||||
'user_login' => $user_login,
|
||||
'redirect' => home_url('/'),
|
||||
'notice' => $ref_notice,
|
||||
];
|
||||
}
|
||||
|
||||
public static function bind_oauth_account(array $post, array $oauth_data): array {
|
||||
if (!wp_verify_nonce($post['_bind_nonce'] ?? '', 'aksrt_bind')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('bind', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
|
||||
$creds = [
|
||||
'user_login' => self::resolve_email_to_username((string) ($post['log'] ?? '')),
|
||||
'user_password' => (string) ($post['pwd'] ?? ''),
|
||||
'remember' => true,
|
||||
];
|
||||
|
||||
$user = wp_signon($creds);
|
||||
if (is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => self::friendly_login_error($user->get_error_message())];
|
||||
}
|
||||
|
||||
if (self::user_requires_email_verification($user)) {
|
||||
wp_logout();
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => __('该账号尚未完成邮箱验证,请先完成验证后再绑定第三方登录。', 'aksrt'),
|
||||
'verify_url' => self::get_email_verification_url((int) $user->ID),
|
||||
];
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
|
||||
if ($provider === '') {
|
||||
return ['success' => false, 'error' => __('绑定数据无效,请重新使用第三方登录', 'aksrt')];
|
||||
}
|
||||
|
||||
$uid = (int) $user->ID;
|
||||
update_user_meta($uid, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
|
||||
|
||||
$bindings = get_user_meta($uid, 'aksrt_oauth_bindings', true) ?: [];
|
||||
if (!is_array($bindings)) {
|
||||
$bindings = [];
|
||||
}
|
||||
if (!in_array($provider, $bindings, true)) {
|
||||
$bindings[] = $provider;
|
||||
}
|
||||
update_user_meta($uid, 'aksrt_oauth_bindings', $bindings);
|
||||
|
||||
if (!empty($oauth_data['user_data']['avatar_url'])) {
|
||||
update_user_meta($uid, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
|
||||
}
|
||||
|
||||
if (!empty($post['obk'])) {
|
||||
delete_transient(sanitize_text_field((string) $post['obk']));
|
||||
}
|
||||
|
||||
wp_set_auth_cookie($uid, true);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user' => $user,
|
||||
'provider' => $provider,
|
||||
];
|
||||
}
|
||||
|
||||
public static function finalize_oauth_registration(int $user_id, array $oauth_data, string $obk = ''): void {
|
||||
public static function finalize_oauth_binding(int $user_id, array $oauth_data, string $obk = ''): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -222,16 +167,17 @@ trait ServicePart1a {
|
||||
}
|
||||
|
||||
update_user_meta($user_id, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
|
||||
update_user_meta($user_id, 'aksrt_oauth_bindings', [$provider]);
|
||||
$bindings = get_user_meta($user_id, 'aksrt_oauth_bindings', true);
|
||||
$bindings = is_array($bindings) ? $bindings : [];
|
||||
if (!in_array($provider, $bindings, true)) {
|
||||
$bindings[] = $provider;
|
||||
}
|
||||
update_user_meta($user_id, 'aksrt_oauth_bindings', array_values($bindings));
|
||||
|
||||
if (!empty($oauth_data['user_data']['avatar_url'])) {
|
||||
update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
|
||||
}
|
||||
|
||||
if (self::oauth_email_is_verified($oauth_data)) {
|
||||
EmailVerification::mark_email_verified($user_id);
|
||||
}
|
||||
|
||||
if ($obk !== '') {
|
||||
delete_transient(sanitize_text_field($obk));
|
||||
}
|
||||
@@ -239,235 +185,9 @@ trait ServicePart1a {
|
||||
wp_set_auth_cookie($user_id, true);
|
||||
}
|
||||
|
||||
public static function get_email_verification_context(array $query): array {
|
||||
$invalid_payload = [
|
||||
'mode' => 'invalid',
|
||||
'user' => null,
|
||||
'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt'),
|
||||
'method' => '',
|
||||
'email' => '',
|
||||
'uid' => 0,
|
||||
'verified' => false,
|
||||
'access_key' => '',
|
||||
];
|
||||
|
||||
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
|
||||
if ($user_id <= 0) {
|
||||
return $invalid_payload;
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return $invalid_payload;
|
||||
}
|
||||
|
||||
$method = self::current_registration_verification_method();
|
||||
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
|
||||
$token = sanitize_text_field((string) ($query['token'] ?? ''));
|
||||
$access = sanitize_text_field((string) ($query['vk'] ?? ''));
|
||||
|
||||
// 容错:meta 缺失时(例如历史账号被强制要求验证),回退到设置默认值与 user_email
|
||||
if (!in_array($method, ['link', 'code'], true)) {
|
||||
$method = self::current_registration_verification_method();
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
$email = (string) $user->user_email;
|
||||
}
|
||||
$masked_email = self::masked_email($email);
|
||||
$has_access = self::has_valid_email_access_key($user_id, $access);
|
||||
|
||||
// token 路径:链接模式直接走 token 校验;不依赖 vk
|
||||
if ($token !== '') {
|
||||
if (EmailVerification::is_email_verified($user_id)) {
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => $user,
|
||||
'error' => __('该验证链接已失效或已使用,请重新发起验证流程', 'aksrt'),
|
||||
'method' => $has_access ? $method : '',
|
||||
'email' => $has_access ? $masked_email : '',
|
||||
'uid' => $has_access ? $user_id : 0,
|
||||
'verified' => true,
|
||||
'access_key' => $has_access ? $access : '',
|
||||
];
|
||||
}
|
||||
|
||||
$result = self::verify_email_token($user_id, $token);
|
||||
if (!empty($result['success'])) {
|
||||
return [
|
||||
'mode' => 'completed',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'method' => $method,
|
||||
'email' => $masked_email,
|
||||
'uid' => $user_id,
|
||||
'verified' => true,
|
||||
'redirect' => $result['redirect'] ?? home_url('/'),
|
||||
'message' => $result['message'] ?? '',
|
||||
'access_key' => $has_access ? $access : '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => $user,
|
||||
'error' => $result['error'] ?? __('验证链接无效或已过期', 'aksrt'),
|
||||
'method' => $has_access ? $method : '',
|
||||
'email' => $has_access ? $masked_email : '',
|
||||
'uid' => $has_access ? $user_id : 0,
|
||||
'verified' => false,
|
||||
'access_key' => $has_access ? $access : '',
|
||||
];
|
||||
}
|
||||
|
||||
// 没有 token 也没有 vk:返回完全脱敏的通用错误,避免根据 uid 枚举账号状态
|
||||
if (!$has_access) {
|
||||
return $invalid_payload;
|
||||
}
|
||||
|
||||
if (!self::user_requires_email_verification($user)) {
|
||||
return [
|
||||
'mode' => 'verified',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'method' => '',
|
||||
'email' => self::masked_email((string) $user->user_email),
|
||||
'uid' => $user_id,
|
||||
'verified' => true,
|
||||
'access_key' => $access,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => $method === 'code' ? 'code' : 'pending',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'method' => $method,
|
||||
'email' => $masked_email ?: self::masked_email((string) $user->user_email),
|
||||
'uid' => $user_id,
|
||||
'verified' => false,
|
||||
'access_key' => $access,
|
||||
];
|
||||
}
|
||||
|
||||
public static function verify_email_code_submission(array $post): array {
|
||||
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
|
||||
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($post['email_code'] ?? ''));
|
||||
if ($user_id <= 0 || $code === '') {
|
||||
return ['success' => false, 'error' => __('请输入完整的邮箱验证码', 'aksrt')];
|
||||
}
|
||||
|
||||
if (!self::has_valid_email_access_key($user_id, $access)) {
|
||||
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('verify_email', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
|
||||
return self::verify_email_code($user_id, $code);
|
||||
}
|
||||
|
||||
public static function resend_email_verification(array $post): array {
|
||||
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
|
||||
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
|
||||
|
||||
// 必须先持有有效 vk 才允许重发,避免未登录用户用 uid 枚举触发邮件
|
||||
if ($user_id <= 0 || !self::has_valid_email_access_key($user_id, $access)) {
|
||||
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
|
||||
}
|
||||
|
||||
// IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次(跨账号合并计数)
|
||||
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
|
||||
if ($ip !== '') {
|
||||
$ip_key = 'aksrt_email_resend_ip_' . md5($ip);
|
||||
$ip_attempts = (int) get_transient($ip_key);
|
||||
if ($ip_attempts >= self::RESEND_IP_MAX) {
|
||||
self::log_event('verify.resend.ip_throttled', ['uid' => $user_id, 'ip' => $ip]);
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
set_transient($ip_key, $ip_attempts + 1, self::RESEND_IP_WINDOW);
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
|
||||
}
|
||||
|
||||
if (!self::user_requires_email_verification($user)) {
|
||||
return ['success' => false, 'error' => __('该账号已完成邮箱验证,无需重复发送', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('verify_email', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
|
||||
$method = self::current_registration_verification_method();
|
||||
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
|
||||
|
||||
// 容错:meta 缺失时回退到设置默认值与 user_email(兼容历史账号 / meta 被外部清理的情况)
|
||||
if (!in_array($method, ['link', 'code'], true)) {
|
||||
$method = \AKSRT\Settings\Settings::registration_verification_method();
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
$email = (string) $user->user_email;
|
||||
}
|
||||
if ($email === '' || !is_email($email)) {
|
||||
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,请联系管理员', 'aksrt')];
|
||||
}
|
||||
|
||||
$requested_at = (int) get_user_meta($user_id, self::EMAIL_REQUESTED_AT_META, true);
|
||||
if ($requested_at > 0 && (time() - $requested_at) < 60) {
|
||||
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
|
||||
}
|
||||
|
||||
return self::begin_email_verification($user, $method, [
|
||||
'email' => $email,
|
||||
'obk' => (string) get_user_meta($user_id, self::EMAIL_OBK_META, true),
|
||||
'force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function user_requires_email_verification($user): bool {
|
||||
return EmailVerification::user_requires_email_verification($user);
|
||||
}
|
||||
|
||||
private static function mark_email_verified(int $user_id): void {
|
||||
EmailVerification::mark_email_verified($user_id);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 公开 API:管理员后台 / 内部业务调用(委托 EmailVerification)
|
||||
// ============================================================
|
||||
|
||||
public static function user_is_email_verified(int $user_id): bool {
|
||||
return EmailVerification::user_is_email_verified($user_id);
|
||||
}
|
||||
|
||||
public static function get_user_verification_state(int $user_id): array {
|
||||
return EmailVerification::get_user_verification_state($user_id);
|
||||
}
|
||||
|
||||
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
return EmailVerification::admin_mark_verified($user_id, $by_admin_id);
|
||||
}
|
||||
|
||||
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
|
||||
return EmailVerification::admin_unmark_verified($user_id, $by_admin_id);
|
||||
update_user_meta($user_id, self::EMAIL_VERIFIED_META, '1');
|
||||
do_action('aksrt_email_verified', $user_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+36
-351
@@ -9,227 +9,6 @@ defined('ABSPATH') || exit;
|
||||
|
||||
trait ServicePart1b {
|
||||
|
||||
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
|
||||
return EmailVerification::admin_send_verification($user_id, $by_admin_id);
|
||||
}
|
||||
|
||||
public static function backfill_legacy_users_as_verified(bool $force = false): int {
|
||||
return EmailVerification::backfill_legacy_users_as_verified($force);
|
||||
}
|
||||
|
||||
public static function run_async_backfill_batch(int $offset = 0): void {
|
||||
EmailVerification::run_async_backfill_batch($offset);
|
||||
}
|
||||
|
||||
private static function delete_user_account(int $user_id): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
if (!function_exists('wp_delete_user')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/user.php';
|
||||
}
|
||||
if (function_exists('wp_delete_user')) {
|
||||
wp_delete_user($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
private static function oauth_email_is_verified(array $oauth_data): bool {
|
||||
$user_data = isset($oauth_data['user_data']) && is_array($oauth_data['user_data']) ? $oauth_data['user_data'] : $oauth_data;
|
||||
if (empty($user_data['email'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($user_data['email_verified'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$provider = sanitize_key((string) ($user_data['provider'] ?? $oauth_data['provider'] ?? ''));
|
||||
if ($provider === 'google' && !empty($user_data['email'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($provider === 'github' && isset($user_data['email_verified'])) {
|
||||
return !empty($user_data['email_verified']);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function begin_email_verification(\WP_User $user, string $method, array $args = []): array {
|
||||
return EmailVerification::begin_email_verification($user, $method, $args);
|
||||
}
|
||||
|
||||
private static function verify_email_token(int $user_id, string $token): array {
|
||||
return EmailVerification::verify_email_token($user_id, $token);
|
||||
}
|
||||
|
||||
private static function verify_email_code(int $user_id, string $code): array {
|
||||
return EmailVerification::verify_email_code($user_id, $code);
|
||||
}
|
||||
|
||||
public static function get_reset_context(array $query): array {
|
||||
if (!empty($query['key']) || !empty($query['login']) || !empty($query['legacy'])) {
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => null,
|
||||
'error' => __('旧的重置密码链接已失效,请重新申请重置密码。', 'aksrt'),
|
||||
'token' => '',
|
||||
'user_id'=> 0,
|
||||
];
|
||||
}
|
||||
|
||||
$token = sanitize_text_field($query['token'] ?? '');
|
||||
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
|
||||
|
||||
if ($token === '' || $user_id <= 0) {
|
||||
return [
|
||||
'mode' => 'request',
|
||||
'user' => null,
|
||||
'error' => '',
|
||||
'token' => '',
|
||||
'user_id'=> 0,
|
||||
];
|
||||
}
|
||||
|
||||
$user = self::validate_reset_token($user_id, $token);
|
||||
if (is_wp_error($user)) {
|
||||
return [
|
||||
'mode' => 'invalid',
|
||||
'user' => null,
|
||||
'error' => $user->get_error_message(),
|
||||
'token' => $token,
|
||||
'user_id'=> $user_id,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => 'reset',
|
||||
'user' => $user,
|
||||
'error' => '',
|
||||
'token' => $token,
|
||||
'user_id'=> $user_id,
|
||||
];
|
||||
}
|
||||
|
||||
public static function request_password_reset(array $post): array {
|
||||
if (!wp_verify_nonce($post['_reset_nonce'] ?? '', 'aksrt_reset_password')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('lostpassword', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
|
||||
$raw_login = trim((string) ($post['user_login'] ?? ''));
|
||||
if ($raw_login === '') {
|
||||
return ['success' => false, 'error' => __('请输入邮箱地址或用户名', 'aksrt')];
|
||||
}
|
||||
|
||||
$user = null;
|
||||
if (is_email($raw_login)) {
|
||||
$user = get_user_by('email', $raw_login);
|
||||
} else {
|
||||
$user = get_user_by('login', sanitize_user($raw_login, true));
|
||||
}
|
||||
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
|
||||
}
|
||||
|
||||
$token = self::create_reset_token($user->ID);
|
||||
$sent = self::send_password_reset_email($user, $token);
|
||||
|
||||
if (!$sent) {
|
||||
return ['success' => false, 'error' => __('重置邮件发送失败,请稍后再试', 'aksrt')];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
|
||||
}
|
||||
|
||||
public static function reset_password_with_token(array $post): array {
|
||||
if (!wp_verify_nonce($post['_setpw_nonce'] ?? '', 'aksrt_set_password')) {
|
||||
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
|
||||
}
|
||||
|
||||
$token = sanitize_text_field($post['reset_token'] ?? '');
|
||||
$user_id = isset($post['reset_uid']) ? absint($post['reset_uid']) : 0;
|
||||
$pass1 = (string) ($post['pass1'] ?? '');
|
||||
$pass2 = (string) ($post['pass2'] ?? '');
|
||||
|
||||
$user = self::validate_reset_token($user_id, $token);
|
||||
if (is_wp_error($user)) {
|
||||
return ['success' => false, 'error' => $user->get_error_message(), 'mode' => 'invalid'];
|
||||
}
|
||||
|
||||
$password_check = self::validate_password($pass1, $pass2);
|
||||
if ($password_check !== true) {
|
||||
return ['success' => false, 'error' => $password_check, 'mode' => 'reset'];
|
||||
}
|
||||
|
||||
wp_set_password($pass1, $user_id);
|
||||
self::clear_reset_token($user_id);
|
||||
|
||||
return ['success' => true, 'message' => __('密码已设置成功!', 'aksrt')];
|
||||
}
|
||||
|
||||
private static function create_reset_token(int $user_id): string {
|
||||
$token = wp_generate_password(48, false, false);
|
||||
update_user_meta($user_id, self::RESET_TOKEN_HASH_META, hash('sha256', $token));
|
||||
update_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, time() + self::RESET_TOKEN_TTL);
|
||||
update_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT, time());
|
||||
return $token;
|
||||
}
|
||||
|
||||
private static function validate_reset_token(int $user_id, string $token) {
|
||||
if ($user_id <= 0 || $token === '') {
|
||||
return new \WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user || is_wp_error($user)) {
|
||||
return new \WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
|
||||
}
|
||||
|
||||
$stored_hash = (string) get_user_meta($user_id, self::RESET_TOKEN_HASH_META, true);
|
||||
$expires_at = (int) get_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, true);
|
||||
$token_hash = hash('sha256', $token);
|
||||
|
||||
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, $token_hash)) {
|
||||
return new \WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private static function clear_reset_token(int $user_id): void {
|
||||
delete_user_meta($user_id, self::RESET_TOKEN_HASH_META);
|
||||
delete_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META);
|
||||
delete_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT);
|
||||
}
|
||||
|
||||
private static function send_password_reset_email(\WP_User $user, string $token): bool {
|
||||
$reset_url = aksrt_get_reset_password_url($token, $user->ID);
|
||||
$site_name = get_bloginfo('name');
|
||||
$default_subject = '[' . $site_name . '] ' . __('重置密码', 'aksrt');
|
||||
$default_content = sprintf(
|
||||
__("您好 %s,\n\n有人请求重置您在 %s 的密码。\n\n请点击以下链接设置新密码:\n\n%s\n\n如果这不是您本人的操作,请忽略此邮件,您的密码不会被更改。", 'aksrt'),
|
||||
$user->user_login, $site_name, $reset_url
|
||||
);
|
||||
|
||||
return \AKSRT\Settings\Settings::send_event_email(
|
||||
'password_reset', $user->user_email, $default_subject, $default_content,
|
||||
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $reset_url]
|
||||
);
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
* 手机号认证(注册 / 登录 / 绑定)
|
||||
* 对齐 legacy 的 signup_captcha / signin_captcha / bind_phone_captcha
|
||||
* ================================================================= */
|
||||
|
||||
/**
|
||||
* 手机号注册
|
||||
*
|
||||
@@ -238,34 +17,39 @@ trait ServicePart1b {
|
||||
*/
|
||||
public static function register_by_phone(array $post): array {
|
||||
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($post['code'] ?? ''));
|
||||
$pass = (string) ($post['password'] ?? '');
|
||||
$confirm = (string) ($post['password_confirm'] ?? '');
|
||||
$username = sanitize_user(wp_unslash((string) ($post['user_login'] ?? '')), true);
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
|
||||
}
|
||||
if (empty($code)) {
|
||||
return ['success' => false, 'error' => __('请输入验证码', 'aksrt')];
|
||||
if (empty($post['_auth_code_verified'])) {
|
||||
return ['success' => false, 'error' => __('请先完成手机验证', 'aksrt')];
|
||||
}
|
||||
if (strlen($pass) < 6) {
|
||||
return ['success' => false, 'error' => __('密码至少 6 位', 'aksrt')];
|
||||
if ($username === '' || !validate_username($username) || mb_strlen($username) < 3 || mb_strlen($username) > 60) {
|
||||
return ['success' => false, 'error' => __('请输入 3-60 个字符的有效用户名', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('register', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
if (username_exists($username)) {
|
||||
return ['success' => false, 'error' => __('该用户名已被使用', 'aksrt')];
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
foreach (\AKSRT\Settings\Settings::nickname_blacklist() as $keyword) {
|
||||
if ($keyword !== '' && mb_stripos($username, $keyword) !== false) {
|
||||
return ['success' => false, 'error' => __('用户名包含禁止使用的关键词', 'aksrt')];
|
||||
}
|
||||
}
|
||||
if (\AKSRT\Settings\Settings::signup_omit_password_confirmation()) {
|
||||
$confirm = $pass;
|
||||
}
|
||||
$agreement = \AKSRT\Settings\Settings::user_agreement_settings();
|
||||
if ((!empty($agreement['enabled']) || !empty($agreement['privacy'])) && !empty($agreement['required']) && empty($post['agree_terms'])) {
|
||||
return ['success' => false, 'error' => __('请先同意服务条款', 'aksrt')];
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 SMS 验证码
|
||||
if (class_exists('\AKSRT\Sms\SMS')) {
|
||||
$verify_result = \AKSRT\Sms\SMS::get_instance()->verify_code($phone, $code);
|
||||
if (empty($verify_result['success'])) {
|
||||
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
|
||||
}
|
||||
} else {
|
||||
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
|
||||
$password_check = self::validate_password($pass, $confirm);
|
||||
if ($password_check !== true) {
|
||||
return ['success' => false, 'error' => $password_check];
|
||||
}
|
||||
|
||||
// 检查手机号是否已注册
|
||||
@@ -274,15 +58,6 @@ trait ServicePart1b {
|
||||
return ['success' => false, 'error' => __('该手机号已注册', 'aksrt')];
|
||||
}
|
||||
|
||||
// 生成用户名(phone_ + 后4位)
|
||||
$username = 'phone_' . substr($phone, -4);
|
||||
$suffix = 1;
|
||||
$base = $username;
|
||||
while (username_exists($username)) {
|
||||
$username = $base . '_' . $suffix;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
$ref_service = class_exists('\AKSRT\Payment\ReferralAttribution')
|
||||
? \AKSRT\Payment\ReferralAttribution::get_instance()
|
||||
: null;
|
||||
@@ -314,9 +89,15 @@ trait ServicePart1b {
|
||||
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
|
||||
}
|
||||
}
|
||||
if ($submitted_ref !== '' && empty($ref_context['valid']) && !empty($registration['reject_invalid'])) {
|
||||
if ($ref_service) {
|
||||
$ref_service->clear_registration_context();
|
||||
}
|
||||
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
$user_id = wp_create_user($username, $pass, $phone . '@sms.aksrt.local');
|
||||
$user_id = wp_create_user($username, $pass, '');
|
||||
if (is_wp_error($user_id)) {
|
||||
if ($ref_service) {
|
||||
$ref_service->clear_registration_context();
|
||||
@@ -328,16 +109,15 @@ trait ServicePart1b {
|
||||
update_user_meta($user_id, 'aksrt_phone', $phone);
|
||||
update_user_meta($user_id, 'aksrt_phone_verified', '1');
|
||||
|
||||
// 邮箱标记为已验证(手机注册等同于已验证)
|
||||
update_user_meta($user_id, 'aksrt_email_verified', '1');
|
||||
|
||||
if ($ref_service) {
|
||||
$ref_service->reward_registered_user((int) $user_id);
|
||||
}
|
||||
self::mark_email_verified((int) $user_id);
|
||||
|
||||
// 自动登录
|
||||
wp_set_current_user($user_id);
|
||||
wp_set_auth_cookie($user_id, true);
|
||||
$user = get_userdata($user_id);
|
||||
if ($user instanceof \WP_User) {
|
||||
do_action('wp_login', $user->user_login, $user);
|
||||
}
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url')
|
||||
? aksrt_get_user_center_url()
|
||||
@@ -352,101 +132,6 @@ trait ServicePart1b {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号登录(密码登录)
|
||||
*
|
||||
* @param array $post (phone, password)
|
||||
* @return array
|
||||
*/
|
||||
public static function login_by_phone_password(array $post): array {
|
||||
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
|
||||
$pass = (string) ($post['password'] ?? '');
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('login', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
|
||||
$user = self::get_user_by_phone($phone);
|
||||
if (!$user) {
|
||||
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!wp_check_password($pass, $user->data->user_pass, $user->ID)) {
|
||||
return ['success' => false, 'error' => __('密码不正确', 'aksrt')];
|
||||
}
|
||||
|
||||
wp_set_current_user($user->ID);
|
||||
wp_set_auth_cookie($user->ID, true);
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url')
|
||||
? aksrt_get_user_center_url()
|
||||
: home_url('/');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => $user->ID,
|
||||
'redirect' => $redirect,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号免密登录(SMS 验证码登录)
|
||||
*
|
||||
* @param array $post (phone, code)
|
||||
* @return array
|
||||
*/
|
||||
public static function login_by_phone_code(array $post): array {
|
||||
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
|
||||
$code = sanitize_text_field((string) ($post['code'] ?? ''));
|
||||
|
||||
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
|
||||
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
|
||||
}
|
||||
|
||||
if (function_exists('aksrt_require_captcha')) {
|
||||
$captcha_result = aksrt_require_captcha('login', $post);
|
||||
if ($captcha_result !== true) {
|
||||
return ['success' => false, 'error' => $captcha_result];
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 SMS 验证码
|
||||
if (class_exists('\AKSRT\Sms\SMS')) {
|
||||
$verify_result = \AKSRT\Sms\SMS::get_instance()->verify_code($phone, $code);
|
||||
if (empty($verify_result['success'])) {
|
||||
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
|
||||
}
|
||||
} else {
|
||||
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
|
||||
}
|
||||
|
||||
$user = self::get_user_by_phone($phone);
|
||||
if (!$user) {
|
||||
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
|
||||
}
|
||||
|
||||
wp_set_current_user($user->ID);
|
||||
wp_set_auth_cookie($user->ID, true);
|
||||
|
||||
$redirect = function_exists('aksrt_get_user_center_url')
|
||||
? aksrt_get_user_center_url()
|
||||
: home_url('/');
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'user_id' => $user->ID,
|
||||
'redirect' => $redirect,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号到当前用户
|
||||
*
|
||||
|
||||
+10
-42
@@ -113,6 +113,15 @@ trait ScriptsPart1b {
|
||||
$enqueue_module('footer');
|
||||
$enqueue_module('floating-buttons');
|
||||
$enqueue_module('enhancements');
|
||||
if (!is_user_logged_in() || (bool) get_query_var('aksrt_sign')) {
|
||||
$auth_css = get_template_directory() . '/assets/css/routes/sign.css';
|
||||
wp_enqueue_style(
|
||||
'aksrt-auth',
|
||||
$base_uri . '/assets/css/routes/sign.css',
|
||||
[$core_dep],
|
||||
file_exists($auth_css) ? (string) filemtime($auth_css) : $version
|
||||
);
|
||||
}
|
||||
|
||||
// 4a-b. 全局补充样式(骨架屏、空状态、分页、加载指示器)
|
||||
foreach (['skeleton', 'empty-state', 'pagination', 'ajax-spinner', 'modal-runtime'] as $slug) {
|
||||
@@ -443,17 +452,6 @@ trait ScriptsPart1b {
|
||||
: $version,
|
||||
true
|
||||
);
|
||||
// 绑定手机短信发送按钮依赖 sign-register 的 data-aksrt-sms-trigger 契约
|
||||
$sign_register_js = get_template_directory() . '/assets/js/sign-register.js';
|
||||
if (file_exists($sign_register_js)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-sign-register',
|
||||
$base_uri . '/assets/js/sign-register.js',
|
||||
['aksrt-route-user-center'],
|
||||
(string) filemtime($sign_register_js),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,39 +620,9 @@ trait ScriptsPart1b {
|
||||
}
|
||||
}
|
||||
|
||||
// 登录/注册/绑定 — pages + auth 路由专属 CSS
|
||||
// Unified authentication page shell.
|
||||
if ($is_auth_page) {
|
||||
$enqueue_module('pages');
|
||||
// Auth 路由专属样式
|
||||
wp_enqueue_style(
|
||||
'aksrt-route-auth',
|
||||
$base_uri . '/assets/css/routes/auth.css',
|
||||
['aksrt-tokens'],
|
||||
file_exists(get_template_directory() . '/assets/css/routes/auth.css')
|
||||
? (string) filemtime(get_template_directory() . '/assets/css/routes/auth.css')
|
||||
: $version
|
||||
);
|
||||
|
||||
$sign_css = get_template_directory() . '/assets/css/routes/sign.css';
|
||||
if (file_exists($sign_css)) {
|
||||
wp_enqueue_style(
|
||||
'aksrt-route-sign',
|
||||
$base_uri . '/assets/css/routes/sign.css',
|
||||
['aksrt-tokens'],
|
||||
(string) filemtime($sign_css)
|
||||
);
|
||||
}
|
||||
// 手机号注册/登录 + SMS 倒计时 + 人机验证联动
|
||||
$sign_js_path = get_template_directory() . '/assets/js/sign-register.js';
|
||||
if (file_exists($sign_js_path)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-sign-register',
|
||||
$base_uri . '/assets/js/sign-register.js',
|
||||
['loader_js'],
|
||||
(string) filemtime($sign_js_path),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+11
-17
@@ -61,9 +61,6 @@ trait ScriptsPart1c {
|
||||
'imgbox_full' => false,
|
||||
'imgbox_play' => false,
|
||||
'imgbox_down' => false,
|
||||
'sign_type' => 'page',
|
||||
'signin_url' => wp_login_url(home_url('/' . ltrim((string) ($GLOBALS['wp']->request ?? ''), '/'))),
|
||||
'signup_url' => wp_registration_url(),
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'ajaxpager' => '',
|
||||
'ajax_trigger' => __('加载更多', 'aksrt'),
|
||||
@@ -86,7 +83,6 @@ trait ScriptsPart1c {
|
||||
'split_minimum_size' => 10,
|
||||
'comment_upload_img' => false,
|
||||
'translate_config' => [],
|
||||
'qrcode_signin' => ['url' => false, 'state' => false],
|
||||
'i18n' => [
|
||||
'load_more' => __('加载更多', 'aksrt'),
|
||||
'processing' => __('处理中...', 'aksrt'),
|
||||
@@ -402,19 +398,17 @@ trait ScriptsPart1c {
|
||||
);
|
||||
}
|
||||
|
||||
// 弹窗登录/注册(仅未登录用户)
|
||||
if (!is_user_logged_in()) {
|
||||
$modal_auth_js = get_template_directory() . '/assets/js/modal-auth.js';
|
||||
if (file_exists($modal_auth_js)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-modal-auth',
|
||||
$base_uri . '/assets/js/modal-auth.js',
|
||||
['loader_js'],
|
||||
(string) filemtime($modal_auth_js),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
// One state machine owns modal and /sign/ authentication.
|
||||
$auth_js = get_template_directory() . '/assets/js/auth.js';
|
||||
if (file_exists($auth_js)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-auth',
|
||||
$base_uri . '/assets/js/auth.js',
|
||||
['aksrt-runtime'],
|
||||
(string) filemtime($auth_js),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if ($is_fixed_homepage) {
|
||||
$homepage_js_path = get_template_directory() . '/assets/js/homepage-fixed.js';
|
||||
|
||||
+86
-74
@@ -15,41 +15,9 @@ trait AuthReader {
|
||||
return !class_exists(Capabilities::class) || !Capabilities::user_can('comment_add', $user_id);
|
||||
}
|
||||
|
||||
/** 是否允许注册 */
|
||||
/** 是否允许前台注册。主题设置是唯一真值。 */
|
||||
public static function registration_enabled(): bool {
|
||||
return (bool) get_option('users_can_register');
|
||||
}
|
||||
|
||||
/** 注册是否要求邮箱验证 */
|
||||
public static function registration_requires_email_verification(): bool {
|
||||
$s = self::get_all();
|
||||
return self::registration_enabled() && !empty($s['registration_email_verification_enabled']);
|
||||
}
|
||||
|
||||
/** 注册邮箱验证方式 */
|
||||
public static function registration_verification_method(): string {
|
||||
$s = self::get_all();
|
||||
$method = $s['registration_email_verification_method'] ?? 'link';
|
||||
return in_array($method, ['link', 'code'], true) ? $method : 'link';
|
||||
}
|
||||
|
||||
/** 注册邮箱验证有效期(分钟) */
|
||||
public static function registration_verification_ttl_minutes(): int {
|
||||
$s = self::get_all();
|
||||
$ttl = (int) ($s['registration_verification_ttl_minutes'] ?? 30);
|
||||
return max(5, min(1440, $ttl));
|
||||
}
|
||||
|
||||
/** 验证成功后自动登录 */
|
||||
public static function registration_auto_login_after_verify(): bool {
|
||||
$s = self::get_all();
|
||||
return !isset($s['registration_auto_login_after_verify']) || !empty($s['registration_auto_login_after_verify']);
|
||||
}
|
||||
|
||||
/** 注册邮箱验证功能首次启用时间戳(用于历史账号判定) */
|
||||
public static function registration_verification_enabled_at(): int {
|
||||
$s = self::get_all();
|
||||
return (int) ($s['registration_verification_enabled_at'] ?? 0);
|
||||
return !self::close_signup();
|
||||
}
|
||||
|
||||
/** 主题内置默认图片 URL */
|
||||
@@ -57,21 +25,15 @@ trait AuthReader {
|
||||
return get_template_directory_uri() . '/assets/img/' . $filename;
|
||||
}
|
||||
|
||||
/** 获取Logo URL(留空时使用内置默认;legacy别名 logo_src 回退) */
|
||||
/** 获取 Logo URL(留空时使用内置默认) */
|
||||
public static function logo_url(): string {
|
||||
$url = trim((string) self::get_value('site_logo', ''));
|
||||
if ($url === '') {
|
||||
$url = trim((string) get_option('logo_src', ''));
|
||||
}
|
||||
return $url !== '' ? $url : self::default_asset_url('default-logo.svg');
|
||||
}
|
||||
|
||||
/** 获取暗色模式 Logo URL(留空时跟随浅色模式;legacy别名 logo_src_dark 回退) */
|
||||
/** 获取暗色模式 Logo URL(留空时跟随浅色模式) */
|
||||
public static function logo_dark_url(): string {
|
||||
$url = trim((string) self::get_value('site_logo_dark', ''));
|
||||
if ($url === '') {
|
||||
$url = trim((string) get_option('logo_src_dark', ''));
|
||||
}
|
||||
return $url !== '' ? $url : self::logo_url();
|
||||
}
|
||||
|
||||
@@ -88,10 +50,10 @@ trait AuthReader {
|
||||
return (bool) self::get_value('close_sign', false);
|
||||
}
|
||||
|
||||
/** 是否关闭注册(受 close_sign 总开关影响;兼容 user_close_signup 别名) */
|
||||
/** 是否关闭注册。禁用账密登录时同时关闭账密注册。 */
|
||||
public static function close_signup(): bool {
|
||||
if (self::close_sign()) return true;
|
||||
return (bool) self::get_value('user_close_signup', false);
|
||||
return (bool) self::get_value('close_signup', false) || self::close_signin();
|
||||
}
|
||||
|
||||
/** 是否关闭账密登录(仅允许社交登录) */
|
||||
@@ -100,16 +62,10 @@ trait AuthReader {
|
||||
return (bool) self::get_value('close_signin', false);
|
||||
}
|
||||
|
||||
/** 是否关闭登录(受 close_sign 总开关影响;兼容 user_close_login 别名) */
|
||||
public static function close_login(): bool {
|
||||
if (self::close_sign()) return true;
|
||||
return (bool) self::get_value('user_close_login', false);
|
||||
}
|
||||
|
||||
/** 登录注册样式: 'modal' | 'page'(默认 page;modal 运行时未就绪时自动回退 page) */
|
||||
/** 登录注册样式: modal | page。 */
|
||||
public static function user_sign_type(): string {
|
||||
$type = (string) self::get_value('user_sign_type', 'page');
|
||||
if (!in_array($type, ['modal', 'page'], true)) $type = 'page';
|
||||
$type = (string) self::get_value('user_sign_type', 'modal');
|
||||
if (!in_array($type, ['modal', 'page'], true)) $type = 'modal';
|
||||
return $type;
|
||||
}
|
||||
|
||||
@@ -118,16 +74,24 @@ trait AuthReader {
|
||||
return trim((string) self::get_value('admin_sign_key', ''));
|
||||
}
|
||||
|
||||
/** Whether wp-login.php is replaced by /sign/. */
|
||||
public static function replace_wp_login(): bool {
|
||||
return (bool) self::get_value('replace_wp_login', true);
|
||||
}
|
||||
|
||||
/** 登录页 UI 配置数组 */
|
||||
public static function sign_page_ui(): array {
|
||||
$theme_mode = (string) self::get_value('sign_theme_mode', 'no');
|
||||
if (!in_array($theme_mode, ['no', 'white-theme', 'dark-theme'], true)) {
|
||||
$theme_mode = 'no';
|
||||
}
|
||||
return [
|
||||
'background' => self::get_value('sign_page_background', []),
|
||||
'theme_mode' => in_array((string) self::get_value('sign_theme_mode', 'default'), ['default', 'light', 'dark'], true)
|
||||
? (string) self::get_value('sign_theme_mode', 'default') : 'default',
|
||||
'theme_mode' => $theme_mode,
|
||||
'show_header' => (bool) self::get_value('sign_show_header', true),
|
||||
'card_position' => in_array((string) self::get_value('sign_card_position', 'center'), ['left', 'center', 'right'], true)
|
||||
? (string) self::get_value('sign_card_position', 'center') : 'center',
|
||||
'show_logo' => (bool) self::get_value('sign_show_logo', true),
|
||||
'card_position' => in_array((string) self::get_value('sign_card_position', 'right'), ['left', 'center', 'right'], true)
|
||||
? (string) self::get_value('sign_card_position', 'right') : 'right',
|
||||
'show_logo' => (bool) self::get_value('sign_show_logo', false),
|
||||
'footer' => (string) self::get_value('sign_page_footer', ''),
|
||||
];
|
||||
}
|
||||
@@ -136,7 +100,7 @@ trait AuthReader {
|
||||
public static function modal_ui(): array {
|
||||
return [
|
||||
'background' => self::get_value('sign_modal_background', []),
|
||||
'show_logo' => (bool) self::get_value('sign_modal_show_logo', true),
|
||||
'show_logo' => (bool) self::get_value('sign_modal_show_logo', false),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -165,24 +129,70 @@ trait AuthReader {
|
||||
return (bool) self::get_value('user_signin_phone_s', false);
|
||||
}
|
||||
|
||||
/** 免密登录显示模式 */
|
||||
public static function nopas_active_mode(): string {
|
||||
$mode = (string) self::get_value('user_signin_nopas_active', 'nopas_first');
|
||||
return in_array($mode, ['nopas_first', 'password_first', 'nopas_only'], true) ? $mode : 'nopas_first';
|
||||
/** 是否启用验证码免密登录。 */
|
||||
public static function passwordless_login_enabled(): bool {
|
||||
return (bool) self::get_value('user_signin_nopas_s', false);
|
||||
}
|
||||
|
||||
/** 绑定提醒类型(兼容旧 bool 值:true→'phone',false→'close') */
|
||||
/** 免密登录验证渠道。 */
|
||||
public static function passwordless_login_type(): string {
|
||||
$type = (string) self::get_value('user_signin_nopas_type', 'email');
|
||||
return in_array($type, ['email', 'phone', 'email_phone'], true) ? $type : 'email';
|
||||
}
|
||||
|
||||
/** 免密登录显示模式 */
|
||||
public static function nopas_active_mode(): string {
|
||||
$mode = (string) self::get_value('user_signin_nopas_active', 'nopas');
|
||||
return in_array($mode, ['nopas', 'pas', 'only_nopas'], true) ? $mode : 'nopas';
|
||||
}
|
||||
|
||||
/** 是否在注册前验证邮箱或手机号。 */
|
||||
public static function signup_verification_enabled(): bool {
|
||||
return (bool) self::get_value('user_signup_captch', false);
|
||||
}
|
||||
|
||||
/** 注册验证渠道。 */
|
||||
public static function signup_verification_type(): string {
|
||||
$type = (string) self::get_value('captch_type', 'email');
|
||||
return in_array($type, ['email', 'phone', 'email_phone'], true) ? $type : 'email';
|
||||
}
|
||||
|
||||
/** 找回密码验证渠道。 */
|
||||
public static function reset_password_verification_type(): string {
|
||||
$type = (string) self::get_value('user_repas_captch_type', 'email');
|
||||
return in_array($type, ['email', 'phone', 'email_phone'], true) ? $type : 'email';
|
||||
}
|
||||
|
||||
/** 注册时是否省略重复密码。 */
|
||||
public static function signup_omit_password_confirmation(): bool {
|
||||
return (bool) self::get_value('user_signup_no_repas', true);
|
||||
}
|
||||
|
||||
/** 推广码在注册表单中的唯一显示模式。 */
|
||||
public static function invitation_mode(): string {
|
||||
$mode = (string) self::get_value('invit_code_s', 'close');
|
||||
return in_array($mode, ['close', 'open', 'must'], true) ? $mode : 'close';
|
||||
}
|
||||
|
||||
/** 自动弹窗配置。 */
|
||||
public static function sign_modal_auto(): array {
|
||||
return [
|
||||
'enabled' => self::user_sign_type() === 'modal' && (bool) self::get_value('sign_modal_auto_s', false),
|
||||
'waiting' => max(1, min(100, (int) self::get_value('sign_modal_auto_waiting_time', 5))),
|
||||
'expires' => max(0, min(2000, (float) self::get_value('sign_modal_auto_expires', 1))),
|
||||
];
|
||||
}
|
||||
|
||||
/** 绑定提醒类型 */
|
||||
public static function bind_reminder_type(): string {
|
||||
$val = self::get_value('bind_reminder', 'close');
|
||||
if (is_bool($val)) return $val ? 'phone' : 'close';
|
||||
$val = (string) $val;
|
||||
return in_array($val, ['email', 'phone', 'email_phone', 'close'], true) ? $val : 'close';
|
||||
}
|
||||
|
||||
/** 强制绑定类型(兼容旧 bool 值:true→'phone',false→'close') */
|
||||
/** 强制绑定类型 */
|
||||
public static function mandatory_bind_type(): string {
|
||||
$val = self::get_value('mandatory_bind', 'close');
|
||||
if (is_bool($val)) return $val ? 'phone' : 'close';
|
||||
$val = (string) $val;
|
||||
return in_array($val, ['email', 'phone', 'email_phone', 'close'], true) ? $val : 'close';
|
||||
}
|
||||
@@ -196,10 +206,10 @@ trait AuthReader {
|
||||
return array_values(array_filter(array_map('trim', $items), 'strlen'));
|
||||
}
|
||||
|
||||
/** 邮箱限制配置:['type' => 'black'|'white', 'domains' => [...]] */
|
||||
/** 邮箱限制配置:['type' => 'not'|'allow', 'domains' => [...]] */
|
||||
public static function email_limit(): array {
|
||||
$type = (string) self::get_value('user_email_limit_type', 'black');
|
||||
if (!in_array($type, ['black', 'white'], true)) $type = 'black';
|
||||
$type = (string) self::get_value('user_email_limit_type', 'not');
|
||||
if (!in_array($type, ['not', 'allow'], true)) $type = 'not';
|
||||
$raw = (string) self::get_value('user_email_out', '');
|
||||
$domains = [];
|
||||
if ($raw !== '') {
|
||||
@@ -210,12 +220,14 @@ trait AuthReader {
|
||||
}
|
||||
|
||||
|
||||
/** 用户协议前台配置(enabled / required / page) */
|
||||
/** 用户协议及隐私协议前台配置。 */
|
||||
public static function user_agreement_settings(): array {
|
||||
return [
|
||||
'enabled' => !isset(self::get_all()['user_agreement_s']) || !empty(self::get_value('user_agreement_s', true)),
|
||||
'required' => !isset(self::get_all()['user_agreement_check']) || !empty(self::get_value('user_agreement_check', true)),
|
||||
'page' => trim((string) self::get_value('user_agreement_page', '')),
|
||||
'enabled' => (bool) self::get_value('user_agreement_s', false),
|
||||
'privacy' => (bool) self::get_value('user_privacy_s', false),
|
||||
'required' => (bool) self::get_value('user_agreement_check', false),
|
||||
'page' => trim((string) self::get_value('user_agreement_page', '')),
|
||||
'privacy_page'=> trim((string) self::get_value('user_privacy_page', '')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,24 +123,23 @@ class CreditsRuntime {
|
||||
*/
|
||||
public static function get_referral_registration_settings(): array {
|
||||
$all = Settings::get_all();
|
||||
$enabled = !isset($all['referral_registration_enabled']) || !empty($all['referral_registration_enabled']);
|
||||
$register_field = sanitize_key((string) ($all['referral_register_field'] ?? 'optional'));
|
||||
if (!in_array($register_field, ['hidden', 'optional', 'required'], true)) {
|
||||
$register_field = 'optional';
|
||||
$mode = sanitize_key((string) ($all['invit_code_s'] ?? 'close'));
|
||||
if (!in_array($mode, ['close', 'open', 'must'], true)) {
|
||||
$mode = 'close';
|
||||
}
|
||||
$open_desc = trim((string) ($all['referral_open_desc'] ?? ''));
|
||||
$open_desc = trim((string) ($all['invit_code_open_desc'] ?? ''));
|
||||
if ($open_desc === '') {
|
||||
$open_desc = __('推广码为选填,没有推广码也可以注册', 'aksrt');
|
||||
$open_desc = __('如您受到邀请,可填写推广码。部分推广码会有额外积分赠送', 'aksrt');
|
||||
}
|
||||
$required_desc = trim((string) ($all['referral_required_desc'] ?? ''));
|
||||
$required_desc = trim((string) ($all['invit_code_must_desc'] ?? ''));
|
||||
if ($required_desc === '') {
|
||||
$required_desc = __('请输入推广码', 'aksrt');
|
||||
$required_desc = __('<div>本站需使用推广码注册</div><div>获取推广码请与客服联系</div>', 'aksrt');
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'register_field' => $register_field,
|
||||
'reject_invalid' => !isset($all['referral_reject_invalid']) || !empty($all['referral_reject_invalid']),
|
||||
'enabled' => $mode !== 'close',
|
||||
'register_field' => $mode === 'must' ? 'required' : ($mode === 'open' ? 'optional' : 'hidden'),
|
||||
'reject_invalid' => $mode !== 'close',
|
||||
'open_desc' => $open_desc,
|
||||
'required_desc' => $required_desc,
|
||||
];
|
||||
|
||||
@@ -288,7 +288,6 @@ class Integration {
|
||||
'download_ready' => true,
|
||||
'credits_added' => true,
|
||||
'credits_deducted' => true,
|
||||
'verify_email' => true,
|
||||
'pending_comments' => true,
|
||||
'upload_moderation' => true,
|
||||
],
|
||||
|
||||
@@ -174,14 +174,6 @@ class SMS {
|
||||
return ['success' => true, 'message' => __('验证成功', 'aksrt')];
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧接口:verify_code 返回 bool
|
||||
*/
|
||||
public function verify_code_bool(string $phone, string $code): bool {
|
||||
$result = $this->verify_code($phone, $code);
|
||||
return !empty($result['success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
|
||||
+1
-1
@@ -387,7 +387,7 @@ class Home {
|
||||
}
|
||||
|
||||
if (!empty($verify_settings['accept_email_verified']) && class_exists('\AKSRT\Auth\Service')) {
|
||||
return \AKSRT\Auth\Service::user_is_email_verified($user_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user