feat: enhance webhook security with configurable allow list (#719)

- Add enableAllowList flag to webhook settings for flexible access control
- Update frontend UI with toggle switch and improved user experience
- Maintain backward compatibility with default allow-all behavior
- Add input validation hints and better form controls across admin panels

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2025-09-05 17:24:30 +08:00
committed by GitHub
parent 3fbace871c
commit 37cf0776b5
12 changed files with 142 additions and 51 deletions

View File

@@ -13,6 +13,7 @@ const { t } = useI18n({
messages: {
en: {
tip: 'You can manually input the following multiple select input and enter',
manualInputPrompt: 'Type and press Enter to add',
save: 'Save',
successTip: 'Save Success',
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
@@ -38,6 +39,7 @@ const { t } = useI18n({
},
zh: {
tip: '您可以手动输入以下多选输入框, 回车增加',
manualInputPrompt: '输入后按回车键添加',
save: '保存',
successTip: '保存成功',
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
@@ -209,25 +211,55 @@ onMounted(async () => {
</n-flex>
<n-form-item-row :label="t('address_block_list')">
<n-select v-model:value="addressBlockList" filterable multiple tag
:placeholder="t('address_block_list_placeholder')" />
:placeholder="t('address_block_list_placeholder')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row>
<n-form-item-row :label="t('send_address_block_list')">
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
:placeholder="t('address_block_list_placeholder')" />
:placeholder="t('address_block_list_placeholder')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row>
<n-form-item-row :label="t('noLimitSendAddressList')">
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag
:placeholder="t('noLimitSendAddressList')" />
:placeholder="t('noLimitSendAddressList')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row>
<n-form-item-row :label="t('verified_address_list')">
<n-select v-model:value="verifiedAddressList" filterable multiple tag
:placeholder="t('verified_address_list')" />
:placeholder="t('verified_address_list')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row>
<n-form-item-row :label="t('fromBlockList')">
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')" />
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row>
<n-form-item-row :label="t('block_receive_unknow_address_email')">
<n-checkbox v-model:checked="emailRuleSettings.blockReceiveUnknowAddressEmail" />
<n-switch v-model:value="emailRuleSettings.blockReceiveUnknowAddressEmail" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('email_forwarding_config')">
<n-button @click="openEmailForwardingModal">{{ t('config') }}</n-button>

View File

@@ -77,7 +77,7 @@ onMounted(async () => {
</n-modal>
<n-card :bordered="false" embedded style="max-width: 600px;">
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
<n-checkbox v-model:checked="enablePrefix" />
<n-switch v-model:value="enablePrefix" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('address')">
<n-input-group>

View File

@@ -15,25 +15,29 @@ const { t } = useI18n({
init: 'Init',
successTip: 'Success',
status: 'Check Status',
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input user ID)',
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input Chat ID)',
enable: 'Enable',
telegramAllowList: 'Telegram Allow List(Manually input telegram user ID)',
telegramAllowList: 'Telegram Allow List(Manually input telegram Chat ID)',
manualInputPrompt: 'Type and press Enter to add',
save: 'Save',
miniAppUrl: 'Telegram Mini App URL',
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
globalMailPushList: 'Global Mail Push List',
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram Chat ID)',
globalMailPushList: 'Global Mail Push Chat ID List',
globalMailPushListTip: 'Support chat_id of private chat/group/channel. You can send a message to your bot, then visit this link to see chat_id, https://api.telegram.org/bot<Replace with your BOT TOKEN>/getUpdates',
},
zh: {
init: '初始化',
successTip: '成功',
status: '查看状态',
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID, 回车增加)',
enableTelegramAllowList: '启用 Telegram 白名单(手动输入 Chat ID, 回车增加)',
enable: '启用',
telegramAllowList: 'Telegram 白名单(手动输入用户 ID, 回车增加)',
telegramAllowList: 'Telegram 白名单(手动输入 Chat ID, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
save: '保存',
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID, 回车增加)',
globalMailPushList: '全局邮件推送用户列表',
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram Chat ID, 回车增加)',
globalMailPushList: '全局邮件推送 Chat ID 列表',
globalMailPushListTip: '支持对话/群组/频道的 Chat ID, 您可以发送一条消息给您的机器人,然后访问此链接来查看 chat_id, https://api.telegram.org/bot<这里替换成您的 BOT TOKEN>/getUpdates',
}
}
});
@@ -113,6 +117,17 @@ onMounted(async () => {
<template>
<div class="center">
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
<n-flex justify="end">
<n-button @click="fetchStatus" secondary>
{{ t('status') }}
</n-button>
<n-button @click="init" type="primary">
{{ t('init') }}
</n-button>
<n-button @click="saveSettings" type="primary">
{{ t('save') }}
</n-button>
</n-flex>
<n-card :bordered="false" embedded>
<n-form-item-row :label="t('enableTelegramAllowList')">
<n-input-group>
@@ -120,31 +135,41 @@ onMounted(async () => {
{{ t('enable') }}
</n-checkbox>
<n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;"
:placeholder="t('telegramAllowList')" />
:placeholder="t('telegramAllowList')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group>
</n-form-item-row>
<br />
<n-form-item-row :label="t('enableGlobalMailPush')">
<n-input-group>
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
{{ t('enable') }}
</n-checkbox>
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag
style="width: 80%;" :placeholder="t('globalMailPushList')" />
style="width: 80%;" :placeholder="t('globalMailPushList')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group>
<template #feedback>
<n-text depth="3">
{{ t('globalMailPushListTip') }}
</n-text>
</template>
</n-form-item-row>
<br />
<n-form-item-row :label="t('miniAppUrl')">
<n-input v-model:value="settings.miniAppUrl"></n-input>
</n-form-item-row>
<n-button @click="saveSettings" type="primary" block>
{{ t('save') }}
</n-button>
</n-card>
<n-button @click="init" type="primary" block>
{{ t('init') }}
</n-button>
<n-button @click="fetchStatus" secondary block>
{{ t('status') }}
</n-button>
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
</n-card>
</div>
@@ -157,8 +182,4 @@ onMounted(async () => {
place-items: center;
justify-content: center;
}
.n-button {
margin-top: 10px;
}
</style>

View File

@@ -21,6 +21,7 @@ const { t } = useI18n({
successTip: 'Save Success',
enable: 'Enable',
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
manualInputPrompt: 'Type and press Enter to add',
mailAllowList: 'Mail Address Allow List',
addOauth2: 'Add Oauth2',
name: 'Name',
@@ -33,6 +34,7 @@ const { t } = useI18n({
successTip: '保存成功',
enable: '启用',
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
mailAllowList: '邮件地址白名单',
addOauth2: '添加 Oauth2',
name: '名称',
@@ -184,7 +186,7 @@ onMounted(async () => {
</template>
</n-modal>
<n-card :bordered="false" embedded style="max-width: 600px;">
<n-alert :show-icon="false" type="warning" closable style="margin-bottom: 10px;">
<n-alert :show-icon="false" :bordered="false" type="warning" closable style="margin-bottom: 10px;">
{{ t("tip") }}
</n-alert>
<n-flex justify="end">
@@ -246,7 +248,13 @@ onMounted(async () => {
</n-checkbox>
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
multiple tag style="width: 80%;" :options="mailAllowOptions"
:placeholder="t('mailAllowList')" />
:placeholder="t('mailAllowList')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group>
</n-form-item-row>
</n-form>

View File

@@ -18,6 +18,7 @@ const { t } = useI18n({
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
verifyMailSender: 'Verify Mail Sender',
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
manualInputPrompt: 'Type and press Enter to add',
mailAllowList: 'Mail Address Allow List',
maxAddressCount: 'Maximum number of email addresses that can be binded',
},
@@ -29,6 +30,7 @@ const { t } = useI18n({
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
verifyMailSender: '验证邮件发送地址',
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
mailAllowList: '邮件地址白名单',
maxAddressCount: '可绑定最大邮箱地址数量',
}
@@ -83,9 +85,14 @@ onMounted(async () => {
<template>
<div class="center">
<n-card :bordered="false" embedded style="max-width: 600px;">
<n-flex justify="end">
<n-button @click="save" type="primary" :loading="loading">
{{ t('save') }}
</n-button>
</n-flex>
<n-form :model="userSettings">
<n-form-item-row :label="t('enableUserRegister')">
<n-checkbox v-model:checked="userSettings.enable" />
<n-switch v-model:value="userSettings.enable" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('enableMailVerify')">
<n-input-group>
@@ -103,7 +110,13 @@ onMounted(async () => {
</n-checkbox>
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
:placeholder="t('mailAllowList')" />
:placeholder="t('mailAllowList')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-input-group>
</n-form-item-row>
<n-form-item-row :label="t('maxAddressCount')">
@@ -112,9 +125,6 @@ onMounted(async () => {
:placeholder="t('maxAddressCount')" />
</n-input-group>
</n-form-item-row>
<n-button @click="save" type="primary" block :loading="loading">
{{ t('save') }}
</n-button>
</n-form>
</n-card>
</div>

View File

@@ -13,13 +13,17 @@ const { t } = useI18n({
messages: {
en: {
successTip: 'Success',
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook and enter)',
enableAllowList: 'Enable Allow List (Restrict webhook access to specific users)',
webhookAllowList: 'Webhook Allow List(Enter the mail address that is allowed to use webhook and enter)',
manualInputPrompt: 'Type and press Enter to add',
save: 'Save',
notEnabled: 'Webhook is not enabled',
},
zh: {
successTip: '成功',
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址, 回车增加)',
enableAllowList: '启用白名单 (限制 webhook 访问权限,只有白名单中的用户可以使用)',
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的邮箱地址, 回车增加)',
manualInputPrompt: '输入后按回车键添加',
save: '保存',
notEnabled: 'Webhook 未开启',
}
@@ -27,14 +31,16 @@ const { t } = useI18n({
});
class WebhookSettings {
enableAllowList: boolean;
allowList: string[];
constructor(allowList: string[]) {
constructor(enableAllowList: boolean, allowList: string[]) {
this.enableAllowList = enableAllowList;
this.allowList = allowList;
}
}
const webhookSettings = ref(new WebhookSettings([]))
const webhookSettings = ref(new WebhookSettings(false, []))
const webhookEnabled = ref(false)
const errorInfo = ref('')
@@ -68,13 +74,24 @@ onMounted(async () => {
<template>
<div class="center">
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
<n-flex justify="end">
<n-button @click="saveSettings" type="primary">
{{ t('save') }}
</n-button>
</n-flex>
<n-form-item-row :label="t('enableAllowList')">
<n-switch v-model:value="webhookSettings.enableAllowList" :round="false" />
</n-form-item-row>
<n-form-item-row :label="t('webhookAllowList')">
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
:placeholder="t('webhookAllowList')" />
:placeholder="t('webhookAllowList')">
<template #empty>
<n-text depth="3">
{{ t('manualInputPrompt') }}
</n-text>
</template>
</n-select>
</n-form-item-row>
<n-button @click="saveSettings" type="primary" block>
{{ t('save') }}
</n-button>
</n-card>
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
</div>

View File

@@ -26,7 +26,7 @@ onMounted(async () => {
<template>
<div class="center">
<n-card :bordered="false" embedded style="max-width: 600px; overflow: auto;">
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
</n-card>
</div>