feat: 添加身份认证服务商配置页
This commit is contained in:
1 parent
cc33664c1b
commit
92e445dbd4
7 files changed
+538
-1
No files matched your search
@@ -20,6 +20,14 @@ export const usersApiMethods = {
|
|||||||
deleteNotification(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }) },
|
deleteNotification(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }) },
|
||||||
runNotificationCleanup() { return this.request('/notifications/cleanup', { method: 'POST', body: JSON.stringify({}) }) },
|
runNotificationCleanup() { return this.request('/notifications/cleanup', { method: 'POST', body: JSON.stringify({}) }) },
|
||||||
|
|
||||||
|
getIdentitySettings() { return this.request('/identity/settings') },
|
||||||
|
saveIdentitySettings(data) {
|
||||||
|
return this.request('/identity/settings', { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
testIdentitySettings(data = {}) {
|
||||||
|
return this.request('/identity/settings/test', { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
|
||||||
getCreditSettings() { return this.request('/credits/settings') },
|
getCreditSettings() { return this.request('/credits/settings') },
|
||||||
saveCreditSettings(data) { return this.request('/credits/settings', { method: 'POST', body: JSON.stringify(data) }) },
|
saveCreditSettings(data) { return this.request('/credits/settings', { method: 'POST', body: JSON.stringify(data) }) },
|
||||||
listCreditUsers(params = {}) {
|
listCreditUsers(params = {}) {
|
||||||
|
|||||||
@@ -63,11 +63,12 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'credits',
|
section: 'credits',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
customPage({
|
||||||
key: 'users-verify',
|
key: 'users-verify',
|
||||||
name: 'UsersVerify',
|
name: 'UsersVerify',
|
||||||
label: '身份认证',
|
label: '身份认证',
|
||||||
route: '/users/verify',
|
route: '/users/verify',
|
||||||
|
component: 'UsersVerify',
|
||||||
title: '身份认证',
|
title: '身份认证',
|
||||||
desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。',
|
desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。',
|
||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<div class="aksrt-page-container">
|
||||||
|
<PageHeader
|
||||||
|
title="身份认证"
|
||||||
|
desc="统一管理认证申请规则与云服务商凭证;审核、驳回和取消认证仍在 WordPress 用户菜单处理。"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-spin :show="loading" style="width: 100%; min-height: 240px">
|
||||||
|
<n-grid :cols="3" :x-gap="12" :y-gap="12" responsive="screen" item-responsive>
|
||||||
|
<n-grid-item span="3 m:1">
|
||||||
|
<n-card class="aksrt-status-card" size="small">
|
||||||
|
<n-statistic label="身份认证功能">
|
||||||
|
<template #default>{{ verifyForm.user_verify_enabled ? '已启用' : '已停用' }}</template>
|
||||||
|
</n-statistic>
|
||||||
|
<n-tag :type="verifyForm.user_verify_enabled ? 'success' : 'default'" round size="small">
|
||||||
|
{{ verifyForm.user_verify_enabled ? '运行中' : '未运行' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item span="3 m:1" v-for="key in providerKeys" :key="key">
|
||||||
|
<n-card class="aksrt-status-card" size="small">
|
||||||
|
<n-statistic :label="providerLabel(key) + '凭证'">
|
||||||
|
<template #default>{{ providerState(key).configured ? '已配置' : '未配置' }}</template>
|
||||||
|
</n-statistic>
|
||||||
|
<n-tag :type="providerState(key).configured ? 'success' : 'warning'" round size="small">
|
||||||
|
{{ identityForm.provider === key ? '当前服务商' : '备用服务商' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<n-card class="aksrt-section-card" title="通用认证设置">
|
||||||
|
<n-form :model="verifyForm" label-placement="left" label-width="190">
|
||||||
|
<n-form-item label="启用身份认证"><n-switch v-model:value="verifyForm.user_verify_enabled" /></n-form-item>
|
||||||
|
<n-form-item label="公开页显示认证徽标"><n-switch v-model:value="verifyForm.user_verify_public_display" /></n-form-item>
|
||||||
|
<n-form-item label="允许用户前台申请"><n-switch v-model:value="verifyForm.user_verify_allow_apply" /></n-form-item>
|
||||||
|
<n-form-item label="个人认证徽标文案">
|
||||||
|
<n-input v-model:value="verifyForm.user_verify_personal_badge_text" placeholder="个人认证" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="企业认证徽标文案">
|
||||||
|
<n-input v-model:value="verifyForm.user_verify_enterprise_badge_text" placeholder="企业认证" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="停用申请说明">
|
||||||
|
<n-input v-model:value="verifyForm.user_verify_disable_desc" type="textarea" :rows="2" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="申请介绍">
|
||||||
|
<n-input v-model:value="verifyForm.user_verify_desc" type="textarea" :rows="3" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-card class="aksrt-section-card" title="云服务商">
|
||||||
|
<n-form :model="identityForm" label-placement="left" label-width="190">
|
||||||
|
<n-form-item label="当前服务商">
|
||||||
|
<n-radio-group v-model:value="identityForm.provider">
|
||||||
|
<n-space>
|
||||||
|
<n-radio-button value="aliyun">阿里云</n-radio-button>
|
||||||
|
<n-radio-button value="tencent">腾讯云</n-radio-button>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="回调地址">
|
||||||
|
<n-input-group>
|
||||||
|
<n-input :value="callbackUrl" readonly />
|
||||||
|
<n-button @click="copyCallback">复制</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
<template #feedback>将此地址填入服务商控制台需要的回调地址配置。</template>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-tabs v-model:value="activeTab" type="segment" animated class="aksrt-provider-tabs">
|
||||||
|
<n-tab-pane v-for="key in providerKeys" :key="key" :name="key" :tab="providerLabel(key)">
|
||||||
|
<n-card class="aksrt-section-card">
|
||||||
|
<template #header>
|
||||||
|
<n-space align="center">
|
||||||
|
<span>{{ providerLabel(key) }}凭证</span>
|
||||||
|
<n-tag :type="providerState(key).configured ? 'success' : 'warning'" size="small" round>
|
||||||
|
{{ providerState(key).configured ? '已配置' : '未配置' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<n-alert type="info" :show-icon="false" style="margin-bottom: 18px">
|
||||||
|
已保存凭证不会回显。留空表示保留原值;输入新值并保存会替换对应凭证。
|
||||||
|
</n-alert>
|
||||||
|
<n-form label-placement="left" label-width="190">
|
||||||
|
<n-form-item v-for="field in credentialFields[key]" :key="field.key" :label="field.label">
|
||||||
|
<n-input
|
||||||
|
v-model:value="credentials[key][field.key]"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
:placeholder="credentialPlaceholder(key, field.key)"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="显式清除凭证">
|
||||||
|
<n-checkbox v-model:checked="clearCredentials[key]">
|
||||||
|
保存时删除 {{ providerLabel(key) }}全部凭证
|
||||||
|
</n-checkbox>
|
||||||
|
<template #feedback>清除后该服务商立即变为未配置;此操作不会因输入框留空而触发。</template>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="配置测试">
|
||||||
|
<n-button :loading="testing === key" :disabled="saving" @click="testProvider(key)">
|
||||||
|
测试 {{ providerLabel(key) }}配置
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
|
||||||
|
<div class="aksrt-form-actions">
|
||||||
|
<n-button :disabled="!dirty" @click="load">放弃更改</n-button>
|
||||||
|
<n-button type="primary" :loading="saving" @click="save">保存全部配置</n-button>
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { useDialog, useMessage } from 'naive-ui'
|
||||||
|
import PageHeader from '@/components/PageHeader.vue'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const providerKeys = ['aliyun', 'tencent']
|
||||||
|
const credentialFields = {
|
||||||
|
aliyun: [
|
||||||
|
{ key: 'access_key_id', label: 'AccessKey ID' },
|
||||||
|
{ key: 'access_key_secret', label: 'AccessKey Secret' },
|
||||||
|
],
|
||||||
|
tencent: [
|
||||||
|
{ key: 'secret_id', label: 'SecretId' },
|
||||||
|
{ key: 'secret_key', label: 'SecretKey' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const verifyDefaults = {
|
||||||
|
user_verify_enabled: true,
|
||||||
|
user_verify_public_display: true,
|
||||||
|
user_verify_allow_apply: true,
|
||||||
|
user_verify_personal_badge_text: '个人认证',
|
||||||
|
user_verify_enterprise_badge_text: '企业认证',
|
||||||
|
user_verify_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
|
||||||
|
user_verify_desc: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const saving = ref(false)
|
||||||
|
const testing = ref('')
|
||||||
|
const activeTab = ref('aliyun')
|
||||||
|
const callbackUrl = ref('')
|
||||||
|
const initialSnapshot = ref('')
|
||||||
|
const states = reactive({ aliyun: {}, tencent: {} })
|
||||||
|
const verifyForm = reactive({ ...verifyDefaults })
|
||||||
|
const identityForm = reactive({ provider: 'aliyun' })
|
||||||
|
const credentials = reactive({ aliyun: {}, tencent: {} })
|
||||||
|
const clearCredentials = reactive({ aliyun: false, tencent: false })
|
||||||
|
|
||||||
|
const snapshot = computed(() => JSON.stringify({ verifyForm, identityForm, credentials, clearCredentials }))
|
||||||
|
const dirty = computed(() => initialSnapshot.value !== '' && snapshot.value !== initialSnapshot.value)
|
||||||
|
watch(() => identityForm.provider, value => { activeTab.value = value })
|
||||||
|
|
||||||
|
function providerLabel(key) {
|
||||||
|
return key === 'aliyun' ? '阿里云' : '腾讯云'
|
||||||
|
}
|
||||||
|
function providerState(key) {
|
||||||
|
return states[key] || {}
|
||||||
|
}
|
||||||
|
function credentialPlaceholder(provider, field) {
|
||||||
|
return providerState(provider).configured_fields?.[field] ? '已配置,留空则保留' : '尚未配置'
|
||||||
|
}
|
||||||
|
function extractData(response) {
|
||||||
|
return response?.data ?? response ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [verifyResponse, identityResponse] = await Promise.all([
|
||||||
|
api.getSchemaPage('users', 'verify'),
|
||||||
|
api.getIdentitySettings(),
|
||||||
|
])
|
||||||
|
Object.assign(verifyForm, verifyDefaults, extractData(verifyResponse))
|
||||||
|
const identity = extractData(identityResponse)
|
||||||
|
identityForm.provider = providerKeys.includes(identity.provider) ? identity.provider : 'aliyun'
|
||||||
|
callbackUrl.value = identity.callback_url || ''
|
||||||
|
providerKeys.forEach(key => {
|
||||||
|
states[key] = identity.providers?.[key] || {}
|
||||||
|
credentials[key] = {}
|
||||||
|
clearCredentials[key] = false
|
||||||
|
})
|
||||||
|
activeTab.value = identityForm.provider
|
||||||
|
initialSnapshot.value = snapshot.value
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || '加载身份认证配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProviderPayload(key) {
|
||||||
|
const replacement = {}
|
||||||
|
credentialFields[key].forEach(({ key: field }) => {
|
||||||
|
const value = String(credentials[key]?.[field] || '').trim()
|
||||||
|
if (value) replacement[field] = value
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
credentials: replacement,
|
||||||
|
clear_credentials: clearCredentials[key],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persist() {
|
||||||
|
const [verifyResponse, identityResponse] = await Promise.all([
|
||||||
|
api.saveSchemaPage('users', 'verify', { ...verifyForm }),
|
||||||
|
api.saveIdentitySettings({
|
||||||
|
provider: identityForm.provider,
|
||||||
|
providers: {
|
||||||
|
aliyun: buildProviderPayload('aliyun'),
|
||||||
|
tencent: buildProviderPayload('tencent'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
const identity = extractData(identityResponse)
|
||||||
|
Object.assign(verifyForm, extractData(verifyResponse))
|
||||||
|
providerKeys.forEach(key => {
|
||||||
|
states[key] = identity.providers?.[key] || states[key]
|
||||||
|
credentials[key] = {}
|
||||||
|
clearCredentials[key] = false
|
||||||
|
})
|
||||||
|
callbackUrl.value = identity.callback_url || callbackUrl.value
|
||||||
|
initialSnapshot.value = snapshot.value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const hasClear = providerKeys.some(key => clearCredentials[key])
|
||||||
|
const run = async () => {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await persist()
|
||||||
|
message.success('身份认证配置已保存')
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || '保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasClear) return run()
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认清除凭证',
|
||||||
|
content: '已勾选的服务商凭证将被永久删除,是否继续保存?',
|
||||||
|
positiveText: '清除并保存',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: run,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testProvider(provider) {
|
||||||
|
testing.value = provider
|
||||||
|
try {
|
||||||
|
const response = await api.testIdentitySettings({ provider })
|
||||||
|
message.success(response?.message || `${providerLabel(provider)}配置测试通过`)
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || `${providerLabel(provider)}配置测试失败`)
|
||||||
|
} finally {
|
||||||
|
testing.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyCallback() {
|
||||||
|
if (!callbackUrl.value) return message.warning('回调地址暂不可用')
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(callbackUrl.value)
|
||||||
|
message.success('回调地址已复制')
|
||||||
|
} catch {
|
||||||
|
message.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.aksrt-status-card :deep(.n-card__content) {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.aksrt-section-card { margin-top: 16px; }
|
||||||
|
.aksrt-provider-tabs { margin-top: 16px; }
|
||||||
|
.aksrt-form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export const usersPageLoaders = {
|
export const usersPageLoaders = {
|
||||||
UsersFeatures: () => import('@/pages/users/UsersFeatures.vue'),
|
UsersFeatures: () => import('@/pages/users/UsersFeatures.vue'),
|
||||||
UsersSignin: () => import('@/pages/users/UsersSignin.vue'),
|
UsersSignin: () => import('@/pages/users/UsersSignin.vue'),
|
||||||
|
UsersVerify: () => import('@/pages/users/UsersVerify.vue'),
|
||||||
UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
|
UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
|
||||||
UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
|
UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
|
||||||
UsersWechatTemplate: () => import('@/pages/users/UsersWechatTemplate.vue'),
|
UsersWechatTemplate: () => import('@/pages/users/UsersWechatTemplate.vue'),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class UserController extends ControllerBase {
|
|||||||
use UserControllerReportOperations;
|
use UserControllerReportOperations;
|
||||||
use UserControllerInviteOperations;
|
use UserControllerInviteOperations;
|
||||||
use UserControllerVerifyIntegrations;
|
use UserControllerVerifyIntegrations;
|
||||||
|
use UserControllerIdentitySettings;
|
||||||
use UserControllerAuthPhoneOperations;
|
use UserControllerAuthPhoneOperations;
|
||||||
use UserControllerRestrictionOperations;
|
use UserControllerRestrictionOperations;
|
||||||
use UserControllerFollowOperations;
|
use UserControllerFollowOperations;
|
||||||
@@ -260,6 +261,25 @@ class UserController extends ControllerBase {
|
|||||||
'permission_callback' => function () { return is_user_logged_in(); },
|
'permission_callback' => function () { return is_user_logged_in(); },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Identity verification provider settings (admin only)
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/identity/settings', [
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [$this, 'get_identity_settings'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => [$this, 'update_identity_settings'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
register_rest_route(self::API_NAMESPACE, '/identity/settings/test', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => [$this, 'test_identity_settings'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
]);
|
||||||
|
|
||||||
// SMS provider status
|
// SMS provider status
|
||||||
register_rest_route(self::API_NAMESPACE, '/sms/status', [
|
register_rest_route(self::API_NAMESPACE, '/sms/status', [
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin REST handlers for identity provider configuration.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Admin;
|
||||||
|
|
||||||
|
use AKSRT\Settings\IdentityAdmin;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
trait UserControllerIdentitySettings {
|
||||||
|
public function get_identity_settings(): \WP_REST_Response {
|
||||||
|
return $this->success([
|
||||||
|
'data' => $this->identity_settings_response(IdentityAdmin::read()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update_identity_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
|
$settings = IdentityAdmin::update($this->get_json_params($request));
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'message' => __('身份认证服务商配置已保存', 'aksrt'),
|
||||||
|
'data' => $this->identity_settings_response($settings),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_identity_settings(\WP_REST_Request $request): \WP_REST_Response {
|
||||||
|
$params = $this->get_json_params($request);
|
||||||
|
$provider = sanitize_key((string) ($params['provider'] ?? ''));
|
||||||
|
$result = IdentityAdmin::test($provider);
|
||||||
|
|
||||||
|
if (empty($result['success'])) {
|
||||||
|
return $this->error((string) ($result['message'] ?? __('配置检查失败', 'aksrt')), 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'message' => (string) $result['message'],
|
||||||
|
'data' => ['provider' => $provider, 'configured' => true],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function identity_settings_response(array $settings): array {
|
||||||
|
$settings['callback_url'] = rest_url(self::API_NAMESPACE . '/identity/callback');
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Identity provider configuration storage for the admin REST surface.
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Settings;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
final class IdentityAdmin {
|
||||||
|
private const OPTION_KEY = 'aksrt_identity_settings';
|
||||||
|
|
||||||
|
private const PROVIDER_FIELDS = [
|
||||||
|
'aliyun' => ['access_key_id', 'access_key_secret'],
|
||||||
|
'tencent' => ['secret_id', 'secret_key'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function read(): array {
|
||||||
|
$stored = get_option(self::OPTION_KEY, []);
|
||||||
|
$stored = is_array($stored) ? $stored : [];
|
||||||
|
$provider = sanitize_key((string) ($stored['provider'] ?? 'aliyun'));
|
||||||
|
|
||||||
|
if (!isset(self::PROVIDER_FIELDS[$provider])) {
|
||||||
|
$provider = 'aliyun';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'provider' => $provider,
|
||||||
|
'providers' => [
|
||||||
|
'aliyun' => self::provider_state('aliyun', $stored),
|
||||||
|
'tencent' => self::provider_state('tencent', $stored),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function update(array $input): array {
|
||||||
|
$stored = get_option(self::OPTION_KEY, []);
|
||||||
|
$stored = is_array($stored) ? $stored : [];
|
||||||
|
|
||||||
|
if (array_key_exists('provider', $input)) {
|
||||||
|
$provider = sanitize_key((string) $input['provider']);
|
||||||
|
if (isset(self::PROVIDER_FIELDS[$provider])) {
|
||||||
|
$stored['provider'] = $provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider_input = isset($input['providers']) && is_array($input['providers'])
|
||||||
|
? $input['providers']
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach (self::PROVIDER_FIELDS as $provider => $fields) {
|
||||||
|
$changes = isset($provider_input[$provider]) && is_array($provider_input[$provider])
|
||||||
|
? $provider_input[$provider]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!empty($changes['clear_credentials'])) {
|
||||||
|
unset($stored['providers'][$provider]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = isset($changes['credentials']) && is_array($changes['credentials'])
|
||||||
|
? $changes['credentials']
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (!isset($credentials[$field]) || trim((string) $credentials[$field]) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$stored['providers'][$provider][$field] = self::encrypt(
|
||||||
|
sanitize_text_field((string) $credentials[$field])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option(self::OPTION_KEY, $stored, false);
|
||||||
|
return self::read();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function test(string $provider): array {
|
||||||
|
$settings = self::read();
|
||||||
|
if (!isset(self::PROVIDER_FIELDS[$provider])) {
|
||||||
|
return ['success' => false, 'message' => '不支持的身份认证服务商'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$state = $settings['providers'][$provider];
|
||||||
|
if (!$state['configured']) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => sprintf('%s凭证不完整,请补充后保存', $state['label']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => sprintf('%s配置检查通过,凭证字段完整', $state['label']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function encrypt(string $value): string {
|
||||||
|
if ($value === '' || !function_exists('openssl_encrypt')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = hash('sha256', wp_salt('auth'), true);
|
||||||
|
$iv = random_bytes(12);
|
||||||
|
$tag = '';
|
||||||
|
$ciphertext = openssl_encrypt($value, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||||
|
if ($ciphertext === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($iv . $tag . $ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function decrypt(string $encoded): string {
|
||||||
|
if ($encoded === '' || !function_exists('openssl_decrypt')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = base64_decode($encoded, true);
|
||||||
|
if ($payload === false || strlen($payload) <= 28) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$iv = substr($payload, 0, 12);
|
||||||
|
$tag = substr($payload, 12, 16);
|
||||||
|
$ciphertext = substr($payload, 28);
|
||||||
|
$key = hash('sha256', wp_salt('auth'), true);
|
||||||
|
$plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
|
||||||
|
|
||||||
|
return is_string($plaintext) ? $plaintext : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function provider_state(string $provider, array $stored): array {
|
||||||
|
$credentials = isset($stored['providers'][$provider]) && is_array($stored['providers'][$provider])
|
||||||
|
? $stored['providers'][$provider]
|
||||||
|
: [];
|
||||||
|
$missing = [];
|
||||||
|
$configured_fields = [];
|
||||||
|
|
||||||
|
foreach (self::PROVIDER_FIELDS[$provider] as $field) {
|
||||||
|
$has_value = self::decrypt((string) ($credentials[$field] ?? '')) !== '';
|
||||||
|
$configured_fields[$field] = $has_value;
|
||||||
|
if (!$has_value) {
|
||||||
|
$missing[] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => $provider === 'aliyun' ? '阿里云' : '腾讯云',
|
||||||
|
'configured' => $missing === [],
|
||||||
|
'configured_fields' => $configured_fields,
|
||||||
|
'missing_fields' => $missing,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user