feat: 实现域名绑定与授权 ip 功能 (#1089)

This commit is contained in:
ssongliu 2023-05-19 21:47:46 +08:00 committed by GitHub
parent c2f5908a9d
commit 8fd4060562
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 591 additions and 7 deletions

View file

@ -18,6 +18,8 @@ type SettingInfo struct {
ServerPort string `json:"serverPort"` ServerPort string `json:"serverPort"`
SSL string `json:"ssl"` SSL string `json:"ssl"`
SSLType string `json:"sslType"` SSLType string `json:"sslType"`
BindDomain string `json:"bindDomain"`
AllowIPs string `json:"allowIPs"`
SecurityEntrance string `json:"securityEntrance"` SecurityEntrance string `json:"securityEntrance"`
ExpirationDays string `json:"expirationDays"` ExpirationDays string `json:"expirationDays"`
ExpirationTime string `json:"expirationTime"` ExpirationTime string `json:"expirationTime"`

View file

@ -67,6 +67,12 @@ func (u *SettingService) Update(key, value string) error {
return err return err
} }
} }
if key == "BindDomain" {
global.CONF.System.BindDomain = value
}
if key == "AllowIPs" {
global.CONF.System.AllowIPs = value
}
if err := settingRepo.Update(key, value); err != nil { if err := settingRepo.Update(key, value); err != nil {
return err return err
} }

View file

@ -21,4 +21,6 @@ type System struct {
IsDemo bool `mapstructure:"is_demo"` IsDemo bool `mapstructure:"is_demo"`
AppRepo string `mapstructure:"app_repo"` AppRepo string `mapstructure:"app_repo"`
ChangeUserInfo bool `mapstructure:"change_user_info"` ChangeUserInfo bool `mapstructure:"change_user_info"`
AllowIPs string `mapstructure:"allow_ips"`
BindDomain string `mapstructure:"bind_domain"`
} }

View file

@ -14,6 +14,8 @@ const (
CodePasswordExpired = 405 CodePasswordExpired = 405
CodeAuth = 406 CodeAuth = 406
CodeGlobalLoading = 407 CodeGlobalLoading = 407
CodeErrIP = 408
CodeErrDomain = 409
CodeErrInternalServer = 500 CodeErrInternalServer = 500
CodeErrHeader = 406 CodeErrHeader = 406
) )

View file

@ -26,6 +26,18 @@ func Init() {
} }
global.CONF.System.SSL = sslSetting.Value global.CONF.System.SSL = sslSetting.Value
ipsSetting, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs"))
if err != nil {
global.LOG.Errorf("load allow ips from setting failed, err: %v", err)
}
global.CONF.System.AllowIPs = ipsSetting.Value
domainSetting, err := settingRepo.Get(settingRepo.WithByKey("BindDomain"))
if err != nil {
global.LOG.Errorf("load bind domain from setting failed, err: %v", err)
}
global.CONF.System.BindDomain = domainSetting.Value
if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil { if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil {
_ = settingRepo.Create("SystemStatus", "Free") _ = settingRepo.Create("SystemStatus", "Free")
} }

View file

@ -28,6 +28,7 @@ func Init() {
migrations.AddEntranceAndSSL, migrations.AddEntranceAndSSL,
migrations.UpdateTableSetting, migrations.UpdateTableSetting,
migrations.UpdateTableAppDetail, migrations.UpdateTableAppDetail,
migrations.AddBindAndAllowIPs,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View file

@ -335,3 +335,16 @@ var UpdateTableAppDetail = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddBindAndAllowIPs = &gormigrate.Migration{
ID: "20230414-add-bind-and-allow",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "BindDomain", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "AllowIPs", Value: ""}).Error; err != nil {
return err
}
return nil
},
}

View file

