From 2975cf6d5a37628b50ef920ba327520e513a7af9 Mon Sep 17 00:00:00 2001 From: ssongliu Date: Wed, 17 May 2023 18:45:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20sshd=20=E6=94=AF=E6=8C=81=E9=87=8D?= =?UTF-8?q?=E5=90=AF=E3=80=81=E5=81=9C=E6=AD=A2=E7=AD=89=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=8A=B6=E6=80=81=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/ssh.go | 26 +++ backend/app/dto/common_req.go | 4 + backend/app/dto/ssh.go | 11 +- backend/app/service/ssh.go | 39 ++++- backend/router/ro_host.go | 1 + frontend/src/api/interface/host.ts | 1 + frontend/src/api/modules/host.ts | 3 + .../src/components/error-message/unsafe.vue | 19 +-- frontend/src/lang/modules/en.ts | 5 +- frontend/src/lang/modules/zh.ts | 7 +- frontend/src/routers/modules/host.ts | 38 ++--- frontend/src/views/database/mysql/index.vue | 25 ++- .../database/mysql/root-password/index.vue | 25 +-- .../views/database/redis/password/index.vue | 25 +-- frontend/src/views/host/ssh/log/log.vue | 9 +- .../src/views/host/ssh/ssh/address/index.vue | 103 ++++++++++++ frontend/src/views/host/ssh/ssh/index.vue | 150 ++++++++++++++---- .../src/views/host/ssh/ssh/port/index.vue | 99 ++++++++++++ .../src/views/host/ssh/ssh/pubkey/index.vue | 28 ++-- .../src/views/host/ssh/ssh/root/index.vue | 119 ++++++++++++++ frontend/src/views/setting/safe/mfa/index.vue | 19 +-- 21 files changed, 617 insertions(+), 139 deletions(-) create mode 100644 frontend/src/views/host/ssh/ssh/address/index.vue create mode 100644 frontend/src/views/host/ssh/ssh/port/index.vue create mode 100644 frontend/src/views/host/ssh/ssh/root/index.vue diff --git a/backend/app/api/v1/ssh.go b/backend/app/api/v1/ssh.go index 1376e21bb..67bfebc40 100644 --- a/backend/app/api/v1/ssh.go +++ b/backend/app/api/v1/ssh.go @@ -23,6 +23,32 @@ func (b *BaseApi) GetSSHInfo(c *gin.Context) { helper.SuccessWithData(c, info) } +// @Tags SSH +// @Summary Operate ssh +// @Description 修改 SSH 服务状态 +// @Accept json +// @Param request body dto.Operate true "request" +// @Security ApiKeyAuth +// @Router /host/ssh/operate [post] +// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"} +func (b *BaseApi) OperateSSH(c *gin.Context) { + var req dto.Operate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := sshService.OperateSSH(req.Operation); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + // @Tags SSH // @Summary Update host ssh setting // @Description 更新 SSH 配置 diff --git a/backend/app/dto/common_req.go b/backend/app/dto/common_req.go index 5b6a0b532..014c06e2d 100644 --- a/backend/app/dto/common_req.go +++ b/backend/app/dto/common_req.go @@ -23,6 +23,10 @@ type OperateByID struct { ID uint `json:"id" validate:"required"` } +type Operate struct { + Operation string `json:"operation" validate:"required"` +} + type BatchDeleteReq struct { Ids []uint `json:"ids" validate:"required"` } diff --git a/backend/app/dto/ssh.go b/backend/app/dto/ssh.go index 11ea751bb..6686e031b 100644 --- a/backend/app/dto/ssh.go +++ b/backend/app/dto/ssh.go @@ -3,12 +3,13 @@ package dto import "time" type SSHInfo struct { - Port string `json:"port" validate:"required,number,max=65535,min=1"` + Status string `json:"status"` + Port string `json:"port"` ListenAddress string `json:"listenAddress"` - PasswordAuthentication string `json:"passwordAuthentication" validate:"required,oneof=yes no"` - PubkeyAuthentication string `json:"pubkeyAuthentication" validate:"required,oneof=yes no"` - PermitRootLogin string `json:"permitRootLogin" validate:"required,oneof=yes no without-password forced-commands-only"` - UseDNS string `json:"useDNS" validate:"required,oneof=yes no"` + PasswordAuthentication string `json:"passwordAuthentication"` + PubkeyAuthentication string `json:"pubkeyAuthentication"` + PermitRootLogin string `json:"permitRootLogin"` + UseDNS string `json:"useDNS"` } type GenerateSSH struct { diff --git a/backend/app/service/ssh.go b/backend/app/service/ssh.go index 18dbb32ae..3676911e4 100644 --- a/backend/app/service/ssh.go +++ b/backend/app/service/ssh.go @@ -24,6 +24,7 @@ type SSHService struct{} type ISSHService interface { GetSSHInfo() (*dto.SSHInfo, error) + OperateSSH(operation string) error UpdateByFile(value string) error Update(key, value string) error GenerateSSH(req dto.GenerateSSH) error @@ -37,6 +38,7 @@ func NewISSHService() ISSHService { func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { data := dto.SSHInfo{ + Status: constant.StatusDisable, Port: "22", ListenAddress: "0.0.0.0", PasswordAuthentication: "yes", @@ -44,6 +46,25 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { PermitRootLogin: "yes", UseDNS: "yes", } + sudo := "" + hasSudo := cmd.HasNoPasswordSudo() + if hasSudo { + sudo = "sudo" + } + stdout, err := cmd.Execf("%s systemctl status sshd", sudo) + if err != nil { + return &data, nil + } + stdLines := strings.Split(stdout, "\n") + for _, stdline := range stdLines { + if strings.Contains(stdline, "active (running)") { + data.Status = constant.StatusEnable + break + } + } + if data.Status == constant.StatusDisable { + return &data, nil + } sshConf, err := os.ReadFile(sshPath) if err != nil { return &data, err @@ -72,6 +93,22 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { return &data, err } +func (u *SSHService) OperateSSH(operation string) error { + if operation == "start" || operation == "stop" || operation == "restart" { + sudo := "" + hasSudo := cmd.HasNoPasswordSudo() + if hasSudo { + sudo = "sudo" + } + stdout, err := cmd.Execf("%s systemctl %s sshd", sudo, operation) + if err != nil { + return fmt.Errorf("%s sshd failed, stdout: %s, err: %v", operation, stdout, err) + } + return nil + } + return fmt.Errorf("not support such operation: %s", operation) +} + func (u *SSHService) Update(key, value string) error { sshConf, err := os.ReadFile(sshPath) if err != nil { @@ -96,7 +133,7 @@ func (u *SSHService) Update(key, value string) error { sudo = "sudo" } if key == "Port" { - stdout, _ := cmd.Exec("getenforce") + stdout, _ := cmd.Execf("%s getenforce", sudo) if stdout == "Enforcing\n" { _, _ = cmd.Execf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, value) } diff --git a/backend/router/ro_host.go b/backend/router/ro_host.go index 3e75a5d04..8d98bc3fb 100644 --- a/backend/router/ro_host.go +++ b/backend/router/ro_host.go @@ -41,6 +41,7 @@ func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) { hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret) hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs) hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile) + hostRouter.POST("/ssh/operate", baseApi.OperateSSH) hostRouter.GET("/command", baseApi.ListCommand) hostRouter.POST("/command", baseApi.CreateCommand) diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index 9d353475e..68e0ad117 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -105,6 +105,7 @@ export namespace Host { } export interface SSHInfo { + status: string; port: string; listenAddress: string; passwordAuthentication: string; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index b2ea23cee..9c2c4f88c 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -101,6 +101,9 @@ export const batchOperateRule = (params: Host.BatchRule) => { export const getSSHInfo = () => { return http.post(`/hosts/ssh/search`); }; +export const operateSSH = (operation: string) => { + return http.post(`/hosts/ssh/operate`, { operation: operation }); +}; export const updateSSH = (key: string, value: string) => { return http.post(`/hosts/ssh/update`, { key: key, value: value }); }; diff --git a/frontend/src/components/error-message/unsafe.vue b/frontend/src/components/error-message/unsafe.vue index 92ea972bf..9e5fbdf20 100644 --- a/frontend/src/components/error-message/unsafe.vue +++ b/frontend/src/components/error-message/unsafe.vue @@ -18,16 +18,17 @@ diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 68084e84e..901542f27 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -816,8 +816,9 @@ const message = { searchHelper: 'Support wildcards such as *', }, ssh: { + sshOperate: 'Operation [{0}] on the SSH service is performed. Do you want to continue?', sshChange: 'SSH Setting', - sshChangeHelper: 'Are you sure to change the SSH {0} configuration to {1}?', + sshChangeHelper: 'This action changed {0} to [{1}]. Do you want to continue?', sshFileChangeHelper: 'Modifying the configuration file may cause service availability. Exercise caution when performing this operation. Do you want to continue?', port: 'Port', @@ -837,7 +838,7 @@ const message = { key: 'Key', pubkey: 'Key info', encryptionMode: 'Encryption mode', - passwordHelper: 'Please enter a 6-10 digit encryption password', + passwordHelper: 'Can contain 6 to 10 digits and English cases', generate: 'Generate key', reGenerate: 'Regenerate key', keyAuthHelper: 'Whether to enable key authentication. This parameter is enabled by default.', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index a029067c8..1340903c4 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -821,8 +821,9 @@ const message = { searchHelper: '支持 * 等通配符', }, ssh: { + sshOperate: '对 SSH 服务进行 [{0}] 操作,是否继续?', sshChange: 'SSH 配置修改', - sshChangeHelper: '确认将 SSH {0} 配置修改为 {1} 吗?', + sshChangeHelper: '此操作将 {0} 修改为 [{1}] ,是否继续?', sshFileChangeHelper: '直接修改配置文件可能会导致服务不可用,请谨慎操作,是否继续?', port: '连接端口', portHelper: '指定 SSH 服务监听的端口号,默认为 22。', @@ -833,14 +834,14 @@ const message = { rootHelper1: '允许 SSH 登录', rootHelper2: '禁止 SSH 登录', rootHelper3: '仅允许密钥登录', - rootHelper4: '仅允许执行预先定义的命令,不能进行其他操作。', + rootHelper4: '仅允许执行预先定义的命令,不能进行其他操作', passwordAuthentication: '密码认证', pwdAuthHelper: '是否启用密码认证,默认启用。', pubkeyAuthentication: '密钥认证', key: '密钥', pubkey: '密钥信息', encryptionMode: '加密方式', - passwordHelper: '请输入 6-10 位加密密码', + passwordHelper: '支持大小写英文、数字,长度6-10', generate: '生成密钥', reGenerate: '重新生成密钥', keyAuthHelper: '是否启用密钥认证,默认启用。', diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index 649fcc969..e65264e68 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -50,6 +50,25 @@ const hostRouter = { requiresAuth: false, }, }, + { + path: '/hosts/firewall/port', + name: 'FirewallPort', + component: () => import('@/views/host/firewall/port/index.vue'), + meta: { + title: 'menu.firewall', + requiresAuth: false, + }, + }, + { + path: '/hosts/firewall/ip', + name: 'FirewallIP', + component: () => import('@/views/host/firewall/ip/index.vue'), + hidden: true, + meta: { + activeMenu: '/hosts/firewall/port', + requiresAuth: false, + }, + }, { path: '/hosts/ssh/ssh', name: 'SSH', @@ -71,25 +90,6 @@ const hostRouter = { requiresAuth: false, }, }, - { - path: '/hosts/firewall/port', - name: 'FirewallPort', - component: () => import('@/views/host/firewall/port/index.vue'), - meta: { - title: 'menu.firewall', - requiresAuth: false, - }, - }, - { - path: '/hosts/firewall/ip', - name: 'FirewallIP', - component: () => import('@/views/host/firewall/ip/index.vue'), - hidden: true, - meta: { - activeMenu: '/hosts/firewall/port', - requiresAuth: false, - }, - }, ], }; diff --git a/frontend/src/views/database/mysql/index.vue b/frontend/src/views/database/mysql/index.vue index aae758984..a767f37ff 100644 --- a/frontend/src/views/database/mysql/index.vue +++ b/frontend/src/views/database/mysql/index.vue @@ -73,11 +73,7 @@
- +
@@ -168,7 +164,9 @@ import { Database } from '@/api/interface/database'; import { App } from '@/api/interface/app'; import { GetAppPort } from '@/api/modules/app'; import router from '@/routers'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; +import useClipboard from 'vue-clipboard3'; +const { toClipboard } = useClipboard(); const loading = ref(false); const maskShow = ref(true); @@ -290,14 +288,13 @@ const checkExist = (data: App.CheckInstalled) => { } }; -const onCopyPassword = (row: Database.MysqlDBInfo) => { - let input = document.createElement('input'); - input.value = row.password; - document.body.appendChild(input); - input.select(); - document.execCommand('Copy'); - document.body.removeChild(input); - MsgSuccess(i18n.global.t('commons.msg.copySuccess')); +const onCopy = async (row: any) => { + try { + await toClipboard(row.password); + MsgSuccess(i18n.global.t('commons.msg.copySuccess')); + } catch (e) { + MsgError(i18n.global.t('commons.msg.copyfailed')); + } }; const onDelete = async (row: Database.MysqlDBInfo) => { diff --git a/frontend/src/views/database/mysql/root-password/index.vue b/frontend/src/views/database/mysql/root-password/index.vue index f1c416b09..095ca1f87 100644 --- a/frontend/src/views/database/mysql/root-password/index.vue +++ b/frontend/src/views/database/mysql/root-password/index.vue @@ -9,21 +9,21 @@ {{ form.serviceName }} - + {{ $t('database.serviceNameHelper') }} {{ form.serviceName + ':3306' }} - + {{ $t('database.containerConnHelper') }} @@ -60,9 +60,11 @@ import { updateMysqlPassword } from '@/api/modules/database'; import ConfirmDialog from '@/components/confirm-dialog/index.vue'; import { GetAppConnInfo } from '@/api/modules/app'; import DrawerHeader from '@/components/drawer-header/index.vue'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; import { getRandomStr } from '@/utils/util'; import { App } from '@/api/interface/app'; +import useClipboard from 'vue-clipboard3'; +const { toClipboard } = useClipboard(); const loading = ref(false); @@ -88,14 +90,13 @@ const random = async () => { form.value.password = getRandomStr(16); }; -const copy = async (value: string) => { - let input = document.createElement('input'); - input.value = value; - document.body.appendChild(input); - input.select(); - document.execCommand('Copy'); - document.body.removeChild(input); - MsgSuccess(i18n.global.t('commons.msg.copySuccess')); +const onCopy = async (value: string) => { + try { + await toClipboard(value); + MsgSuccess(i18n.global.t('commons.msg.copySuccess')); + } catch (e) { + MsgError(i18n.global.t('commons.msg.copyfailed')); + } }; const handleClose = () => { diff --git a/frontend/src/views/database/redis/password/index.vue b/frontend/src/views/database/redis/password/index.vue index 385c004d8..a65813cee 100644 --- a/frontend/src/views/database/redis/password/index.vue +++ b/frontend/src/views/database/redis/password/index.vue @@ -9,21 +9,21 @@ {{ form.serviceName }} - + {{ $t('database.serviceNameHelper') }} {{ form.serviceName + ':6379' }} - + {{ $t('database.containerConnHelper') }} @@ -59,10 +59,12 @@ import { ElForm } from 'element-plus'; import { changeRedisPassword } from '@/api/modules/database'; import ConfirmDialog from '@/components/confirm-dialog/index.vue'; import { GetAppConnInfo } from '@/api/modules/app'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; import DrawerHeader from '@/components/drawer-header/index.vue'; import { App } from '@/api/interface/app'; import { getRandomStr } from '@/utils/util'; +import useClipboard from 'vue-clipboard3'; +const { toClipboard } = useClipboard(); const loading = ref(false); @@ -93,14 +95,13 @@ const random = async () => { form.value.password = getRandomStr(16); }; -const copy = async (value: string) => { - let input = document.createElement('input'); - input.value = value; - document.body.appendChild(input); - input.select(); - document.execCommand('Copy'); - document.body.removeChild(input); - MsgSuccess(i18n.global.t('commons.msg.copySuccess')); +const onCopy = async (value: string) => { + try { + await toClipboard(value); + MsgSuccess(i18n.global.t('commons.msg.copySuccess')); + } catch (e) { + MsgError(i18n.global.t('commons.msg.copyfailed')); + } }; const loadPassword = async () => { diff --git a/frontend/src/views/host/ssh/log/log.vue b/frontend/src/views/host/ssh/log/log.vue index c8c007950..01119aeb3 100644 --- a/frontend/src/views/host/ssh/log/log.vue +++ b/frontend/src/views/host/ssh/log/log.vue @@ -10,10 +10,10 @@ - + {{ $t('commons.status.success') }}: {{ successfulCount }} - + {{ $t('commons.status.failed') }}: {{ faliedCount }} @@ -120,11 +120,6 @@ const search = async () => { }); }; -const onSearch = (status: string) => { - searchStatus.value = status; - search(); -}; - onMounted(() => { search(); }); diff --git a/frontend/src/views/host/ssh/ssh/address/index.vue b/frontend/src/views/host/ssh/ssh/address/index.vue new file mode 100644 index 000000000..64fd4f382 --- /dev/null +++ b/frontend/src/views/host/ssh/ssh/address/index.vue @@ -0,0 +1,103 @@ + + diff --git a/frontend/src/views/host/ssh/ssh/index.vue b/frontend/src/views/host/ssh/ssh/index.vue index 5b433179e..75e472519 100644 --- a/frontend/src/views/host/ssh/ssh/index.vue +++ b/frontend/src/views/host/ssh/ssh/index.vue @@ -2,6 +2,38 @@
+
+ +
+ SSH + + {{ $t('commons.status.running') }} + + + {{ $t('commons.status.stopped') }} + + + + {{ $t('commons.button.stop') }} + + + + {{ $t('container.restart') }} + + + + + {{ $t('commons.button.start') }} + + + + {{ $t('container.restart') }} + + +
+
+
+ - + + +
+
@@ -115,11 +144,13 @@ import LayoutContent from '@/layout/layout-content.vue'; import { javascript } from '@codemirror/lang-javascript'; import { oneDark } from '@codemirror/theme-one-dark'; import PubKey from '@/views/host/ssh/ssh/pubkey/index.vue'; +import Root from '@/views/host/ssh/ssh/root/index.vue'; +import Port from '@/views/host/ssh/ssh/port/index.vue'; +import Address from '@/views/host/ssh/ssh/address/index.vue'; import i18n from '@/lang'; import { MsgSuccess } from '@/utils/message'; -import { getSSHInfo, updateSSH, updateSSHByfile } from '@/api/modules/host'; +import { getSSHInfo, operateSSH, updateSSH, updateSSHByfile } from '@/api/modules/host'; import { LoadFile } from '@/api/modules/files'; -import { Rules } from '@/global/form-rules'; import { ElMessageBox, FormInstance } from 'element-plus'; const loading = ref(false); @@ -127,9 +158,13 @@ const formRef = ref(); const extensions = [javascript(), oneDark]; const confShowType = ref('base'); const pubKeyRef = ref(); +const portRef = ref(); +const addressRef = ref(); +const rootsRef = ref(); const sshConf = ref(); const form = reactive({ + status: 'enable', port: 22, listenAddress: '', passwordAuthentication: 'yes', @@ -137,6 +172,7 @@ const form = reactive({ encryptionMode: '', primaryKey: '', permitRootLogin: 'yes', + permitRootLoginItem: 'yes', useDNS: 'no', }); @@ -162,6 +198,35 @@ const onOpenDrawer = () => { pubKeyRef.value.acceptParams(); }; +const onChangePort = () => { + portRef.value.acceptParams({ port: form.port }); +}; +const onChangeRoot = () => { + rootsRef.value.acceptParams({ permitRootLogin: form.permitRootLogin }); +}; +const onChangeAddress = () => { + addressRef.value.acceptParams({ address: form.listenAddress }); +}; + +const onOperate = async (operation: string) => { + ElMessageBox.confirm(i18n.global.t('ssh.sshOperate', [i18n.global.t('commons.button.' + operation)]), 'SSH', { + confirmButtonText: i18n.global.t('commons.button.confirm'), + cancelButtonText: i18n.global.t('commons.button.cancel'), + type: 'info', + }).then(async () => { + loading.value = true; + await operateSSH(operation) + .then(() => { + loading.value = false; + MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); + search(); + }) + .catch(() => { + loading.value = false; + }); + }); +}; + const onSave = async (formEl: FormInstance | undefined, key: string, value: string) => { if (!formEl) return; let itemKey = key.replace(key[0], key[0].toLowerCase()); @@ -208,12 +273,6 @@ const changei18n = (value: string) => { return i18n.global.t('commons.button.enable'); case 'no': return i18n.global.t('commons.button.disable'); - case 'without-password': - return i18n.global.t('ssh.rootHelper3'); - case 'forced-commands-only': - return i18n.global.t('ssh.rootHelper4'); - case 'yes': - return i18n.global.t('commons.button.enable'); default: return value; } @@ -234,15 +293,46 @@ const changeMode = async () => { const search = async () => { const res = await getSSHInfo(); + form.status = res.data.status; form.port = Number(res.data.port); form.listenAddress = res.data.listenAddress; form.passwordAuthentication = res.data.passwordAuthentication; form.pubkeyAuthentication = res.data.pubkeyAuthentication; form.permitRootLogin = res.data.permitRootLogin; + form.permitRootLoginItem = loadPermitLabel(res.data.permitRootLogin); form.useDNS = res.data.useDNS; }; +const loadPermitLabel = (value: string) => { + switch (value) { + case 'yes': + return i18n.global.t('ssh.rootHelper1'); + case 'no': + return i18n.global.t('ssh.rootHelper2'); + case 'without-password': + return i18n.global.t('ssh.rootHelper3'); + case 'forced-commands-only': + return i18n.global.t('ssh.rootHelper4'); + } +}; + onMounted(() => { search(); }); + + diff --git a/frontend/src/views/host/ssh/ssh/port/index.vue b/frontend/src/views/host/ssh/ssh/port/index.vue new file mode 100644 index 000000000..c7e25ddfa --- /dev/null +++ b/frontend/src/views/host/ssh/ssh/port/index.vue @@ -0,0 +1,99 @@ + + diff --git a/frontend/src/views/host/ssh/ssh/pubkey/index.vue b/frontend/src/views/host/ssh/ssh/pubkey/index.vue index 1868b2069..b0b3f604d 100644 --- a/frontend/src/views/host/ssh/ssh/pubkey/index.vue +++ b/frontend/src/views/host/ssh/ssh/pubkey/index.vue @@ -28,28 +28,19 @@ - - {{ form.primaryKey ? $t('ssh.reGenerate') : $t('ssh.generate') }} -
- + {{ $t('file.copy') }} - + {{ $t('commons.button.download') }}
@@ -60,6 +51,9 @@ @@ -69,10 +63,12 @@ import { generateSecret, loadSecret } from '@/api/modules/host'; import { Rules } from '@/global/form-rules'; import i18n from '@/lang'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; import { dateFormatForName, getRandomStr } from '@/utils/util'; +import useClipboard from 'vue-clipboard3'; import { FormInstance } from 'element-plus'; import { reactive, ref } from 'vue'; +const { toClipboard } = useClipboard(); const loading = ref(); const drawerVisiable = ref(); @@ -117,10 +113,10 @@ const onLoadSecret = async () => { const onCopy = async (str: string) => { try { - await navigator.clipboard.writeText(str); + await toClipboard(str); MsgSuccess(i18n.global.t('commons.msg.copySuccess')); - } catch (err) { - MsgSuccess(i18n.global.t('commons.msg.copyfailed')); + } catch (e) { + MsgError(i18n.global.t('commons.msg.copyfailed')); } }; diff --git a/frontend/src/views/host/ssh/ssh/root/index.vue b/frontend/src/views/host/ssh/ssh/root/index.vue new file mode 100644 index 000000000..58bb825fd --- /dev/null +++ b/frontend/src/views/host/ssh/ssh/root/index.vue @@ -0,0 +1,119 @@ + + diff --git a/frontend/src/views/setting/safe/mfa/index.vue b/frontend/src/views/setting/safe/mfa/index.vue index 64f43cf50..a4cb7729c 100644 --- a/frontend/src/views/setting/safe/mfa/index.vue +++ b/frontend/src/views/setting/safe/mfa/index.vue @@ -74,8 +74,10 @@ import { bindMFA, getMFA } from '@/api/modules/setting'; import { reactive, ref } from 'vue'; import { Rules } from '@/global/form-rules'; import i18n from '@/lang'; -import { MsgSuccess } from '@/utils/message'; +import { MsgError, MsgSuccess } from '@/utils/message'; import { FormInstance } from 'element-plus'; +import useClipboard from 'vue-clipboard3'; +const { toClipboard } = useClipboard(); const loading = ref(); const qrImage = ref(); @@ -93,14 +95,13 @@ const acceptParams = (): void => { drawerVisiable.value = true; }; -const onCopy = () => { - let input = document.createElement('input'); - input.value = form.secret; - document.body.appendChild(input); - input.select(); - document.execCommand('Copy'); - document.body.removeChild(input); - MsgSuccess(i18n.global.t('commons.msg.copySuccess')); +const onCopy = async () => { + try { + await toClipboard(form.secret); + MsgSuccess(i18n.global.t('commons.msg.copySuccess')); + } catch (e) { + MsgError(i18n.global.t('commons.msg.copyfailed')); + } }; const loadMfaCode = async () => {