@ -63,6 +63,8 @@ func Routers() *gin.Engine {
}) })
} }
PrivateGroup := Router.Group("/api/v1") PrivateGroup := Router.Group("/api/v1")
PrivateGroup.Use(middleware.WhiteAllow())
PrivateGroup.Use(middleware.BindDomain())
PrivateGroup.Use(middleware.GlobalLoading()) PrivateGroup.Use(middleware.GlobalLoading())
{ {
systemRouter.InitBaseRouter(PrivateGroup) systemRouter.InitBaseRouter(PrivateGroup)

View file

@ -0,0 +1,31 @@
package middleware
import (
"errors"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin"
)
func BindDomain() gin.HandlerFunc {
return func(c *gin.Context) {
if len(global.CONF.System.BindDomain) == 0 {
c.Next()
return
}
domains := c.Request.Host
parts := strings.Split(c.Request.Host, ":")
if len(parts) > 0 {
domains = parts[0]
}
if domains != global.CONF.System.BindDomain {
helper.ErrorWithDetail(c, constant.CodeErrDomain, constant.ErrTypeInternalServer, errors.New("domain not allowed"))
return
}
c.Next()
}
}

View file

@ -0,0 +1,28 @@
package middleware
import (
"errors"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin"
)
func WhiteAllow() gin.HandlerFunc {
return func(c *gin.Context) {
if len(global.CONF.System.AllowIPs) == 0 {
c.Next()
return
}
clientIP := c.ClientIP()
for _, ip := range strings.Split(global.CONF.System.AllowIPs, ",") {
if len(ip) != 0 && ip == clientIP {
c.Next()
return
}
}
helper.ErrorWithDetail(c, constant.CodeErrIP, constant.ErrTypeInternalServer, errors.New("IP address not allowed"))
}
}

View file

@ -71,6 +71,8 @@ declare module 'vue' {
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload']
Err_domain: typeof import('./src/components/error-message/err_domain.vue')['default']
Err_ip: typeof import('./src/components/error-message/err_ip.vue')['default']
FileList: typeof import('./src/components/file-list/index.vue')['default'] FileList: typeof import('./src/components/file-list/index.vue')['default']
FileRole: typeof import('./src/components/file-role/index.vue')['default'] FileRole: typeof import('./src/components/file-role/index.vue')['default']
Footer: typeof import('./src/components/app-layout/footer/index.vue')['default'] Footer: typeof import('./src/components/app-layout/footer/index.vue')['default']

View file

@ -49,9 +49,21 @@ class RequestHttp {
}); });
return Promise.reject(data); return Promise.reject(data);
} }
if (data.code == ResultEnum.EXPIRED) { if (data.code == ResultEnum.ERRIP) {
router.push({ name: 'Expired' }); globalStore.setLogStatus(false);
return data; router.push({
name: 'entrance',
params: { code: 'err-ip' },
});
return Promise.reject(data);
}
if (data.code == ResultEnum.ERRDOMAIN) {
globalStore.setLogStatus(false);
router.push({
name: 'entrance',
params: { code: 'err-domain' },
});
return Promise.reject(data);
} }
if (data.code == ResultEnum.ERRGLOBALLOADDING) { if (data.code == ResultEnum.ERRGLOBALLOADDING) {
globalStore.setGlobalLoading(true); globalStore.setGlobalLoading(true);

View file

@ -17,6 +17,8 @@ export namespace Setting {
serverPort: number; serverPort: number;
ssl: string; ssl: string;
sslType: string; sslType: string;
allowIPs: string;
bindDomain: string;
securityEntrance: string; securityEntrance: string;
expirationDays: number; expirationDays: number;
expirationTime: string; expirationTime: string;

View file

@ -0,0 +1,66 @@
<template>
<div class="not-container">
<img src="@/assets/images/unsafe.svg" class="not-img" alt="404" />
<div class="not-detail">
<h2>{{ $t('commons.login.notSafe') }}</h2>
<h4>{{ $t('commons.login.errDomain1') }}</h4>
<div>
<h4>{{ $t('commons.login.errHelper') }} 1pctl reset-domain</h4>
<div style="cursor: pointer; float: left">
<el-icon color="#409EFC" style="margin-left: 5px; margin-top: 33px" :size="18" @click="onCopy()">
<DocumentCopy />
</el-icon>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="404">
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const onCopy = () => {
let input = document.createElement('input');
input.value = '1pctl reset-domain';
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
};
</script>
<style scoped lang="scss">
.not-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.not-img {
margin-top: 300px;
}
.not-detail {
margin-top: 300px;
display: flex;
flex-direction: column;
h2,
h4 {
padding: 0;
margin: 0;
}
h2 {
font-size: 60px;
color: #434e59;
}
h4 {
margin: 30px 0 20px;
float: left;
font-size: 19px;
font-weight: normal;
color: #848587;
}
}
}
</style>

View file

@ -0,0 +1,65 @@
<template>
<div class="not-container">
<img src="@/assets/images/unsafe.svg" class="not-img" alt="404" />
<div class="not-detail">
<h2>{{ $t('commons.login.notSafe') }}</h2>
<h4>{{ $t('commons.login.errIP1') }}</h4>
<div>
<h4>{{ $t('commons.login.errHelper') }} 1pctl reset-ips</h4>
<div style="cursor: pointer; float: left">
<el-icon color="#409EFC" style="margin-left: 5px; margin-top: 33px" :size="18" @click="onCopy()">
<DocumentCopy />
</el-icon>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="404">
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const onCopy = () => {
let input = document.createElement('input');
input.value = '1pctl reset-ips';
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
};
</script>
<style scoped lang="scss">
.not-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
.not-img {
margin-top: 300px;
}
.not-detail {
margin-top: 300px;
display: flex;
flex-direction: column;
h2,
h4 {
padding: 0;
margin: 0;
}
h2 {
font-size: 60px;
color: #434e59;
}
h4 {
margin: 30px 0 20px;
float: left;
font-size: 19px;
font-weight: normal;
color: #848587;
}
}
}
</style>

View file

@ -7,6 +7,8 @@ export enum ResultEnum {
EXPIRED = 405, EXPIRED = 405,
ERRAUTH = 406, ERRAUTH = 406,
ERRGLOBALLOADDING = 407, ERRGLOBALLOADDING = 407,
ERRIP = 408,
ERRDOMAIN = 409,
TIMEOUT = 20000, TIMEOUT = 20000,
TYPE = 'success', TYPE = 'success',
} }

View file

@ -115,6 +115,9 @@ const message = {
notSafe: 'Access Denied', notSafe: 'Access Denied',
safeEntrance1: 'The secure login has been enabled in the current environment', safeEntrance1: 'The secure login has been enabled in the current environment',
safeEntrance2: 'Enter the following command on the SSH terminal to view the panel entry: 1pctl user-info', safeEntrance2: 'Enter the following command on the SSH terminal to view the panel entry: 1pctl user-info',
errIP1: 'Authorized IP address access is enabled in the current environment',
errDomain1: 'Access domain name binding is enabled in the current environment',
errHelper: 'To reset the binding information, run the following command on the SSH terminal: ',
codeInput: 'Please enter the 6-digit verification code of the MFA validator', codeInput: 'Please enter the 6-digit verification code of the MFA validator',
mfaTitle: 'MFA Certification', mfaTitle: 'MFA Certification',
mfaCode: 'MFA verification code', mfaCode: 'MFA verification code',
@ -933,6 +936,19 @@ const message = {
complexity: 'Complexity verification', complexity: 'Complexity verification',
complexityHelper: complexityHelper:
'The password must contain at least eight characters and contain at least three uppercase letters, lowercase letters, digits, and special characters', 'The password must contain at least eight characters and contain at least three uppercase letters, lowercase letters, digits, and special characters',
bindDomain: 'Bind domain',
bindDomainHelper:
'After the domain binding, only the domain in the setting can be used to access 1Panel service',
bindDomainHelper1: 'If the binding domain is empty, the binding of the domain is cancelled',
bindDomainWarnning:
'If the binding domain is empty, the binding of the domain is cancelled. Do you want to continue?',
allowIPs: 'Authorized IP',
allowIPsHelper:
'After setting the authorized IP address, only the IP address in the setting can access the 1Panel service',
allowIPsWarnning:
'设After setting the authorized IP address, only the IP address in the setting can access the 1Panel service. Do you want to continue?',
allowIPsHelper1: 'If the authorized IP address is empty, the authorized IP address is canceled',
mfa: 'MFA', mfa: 'MFA',
mfaAlert: mfaAlert:
'MFA password is generated based on the current time. Please ensure that the server time is synchronized.', 'MFA password is generated based on the current time. Please ensure that the server time is synchronized.',

View file

@ -119,6 +119,9 @@ const message = {
notSafe: '暂无权限访问', notSafe: '暂无权限访问',
safeEntrance1: '当前环境已经开启了安全入口登录', safeEntrance1: '当前环境已经开启了安全入口登录',
safeEntrance2: ' SSH 终端输入以下命令来查看面板入口: 1pctl user-info', safeEntrance2: ' SSH 终端输入以下命令来查看面板入口: 1pctl user-info',
errIP1: '当前环境已经开启了授权 IP 访问',
errDomain1: '当前环境已经开启了访问域名绑定',
errHelper: '可在 SSH 终端输入以下命令来重置绑定信息: ',
codeInput: '请输入 MFA 验证器的 6 位验证码', codeInput: '请输入 MFA 验证器的 6 位验证码',
mfaTitle: 'MFA 认证', mfaTitle: 'MFA 认证',
mfaCode: 'MFA 验证码', mfaCode: 'MFA 验证码',
@ -965,6 +968,14 @@ const message = {
timeoutHelper: ' {0} 天后 面板密码即将过期过期后需要重新设置密码', timeoutHelper: ' {0} 天后 面板密码即将过期过期后需要重新设置密码',
complexity: '密码复杂度验证', complexity: '密码复杂度验证',
complexityHelper: '开启后密码必须满足密码长度大于 8 位且包含字母数字及特殊字符', complexityHelper: '开启后密码必须满足密码长度大于 8 位且包含字母数字及特殊字符',
bindDomain: '域名绑定',
bindDomainHelper: '设置域名绑定后仅能通过设置中域名访问 1Panel 服务',
bindDomainHelper1: '绑定域名为空时则取消域名绑定',
bindDomainWarnning: '设置域名绑定后仅能通过设置中域名访问 1Panel 服务是否继续',
allowIPs: '授权 IP',
allowIPsHelper: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务',
allowIPsWarnning: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务是否继续',
allowIPsHelper1: '授权 IP 为空时则取消授权 IP',
mfa: '两步验证', mfa: '两步验证',
mfaAlert: '两步验证密码是基于当前时间生成请确保服务器时间已同步', mfaAlert: '两步验证密码是基于当前时间生成请确保服务器时间已同步',
mfaHelper: '开启后会验证手机应用验证码', mfaHelper: '开启后会验证手机应用验证码',

View file

@ -158,6 +158,9 @@ export function getIcon(extention: string): string {
} }
export function checkIp(value: string): boolean { export function checkIp(value: string): boolean {
if (value === '') {
return true;
}
const reg = const reg =
/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
if (!reg.test(value) && value !== '') { if (!reg.test(value) && value !== '') {

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="login-backgroud" v-if="isSafety"> <div class="login-backgroud" v-if="isSafety && !isErr">
<div class="login-wrapper"> <div class="login-wrapper">
<div :class="screenWidth > 1110 ? 'left inline-block' : ''"> <div :class="screenWidth > 1110 ? 'left inline-block' : ''">
<div class="login-title"> <div class="login-title">
@ -15,9 +15,15 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="!isSafety"> <div v-if="!isSafety && !isErr">
<UnSafe /> <UnSafe />
</div> </div>
<div v-if="isErr && mySafetyCode.code === 'err-ip'">
<ErrIP />
</div>
<div v-if="isErr && mySafetyCode.code === 'err-domain'">
<ErrDomain />
</div>
</div> </div>
</template> </template>
@ -25,12 +31,15 @@
import { checkIsSafety } from '@/api/modules/auth'; import { checkIsSafety } from '@/api/modules/auth';
import LoginForm from '../components/login-form.vue'; import LoginForm from '../components/login-form.vue';
import UnSafe from '@/components/error-message/unsafe.vue'; import UnSafe from '@/components/error-message/unsafe.vue';
import ErrIP from '@/components/error-message/err_ip.vue';
import ErrDomain from '@/components/error-message/err_domain.vue';
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const isSafety = ref(true); const isSafety = ref(true);
const screenWidth = ref(null); const screenWidth = ref(null);
const isErr = ref();
const mySafetyCode = defineProps({ const mySafetyCode = defineProps({
code: { code: {
@ -41,7 +50,13 @@ const mySafetyCode = defineProps({
}); });
const getStatus = async () => { const getStatus = async () => {
if (mySafetyCode.code === 'err-ip' || mySafetyCode.code === 'err-domain') {
isErr.value = true;
}
const res = await checkIsSafety(mySafetyCode.code); const res = await checkIsSafety(mySafetyCode.code);
if (mySafetyCode.code === 'err-ip' || mySafetyCode.code === 'err-domain') {
isErr.value = false;
}
isSafety.value = res.data; isSafety.value = res.data;
if (isSafety.value) { if (isSafety.value) {
globalStore.entrance = mySafetyCode.code; globalStore.entrance = mySafetyCode.code;

View file

@ -0,0 +1,133 @@
<template>
<div>
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('setting.allowIPs')" :back="handleClose" />
</template>
<el-form label-position="top" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item>
<table style="width: 100%" class="tab-table">
<tr v-if="allowIPs.length !== 0">
<th scope="col" width="90%" align="left">
<label>IP</label>
</th>
<th align="left"></th>
</tr>
<tr v-for="(row, index) in allowIPs" :key="index">
<td width="90%">
<el-input
:placeholder="$t('container.serverExample')"
style="width: 100%"
v-model="row.value"
/>
</td>
<td>
<el-button link style="font-size: 10px" @click="handlePortsDelete(index)">
{{ $t('commons.button.delete') }}
</el-button>
</td>
</tr>
<tr>
<td align="left">
<el-button @click="handlePortsAdd()">{{ $t('commons.button.add') }}</el-button>
</td>
</tr>
</table>
<span class="input-help">{{ $t('setting.allowIPsHelper1') }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSavePort()">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import { updateSetting } from '@/api/modules/setting';
import { ElMessageBox } from 'element-plus';
import { checkIp } from '@/utils/util';
const emit = defineEmits<{ (e: 'search'): void }>();
const allowIPs = ref();
interface DialogProps {
allowIPs: string;
}
const drawerVisiable = ref();
const loading = ref();
const acceptParams = (params: DialogProps): void => {
allowIPs.value = [];
if (params.allowIPs) {
for (const ip of params.allowIPs.split(',')) {
if (ip) {
allowIPs.value.push({ value: ip });
}
}
}
drawerVisiable.value = true;
};
const handlePortsAdd = () => {
let item = {
value: '',
};
allowIPs.value.push(item);
};
const handlePortsDelete = (index: number) => {
allowIPs.value.splice(index, 1);
};
const onSavePort = async () => {
let allows = '';
if (allowIPs.value.length !== 0) {
for (const ip of allowIPs.value) {
if (checkIp(ip.value)) {
MsgError(i18n.global.t('firewall.addressFormatError'));
return false;
}
allows += ip.value + ',';
}
allows = allows.substring(0, allows.length - 1);
}
ElMessageBox.confirm(i18n.global.t('setting.allowIPsHelper'), i18n.global.t('setting.allowIPs'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await updateSetting({ key: 'AllowIPs', value: allows })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
handleClose();
})
.catch(() => {
loading.value = false;
});
});
};
const handleClose = () => {
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>

View file

@ -0,0 +1,106 @@
<template>
<div>
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('setting.bindDomain')" :back="handleClose" />
</template>
<el-form
ref="formRef"
label-position="top"
:rules="rules"
:model="form"
@submit.prevent
v-loading="loading"
>
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('setting.bindDomain')" prop="bindDomain">
<el-input clearable v-model="form.bindDomain" />
<span class="input-help">{{ $t('setting.bindDomainHelper1') }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSavePort(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { updateSetting } from '@/api/modules/setting';
import { ElMessageBox, FormInstance } from 'element-plus';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
bindDomain: string;
}
const drawerVisiable = ref();
const loading = ref();
const form = reactive({
bindDomain: '',
});
const rules = reactive({
bindDomain: [{ validator: checkSecurityEntrance, trigger: 'blur' }],
});
function checkSecurityEntrance(rule: any, value: any, callback: any) {
if (form.bindDomain !== '') {
const reg =
/^([\w\u4e00-\u9fa5\-\*]{1,100}\.){1,10}([\w\u4e00-\u9fa5\-]{1,24}|[\w\u4e00-\u9fa5\-]{1,24}\.[\w\u4e00-\u9fa5\-]{1,24})$/;
if (!reg.test(form.bindDomain)) {
return callback(new Error(i18n.global.t('commons.rule.domain')));
}
}
callback();
}
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.bindDomain = params.bindDomain;
drawerVisiable.value = true;
};
const onSavePort = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(i18n.global.t('setting.bindDomainWarnning'), i18n.global.t('setting.bindDomain'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await updateSetting({ key: 'BindDomain', value: form.bindDomain })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
handleClose();
})
.catch(() => {
loading.value = false;
});
});
});
};
const handleClose = () => {
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>

View file

@ -39,6 +39,42 @@
<span class="input-help">{{ $t('setting.entranceHelper') }}</span> <span class="input-help">{{ $t('setting.entranceHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('setting.allowIPs')">
<el-input v-if="form.allowIPs" disabled v-model="form.allowIPs">
<template #append>
<el-button @click="onChangeAllowIPs" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
<el-input disabled v-if="!form.allowIPs" v-model="unset">
<template #append>
<el-button @click="onChangeAllowIPs" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('setting.allowIPsHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('setting.bindDomain')">
<el-input disabled v-if="form.bindDomain" v-model="form.bindDomain">
<template #append>
<el-button @click="onChangeBindDomain" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
<el-input disabled v-if="!form.bindDomain" v-model="unset">
<template #append>
<el-button @click="onChangeBindDomain" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('setting.bindDomainHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('setting.expirationTime')" prop="expirationTime"> <el-form-item :label="$t('setting.expirationTime')" prop="expirationTime">
<el-input disabled v-model="form.expirationTime"> <el-input disabled v-model="form.expirationTime">
<template #append> <template #append>
@ -111,6 +147,8 @@
<SSLSetting ref="sslRef" @search="search" /> <SSLSetting ref="sslRef" @search="search" />
<EntranceSetting ref="entranceRef" @search="search" /> <EntranceSetting ref="entranceRef" @search="search" />
<TimeoutSetting ref="timeoutref" @search="search" /> <TimeoutSetting ref="timeoutref" @search="search" />
<DomainSetting ref="domainRef" @search="search" />
<AllowIPsSetting ref="allowIPsRef" @search="search" />
</div> </div>
</template> </template>
@ -123,6 +161,8 @@ import SSLSetting from '@/views/setting/safe/ssl/index.vue';
import MfaSetting from '@/views/setting/safe/mfa/index.vue'; import MfaSetting from '@/views/setting/safe/mfa/index.vue';
import TimeoutSetting from '@/views/setting/safe/timeout/index.vue'; import TimeoutSetting from '@/views/setting/safe/timeout/index.vue';
import EntranceSetting from '@/views/setting/safe/entrance/index.vue'; import EntranceSetting from '@/views/setting/safe/entrance/index.vue';
import DomainSetting from '@/views/setting/safe/domain/index.vue';
import AllowIPsSetting from '@/views/setting/safe/allowips/index.vue';
import { updateSetting, getSettingInfo, getSystemAvailable, updateSSL, loadSSLInfo } from '@/api/modules/setting'; import { updateSetting, getSettingInfo, getSystemAvailable, updateSSL, loadSSLInfo } from '@/api/modules/setting';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
@ -136,6 +176,8 @@ const mfaRef = ref();
const sslRef = ref(); const sslRef = ref();
const sslInfo = ref<Setting.SSLInfo>(); const sslInfo = ref<Setting.SSLInfo>();
const domainRef = ref();
const allowIPsRef = ref();
const form = reactive({ const form = reactive({
serverPort: 9999, serverPort: 9999,
@ -146,6 +188,8 @@ const form = reactive({
expirationTime: '', expirationTime: '',
complexityVerification: 'disable', complexityVerification: 'disable',
mfaStatus: 'disable', mfaStatus: 'disable',
allowIPs: '',
bindDomain: '',
}); });
const unset = ref(i18n.global.t('setting.unSetting')); const unset = ref(i18n.global.t('setting.unSetting'));
@ -163,6 +207,8 @@ const search = async () => {
form.expirationTime = res.data.expirationTime; form.expirationTime = res.data.expirationTime;
form.complexityVerification = res.data.complexityVerification; form.complexityVerification = res.data.complexityVerification;
form.mfaStatus = res.data.mfaStatus; form.mfaStatus = res.data.mfaStatus;
form.allowIPs = res.data.allowIPs || '';
form.bindDomain = res.data.bindDomain;
}; };
const onSaveComplexity = async () => { const onSaveComplexity = async () => {
@ -199,12 +245,18 @@ const handleMFA = async () => {
}); });
}; };
const onChangeEntrance = async () => { const onChangeEntrance = () => {
entranceRef.value.acceptParams({ securityEntrance: form.securityEntrance }); entranceRef.value.acceptParams({ securityEntrance: form.securityEntrance });
}; };
const onChangePort = async () => { const onChangePort = () => {
portRef.value.acceptParams({ serverPort: form.serverPort }); portRef.value.acceptParams({ serverPort: form.serverPort });
}; };
const onChangeBindDomain = () => {
domainRef.value.acceptParams({ bindDomain: form.bindDomain });
};
const onChangeAllowIPs = () => {
allowIPsRef.value.acceptParams({ allowIPs: form.allowIPs });
};
const handleSSL = async () => { const handleSSL = async () => {
if (form.ssl === 'enable') { if (form.ssl === 'enable') {
let params = { let params = {