1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-02-08 01:20:07 +08:00

feat: sshd 支持重启、停止等操作,增加状态条

This commit is contained in:
ssongliu 2023-05-17 18:45:48 +08:00 committed by zhengkunwang223
parent 73d93d6104
commit 2975cf6d5a
21 changed files with 617 additions and 139 deletions

View File

@ -23,6 +23,32 @@ func (b *BaseApi) GetSSHInfo(c *gin.Context) {
helper.SuccessWithData(c, info) 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 // @Tags SSH
// @Summary Update host ssh setting // @Summary Update host ssh setting
// @Description 更新 SSH 配置 // @Description 更新 SSH 配置

View File

@ -23,6 +23,10 @@ type OperateByID struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
} }
type Operate struct {
Operation string `json:"operation" validate:"required"`
}
type BatchDeleteReq struct { type BatchDeleteReq struct {
Ids []uint `json:"ids" validate:"required"` Ids []uint `json:"ids" validate:"required"`
} }

View File

@ -3,12 +3,13 @@ package dto
import "time" import "time"
type SSHInfo struct { 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"` ListenAddress string `json:"listenAddress"`
PasswordAuthentication string `json:"passwordAuthentication" validate:"required,oneof=yes no"` PasswordAuthentication string `json:"passwordAuthentication"`
PubkeyAuthentication string `json:"pubkeyAuthentication" validate:"required,oneof=yes no"` PubkeyAuthentication string `json:"pubkeyAuthentication"`
PermitRootLogin string `json:"permitRootLogin" validate:"required,oneof=yes no without-password forced-commands-only"` PermitRootLogin string `json:"permitRootLogin"`
UseDNS string `json:"useDNS" validate:"required,oneof=yes no"` UseDNS string `json:"useDNS"`
} }
type GenerateSSH struct { type GenerateSSH struct {

View File

@ -24,6 +24,7 @@ type SSHService struct{}
type ISSHService interface { type ISSHService interface {
GetSSHInfo() (*dto.SSHInfo, error) GetSSHInfo() (*dto.SSHInfo, error)
OperateSSH(operation string) error
UpdateByFile(value string) error UpdateByFile(value string) error
Update(key, value string) error Update(key, value string) error
GenerateSSH(req dto.GenerateSSH) error GenerateSSH(req dto.GenerateSSH) error
@ -37,6 +38,7 @@ func NewISSHService() ISSHService {
func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
data := dto.SSHInfo{ data := dto.SSHInfo{
Status: constant.StatusDisable,
Port: "22", Port: "22",
ListenAddress: "0.0.0.0", ListenAddress: "0.0.0.0",
PasswordAuthentication: "yes", PasswordAuthentication: "yes",
@ -44,6 +46,25 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
PermitRootLogin: "yes", PermitRootLogin: "yes",
UseDNS: "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) sshConf, err := os.ReadFile(sshPath)
if err != nil { if err != nil {
return &data, err return &data, err
@ -72,6 +93,22 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
return &data, err 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 { func (u *SSHService) Update(key, value string) error {
sshConf, err := os.ReadFile(sshPath) sshConf, err := os.ReadFile(sshPath)
if err != nil { if err != nil {
@ -96,7 +133,7 @@ func (u *SSHService) Update(key, value string) error {
sudo = "sudo" sudo = "sudo"
} }
if key == "Port" { if key == "Port" {
stdout, _ := cmd.Exec("getenforce") stdout, _ := cmd.Execf("%s getenforce", sudo)
if stdout == "Enforcing\n" { if stdout == "Enforcing\n" {
_, _ = cmd.Execf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, value) _, _ = cmd.Execf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, value)
} }

View File

@ -41,6 +41,7 @@ func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) {
hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret) hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret)
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs) hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile) hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile)
hostRouter.POST("/ssh/operate", baseApi.OperateSSH)
hostRouter.GET("/command", baseApi.ListCommand) hostRouter.GET("/command", baseApi.ListCommand)
hostRouter.POST("/command", baseApi.CreateCommand) hostRouter.POST("/command", baseApi.CreateCommand)

View File

@ -105,6 +105,7 @@ export namespace Host {
} }
export interface SSHInfo { export interface SSHInfo {
status: string;
port: string; port: string;
listenAddress: string; listenAddress: string;
passwordAuthentication: string; passwordAuthentication: string;

View File

@ -101,6 +101,9 @@ export const batchOperateRule = (params: Host.BatchRule) => {
export const getSSHInfo = () => { export const getSSHInfo = () => {
return http.post<Host.SSHInfo>(`/hosts/ssh/search`); return http.post<Host.SSHInfo>(`/hosts/ssh/search`);
}; };
export const operateSSH = (operation: string) => {
return http.post(`/hosts/ssh/operate`, { operation: operation });
};
export const updateSSH = (key: string, value: string) => { export const updateSSH = (key: string, value: string) => {
return http.post(`/hosts/ssh/update`, { key: key, value: value }); return http.post(`/hosts/ssh/update`, { key: key, value: value });
}; };

View File

@ -18,16 +18,17 @@
<script setup lang="ts" name="404"> <script setup lang="ts" name="404">
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard();
const onCopy = () => { const onCopy = async () => {
let input = document.createElement('input'); try {
input.value = '1pctl user-info'; await toClipboard('1pctl user-info');
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
MsgSuccess(i18n.global.t('commons.msg.copySuccess')); MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
} catch (e) {
MsgError(i18n.global.t('commons.msg.copyfailed'));
}
}; };
</script> </script>

View File

@ -816,8 +816,9 @@ const message = {
searchHelper: 'Support wildcards such as *', searchHelper: 'Support wildcards such as *',
}, },
ssh: { ssh: {
sshOperate: 'Operation [{0}] on the SSH service is performed. Do you want to continue?',
sshChange: 'SSH Setting', 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: sshFileChangeHelper:
'Modifying the configuration file may cause service availability. Exercise caution when performing this operation. Do you want to continue?', 'Modifying the configuration file may cause service availability. Exercise caution when performing this operation. Do you want to continue?',
port: 'Port', port: 'Port',
@ -837,7 +838,7 @@ const message = {
key: 'Key', key: 'Key',
pubkey: 'Key info', pubkey: 'Key info',
encryptionMode: 'Encryption mode', 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', generate: 'Generate key',
reGenerate: 'Regenerate key', reGenerate: 'Regenerate key',
keyAuthHelper: 'Whether to enable key authentication. This parameter is enabled by default.', keyAuthHelper: 'Whether to enable key authentication. This parameter is enabled by default.',

View File

@ -821,8 +821,9 @@ const message = {
searchHelper: '支持 * 等通配符', searchHelper: '支持 * 等通配符',
}, },
ssh: { ssh: {
sshOperate: ' SSH 服务进行 [{0}] 操作是否继续',
sshChange: 'SSH 配置修改', sshChange: 'SSH 配置修改',
sshChangeHelper: '确认将 SSH {0} 配置修改为 {1} ', sshChangeHelper: '此操作将 {0} 修改为 [{1}] 是否继续',
sshFileChangeHelper: '直接修改配置文件可能会导致服务不可用请谨慎操作是否继续', sshFileChangeHelper: '直接修改配置文件可能会导致服务不可用请谨慎操作是否继续',
port: '连接端口', port: '连接端口',
portHelper: '指定 SSH 服务监听的端口号默认为 22', portHelper: '指定 SSH 服务监听的端口号默认为 22',
@ -833,14 +834,14 @@ const message = {
rootHelper1: '允许 SSH 登录', rootHelper1: '允许 SSH 登录',
rootHelper2: '禁止 SSH 登录', rootHelper2: '禁止 SSH 登录',
rootHelper3: '仅允许密钥登录', rootHelper3: '仅允许密钥登录',
rootHelper4: '仅允许执行预先定义的命令不能进行其他操作', rootHelper4: '仅允许执行预先定义的命令不能进行其他操作',
passwordAuthentication: '密码认证', passwordAuthentication: '密码认证',
pwdAuthHelper: '是否启用密码认证默认启用', pwdAuthHelper: '是否启用密码认证默认启用',
pubkeyAuthentication: '密钥认证', pubkeyAuthentication: '密钥认证',
key: '密钥', key: '密钥',
pubkey: '密钥信息', pubkey: '密钥信息',
encryptionMode: '加密方式', encryptionMode: '加密方式',
passwordHelper: '请输入 6-10 位加密密码', passwordHelper: '支持大小写英文数字,长度6-10',
generate: '生成密钥', generate: '生成密钥',
reGenerate: '重新生成密钥', reGenerate: '重新生成密钥',
keyAuthHelper: '是否启用密钥认证默认启用', keyAuthHelper: '是否启用密钥认证默认启用',

View File

@ -50,6 +50,25 @@ const hostRouter = {
requiresAuth: false, 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', path: '/hosts/ssh/ssh',
name: 'SSH', name: 'SSH',
@ -71,25 +90,6 @@ const hostRouter = {
requiresAuth: false, 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,
},
},
], ],
}; };

View File

@ -73,11 +73,7 @@
</el-icon> </el-icon>
</div> </div>
<div style="cursor: pointer; float: left"> <div style="cursor: pointer; float: left">
<el-icon <el-icon style="margin-left: 5px; margin-top: 3px" :size="16" @click="onCopy(row)">
style="margin-left: 5px; margin-top: 3px"
:size="16"
@click="onCopyPassword(row)"
>
<DocumentCopy /> <DocumentCopy />
</el-icon> </el-icon>
</div> </div>
@ -168,7 +164,9 @@ import { Database } from '@/api/interface/database';
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import { GetAppPort } from '@/api/modules/app'; import { GetAppPort } from '@/api/modules/app';
import router from '@/routers'; 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 loading = ref(false);
const maskShow = ref(true); const maskShow = ref(true);
@ -290,14 +288,13 @@ const checkExist = (data: App.CheckInstalled) => {
} }
}; };
const onCopyPassword = (row: Database.MysqlDBInfo) => { const onCopy = async (row: any) => {
let input = document.createElement('input'); try {
input.value = row.password; await toClipboard(row.password);
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
MsgSuccess(i18n.global.t('commons.msg.copySuccess')); MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
} catch (e) {
MsgError(i18n.global.t('commons.msg.copyfailed'));
}
}; };
const onDelete = async (row: Database.MysqlDBInfo) => { const onDelete = async (row: Database.MysqlDBInfo) => {

View File

@ -9,21 +9,21 @@
<el-form-item :label="$t('database.rootPassword')" :rules="Rules.requiredInput" prop="password"> <el-form-item :label="$t('database.rootPassword')" :rules="Rules.requiredInput" prop="password">
<el-input type="password" show-password clearable v-model="form.password"> <el-input type="password" show-password clearable v-model="form.password">
<template #append> <template #append>
<el-button @click="copy(form.password)" icon="DocumentCopy"></el-button> <el-button @click="onCopy(form.password)" icon="DocumentCopy"></el-button>
<el-button style="margin-left: 1px" @click="random" icon="RefreshRight"></el-button> <el-button style="margin-left: 1px" @click="random" icon="RefreshRight"></el-button>
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('database.serviceName')" prop="serviceName"> <el-form-item :label="$t('database.serviceName')" prop="serviceName">
<el-tag>{{ form.serviceName }}</el-tag> <el-tag>{{ form.serviceName }}</el-tag>
<el-button @click="copy(form.serviceName)" icon="DocumentCopy" link></el-button> <el-button @click="onCopy(form.serviceName)" icon="DocumentCopy" link></el-button>
<span class="input-help">{{ $t('database.serviceNameHelper') }}</span> <span class="input-help">{{ $t('database.serviceNameHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('database.containerConn')"> <el-form-item :label="$t('database.containerConn')">
<el-tag> <el-tag>
{{ form.serviceName + ':3306' }} {{ form.serviceName + ':3306' }}
</el-tag> </el-tag>
<el-button @click="copy(form.serviceName + ':3306')" icon="DocumentCopy" link></el-button> <el-button @click="onCopy(form.serviceName + ':3306')" icon="DocumentCopy" link></el-button>
<span class="input-help"> <span class="input-help">
{{ $t('database.containerConnHelper') }} {{ $t('database.containerConnHelper') }}
</span> </span>
@ -60,9 +60,11 @@ import { updateMysqlPassword } from '@/api/modules/database';
import ConfirmDialog from '@/components/confirm-dialog/index.vue'; import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { GetAppConnInfo } from '@/api/modules/app'; import { GetAppConnInfo } from '@/api/modules/app';
import DrawerHeader from '@/components/drawer-header/index.vue'; 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 { getRandomStr } from '@/utils/util';
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard();
const loading = ref(false); const loading = ref(false);
@ -88,14 +90,13 @@ const random = async () => {
form.value.password = getRandomStr(16); form.value.password = getRandomStr(16);
}; };
const copy = async (value: string) => { const onCopy = async (value: string) => {
let input = document.createElement('input'); try {
input.value = value; await toClipboard(value);
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
MsgSuccess(i18n.global.t('commons.msg.copySuccess')); MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
} catch (e) {
MsgError(i18n.global.t('commons.msg.copyfailed'));
}
}; };
const handleClose = () => { const handleClose = () => {

View File

@ -9,21 +9,21 @@
<el-form-item :label="$t('database.requirepass')" :rules="Rules.requiredInput" prop="password"> <el-form-item :label="$t('database.requirepass')" :rules="Rules.requiredInput" prop="password">
<el-input type="password" show-password clearable v-model="form.password"> <el-input type="password" show-password clearable v-model="form.password">
<template #append> <template #append>
<el-button @click="copy(form.password)" icon="DocumentCopy"></el-button> <el-button @click="onCopy(form.password)" icon="DocumentCopy"></el-button>
<el-button style="margin-left: 1px" @click="random" icon="RefreshRight"></el-button> <el-button style="margin-left: 1px" @click="random" icon="RefreshRight"></el-button>
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('database.serviceName')" prop="serviceName"> <el-form-item :label="$t('database.serviceName')" prop="serviceName">
<el-tag>{{ form.serviceName }}</el-tag> <el-tag>{{ form.serviceName }}</el-tag>
<el-button @click="copy(form.serviceName)" icon="DocumentCopy" link></el-button> <el-button @click="onCopy(form.serviceName)" icon="DocumentCopy" link></el-button>
<span class="input-help">{{ $t('database.serviceNameHelper') }}</span> <span class="input-help">{{ $t('database.serviceNameHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('database.containerConn')"> <el-form-item :label="$t('database.containerConn')">
<el-tag> <el-tag>
{{ form.serviceName + ':6379' }} {{ form.serviceName + ':6379' }}
</el-tag> </el-tag>
<el-button @click="copy(form.serviceName + ':6379')" icon="DocumentCopy" link></el-button> <el-button @click="onCopy(form.serviceName + ':6379')" icon="DocumentCopy" link></el-button>
<span class="input-help"> <span class="input-help">
{{ $t('database.containerConnHelper') }} {{ $t('database.containerConnHelper') }}
</span> </span>
@ -59,10 +59,12 @@ import { ElForm } from 'element-plus';
import { changeRedisPassword } from '@/api/modules/database'; import { changeRedisPassword } from '@/api/modules/database';
import ConfirmDialog from '@/components/confirm-dialog/index.vue'; import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { GetAppConnInfo } from '@/api/modules/app'; 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 DrawerHeader from '@/components/drawer-header/index.vue';
import { App } from '@/api/interface/app'; import { App } from '@/api/interface/app';
import { getRandomStr } from '@/utils/util'; import { getRandomStr } from '@/utils/util';
import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard();
const loading = ref(false); const loading = ref(false);
@ -93,14 +95,13 @@ const random = async () => {
form.value.password = getRandomStr(16); form.value.password = getRandomStr(16);
}; };
const copy = async (value: string) => { const onCopy = async (value: string) => {
let input = document.createElement('input'); try {
input.value = value; await toClipboard(value);
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
MsgSuccess(i18n.global.t('commons.msg.copySuccess')); MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
} catch (e) {
MsgError(i18n.global.t('commons.msg.copyfailed'));
}
}; };
const loadPassword = async () => { const loadPassword = async () => {

View File

@ -10,10 +10,10 @@
<el-option :label="$t('commons.status.success')" value="Success"></el-option> <el-option :label="$t('commons.status.success')" value="Success"></el-option>
<el-option :label="$t('commons.status.failed')" value="Failed"></el-option> <el-option :label="$t('commons.status.failed')" value="Failed"></el-option>
</el-select> </el-select>
<el-button type="success" plain @click="onSearch('Success')" style="margin-left: 25px"> <el-button type="success" plain style="margin-left: 25px">
{{ $t('commons.status.success') }} {{ successfulCount }} {{ $t('commons.status.success') }} {{ successfulCount }}
</el-button> </el-button>
<el-button type="danger" plain @click="onSearch('Failed')" style="margin-left: 5px"> <el-button type="danger" plain style="margin-left: 5px">
{{ $t('commons.status.failed') }} {{ faliedCount }} {{ $t('commons.status.failed') }} {{ faliedCount }}
</el-button> </el-button>
</el-col> </el-col>
@ -120,11 +120,6 @@ const search = async () => {
}); });
}; };
const onSearch = (status: string) => {
searchStatus.value = status;
search();
};
onMounted(() => { onMounted(() => {
search(); search();
}); });

View File

@ -0,0 +1,103 @@
<template>
<div>
<el-drawer
v-model="drawerVisiable"
:destroy-on-close="true"
@close="handleClose"
:close-on-click-modal="false"
size="30%"
>
<template #header>
<DrawerHeader :header="$t('ssh.listenAddress')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item
:label="$t('ssh.listenAddress')"
prop="listenAddress"
:rules="Rules.requiredInput"
>
<el-input clearable v-model="form.listenAddress" />
</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="onSave(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 { ElMessageBox, FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { updateSSH } from '@/api/modules/host';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
address: string;
}
const drawerVisiable = ref();
const loading = ref();
const form = reactive({
listenAddress: '0.0.0.0',
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.listenAddress = params.address;
drawerVisiable.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('ssh.listenAddress'), form.listenAddress]),
i18n.global.t('ssh.sshChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
)
.then(async () => {
loading.value = true;
await updateSSH('ListenAddress', form.listenAddress)
.then(() => {
loading.value = false;
handleClose();
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {
emit('search');
});
});
};
const handleClose = () => {
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -2,6 +2,38 @@
<div v-loading="loading"> <div v-loading="loading">
<FireRouter /> <FireRouter />
<div class="a-card" style="margin-top: 20px">
<el-card>
<div>
<el-tag style="float: left" effect="dark" type="success">SSH</el-tag>
<el-tag round class="status-content" v-if="form.status === 'Enable'" type="success">
{{ $t('commons.status.running') }}
</el-tag>
<el-tag round class="status-content" v-if="form.status === 'Disable'" type="info">
{{ $t('commons.status.stopped') }}
</el-tag>
<span v-if="form.status === 'Enable'" class="buttons">
<el-button type="primary" @click="onOperate('stop')" link>
{{ $t('commons.button.stop') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperate('restart')" link>
{{ $t('container.restart') }}
</el-button>
</span>
<span v-if="form.status === 'Disable'" class="buttons">
<el-button type="primary" @click="onOperate('start')" link>
{{ $t('commons.button.start') }}
</el-button>
<el-divider direction="vertical" />
<el-button type="primary" @click="onOperate('restart')" link>
{{ $t('container.restart') }}
</el-button>
</span>
</div>
</el-card>
</div>
<LayoutContent style="margin-top: 20px" :title="$t('menu.ssh')" :divider="true"> <LayoutContent style="margin-top: 20px" :title="$t('menu.ssh')" :divider="true">
<template #main> <template #main>
<el-radio-group v-model="confShowType" @change="changeMode"> <el-radio-group v-model="confShowType" @change="changeMode">
@ -12,40 +44,34 @@
<el-col :span="1"><br /></el-col> <el-col :span="1"><br /></el-col>
<el-col :span="10"> <el-col :span="10">
<el-form :model="form" label-position="left" ref="formRef" label-width="120px"> <el-form :model="form" label-position="left" ref="formRef" label-width="120px">
<el-form-item :label="$t('ssh.port')" prop="port" :rules="Rules.port"> <el-form-item :label="$t('ssh.port')" prop="port">
<el-input v-model.number="form.port"> <el-input disabled v-model.number="form.port">
<template #append> <template #append>
<el-button icon="Collection" @click="onSave(formRef, 'Port', form.port + '')"> <el-button @click="onChangePort" icon="Setting">
{{ $t('commons.button.save') }} {{ $t('commons.button.set') }}
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
<span class="input-help">{{ $t('ssh.portHelper') }}</span> <span class="input-help">{{ $t('ssh.portHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('ssh.listenAddress')" prop="listenAddress"> <el-form-item :label="$t('ssh.listenAddress')" prop="listenAddress">
<el-input v-model="form.listenAddress"> <el-input disabled v-model="form.listenAddress">
<template #append> <template #append>
<el-button <el-button @click="onChangeAddress" icon="Setting">
icon="Collection" {{ $t('commons.button.set') }}
@click="onSave(formRef, 'ListenAddress', form.listenAddress)"
>
{{ $t('commons.button.save') }}
</el-button> </el-button>
</template> </template>
</el-input> </el-input>
<span class="input-help">{{ $t('ssh.addressHelper') }}</span> <span class="input-help">{{ $t('ssh.addressHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('ssh.permitRootLogin')" prop="permitRootLogin"> <el-form-item :label="$t('ssh.permitRootLogin')" prop="permitRootLoginItem">
<el-select <el-input disabled v-model="form.permitRootLoginItem">
v-model="form.permitRootLogin" <template #append>
@change="onSave(formRef, 'PermitRootLogin', form.permitRootLogin)" <el-button @click="onChangeRoot" icon="Setting">
style="width: 100%" {{ $t('commons.button.set') }}
> </el-button>
<el-option :label="$t('ssh.rootHelper1')" value="yes" /> </template>
<el-option :label="$t('ssh.rootHelper2')" value="no" /> </el-input>
<el-option :label="$t('ssh.rootHelper3')" value="without-password" />
<el-option :label="$t('ssh.rootHelper4')" value="forced-commands-only" />
</el-select>
<span class="input-help">{{ $t('ssh.rootSettingHelper') }}</span> <span class="input-help">{{ $t('ssh.rootSettingHelper') }}</span>
</el-form-item> </el-form-item>
<el-form-item :label="$t('ssh.passwordAuthentication')" prop="passwordAuthentication"> <el-form-item :label="$t('ssh.passwordAuthentication')" prop="passwordAuthentication">
@ -103,7 +129,10 @@
</template> </template>
</LayoutContent> </LayoutContent>
<PubKey ref="pubKeyRef" /> <PubKey ref="pubKeyRef" @search="search" />
<Port ref="portRef" @search="search" />
<Address ref="addressRef" @search="search" />
<Root ref="rootsRef" @search="search" />
</div> </div>
</template> </template>
@ -115,11 +144,13 @@ import LayoutContent from '@/layout/layout-content.vue';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import PubKey from '@/views/host/ssh/ssh/pubkey/index.vue'; 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 i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; 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 { LoadFile } from '@/api/modules/files';
import { Rules } from '@/global/form-rules';
import { ElMessageBox, FormInstance } from 'element-plus'; import { ElMessageBox, FormInstance } from 'element-plus';
const loading = ref(false); const loading = ref(false);
@ -127,9 +158,13 @@ const formRef = ref();
const extensions = [javascript(), oneDark]; const extensions = [javascript(), oneDark];
const confShowType = ref('base'); const confShowType = ref('base');
const pubKeyRef = ref(); const pubKeyRef = ref();
const portRef = ref();
const addressRef = ref();
const rootsRef = ref();
const sshConf = ref(); const sshConf = ref();
const form = reactive({ const form = reactive({
status: 'enable',
port: 22, port: 22,
listenAddress: '', listenAddress: '',
passwordAuthentication: 'yes', passwordAuthentication: 'yes',
@ -137,6 +172,7 @@ const form = reactive({
encryptionMode: '', encryptionMode: '',
primaryKey: '', primaryKey: '',
permitRootLogin: 'yes', permitRootLogin: 'yes',
permitRootLoginItem: 'yes',
useDNS: 'no', useDNS: 'no',
}); });
@ -162,6 +198,35 @@ const onOpenDrawer = () => {
pubKeyRef.value.acceptParams(); 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) => { const onSave = async (formEl: FormInstance | undefined, key: string, value: string) => {
if (!formEl) return; if (!formEl) return;
let itemKey = key.replace(key[0], key[0].toLowerCase()); let itemKey = key.replace(key[0], key[0].toLowerCase());
@ -208,12 +273,6 @@ const changei18n = (value: string) => {
return i18n.global.t('commons.button.enable'); return i18n.global.t('commons.button.enable');
case 'no': case 'no':
return i18n.global.t('commons.button.disable'); 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: default:
return value; return value;
} }
@ -234,15 +293,46 @@ const changeMode = async () => {
const search = async () => { const search = async () => {
const res = await getSSHInfo(); const res = await getSSHInfo();
form.status = res.data.status;
form.port = Number(res.data.port); form.port = Number(res.data.port);
form.listenAddress = res.data.listenAddress; form.listenAddress = res.data.listenAddress;
form.passwordAuthentication = res.data.passwordAuthentication; form.passwordAuthentication = res.data.passwordAuthentication;
form.pubkeyAuthentication = res.data.pubkeyAuthentication; form.pubkeyAuthentication = res.data.pubkeyAuthentication;
form.permitRootLogin = res.data.permitRootLogin; form.permitRootLogin = res.data.permitRootLogin;
form.permitRootLoginItem = loadPermitLabel(res.data.permitRootLogin);
form.useDNS = res.data.useDNS; 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(() => { onMounted(() => {
search(); search();
}); });
</script> </script>
<style lang="scss" scoped>
.a-card {
font-size: 17px;
.el-card {
--el-card-padding: 12px;
.buttons {
margin-left: 100px;
}
}
}
.status-content {
float: left;
margin-left: 50px;
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div>
<el-drawer
v-model="drawerVisiable"
:destroy-on-close="true"
@close="handleClose"
:close-on-click-modal="false"
size="30%"
>
<template #header>
<DrawerHeader :header="$t('ssh.port')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('ssh.port')" prop="port" :rules="Rules.port">
<el-input clearable v-model.number="form.port" />
</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="onSave(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 { ElMessageBox, FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { updateSSH } from '@/api/modules/host';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
port: number;
}
const drawerVisiable = ref();
const loading = ref();
const form = reactive({
port: 22,
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.port = params.port;
drawerVisiable.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('ssh.port'), form.port]),
i18n.global.t('ssh.sshChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
)
.then(async () => {
loading.value = true;
await updateSSH('Port', form.port + '')
.then(() => {
loading.value = false;
handleClose();
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {
emit('search');
});
});
};
const handleClose = () => {
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -28,28 +28,19 @@
<el-button style="margin-left: 1px" @click="random" icon="RefreshRight"></el-button> <el-button style="margin-left: 1px" @click="random" icon="RefreshRight"></el-button>
</template> </template>
</el-input> </el-input>
<el-button link @click="onGenerate(formRef)" type="primary" class="margintop">
{{ form.primaryKey ? $t('ssh.reGenerate') : $t('ssh.generate') }}
</el-button>
</el-form-item> </el-form-item>
<el-form-item :label="$t('ssh.key')" prop="primaryKey" v-if="form.encryptionMode"> <el-form-item :label="$t('ssh.key')" prop="primaryKey" v-if="form.encryptionMode">
<el-input <el-input
v-model="form.primaryKey" v-model="form.primaryKey"
:autosize="{ minRows: 5, maxRows: 15 }" :autosize="{ minRows: 5, maxRows: 10 }"
type="textarea" type="textarea"
/> />
<div v-if="form.primaryKey"> <div v-if="form.primaryKey">
<el-button <el-button icon="CopyDocument" class="margintop" @click="onCopy(form.primaryKey)">
link
type="primary"
icon="CopyDocument"
class="margintop"
@click="onCopy(form.primaryKey)"
>
{{ $t('file.copy') }} {{ $t('file.copy') }}
</el-button> </el-button>
<el-button link type="primary" icon="Download" class="margintop" @click="onDownload"> <el-button icon="Download" class="margintop" @click="onDownload">
{{ $t('commons.button.download') }} {{ $t('commons.button.download') }}
</el-button> </el-button>
</div> </div>
@ -60,6 +51,9 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button> <el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button @click="onGenerate(formRef)" type="primary">
{{ $t('ssh.generate') }}
</el-button>
</span> </span>
</template> </template>
</el-drawer> </el-drawer>
@ -69,10 +63,12 @@
import { generateSecret, loadSecret } from '@/api/modules/host'; import { generateSecret, loadSecret } from '@/api/modules/host';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import { dateFormatForName, getRandomStr } from '@/utils/util'; import { dateFormatForName, getRandomStr } from '@/utils/util';
import useClipboard from 'vue-clipboard3';
import { FormInstance } from 'element-plus'; import { FormInstance } from 'element-plus';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
const { toClipboard } = useClipboard();
const loading = ref(); const loading = ref();
const drawerVisiable = ref(); const drawerVisiable = ref();
@ -117,10 +113,10 @@ const onLoadSecret = async () => {
const onCopy = async (str: string) => { const onCopy = async (str: string) => {
try { try {
await navigator.clipboard.writeText(str); await toClipboard(str);
MsgSuccess(i18n.global.t('commons.msg.copySuccess')); MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
} catch (err) { } catch (e) {
MsgSuccess(i18n.global.t('commons.msg.copyfailed')); MsgError(i18n.global.t('commons.msg.copyfailed'));
} }
}; };

View File

@ -0,0 +1,119 @@
<template>
<div>
<el-drawer
v-model="drawerVisiable"
:destroy-on-close="true"
@close="handleClose"
:close-on-click-modal="false"
size="30%"
>
<template #header>
<DrawerHeader :header="$t('ssh.permitRootLogin')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('ssh.permitRootLogin')" prop="permitRootLogin">
<el-select v-model="form.permitRootLogin" style="width: 100%">
<el-option :label="$t('ssh.rootHelper1')" value="yes" />
<el-option :label="$t('ssh.rootHelper2')" value="no" />
<el-option :label="$t('ssh.rootHelper3')" value="without-password" />
<el-option :label="$t('ssh.rootHelper4')" value="forced-commands-only" />
</el-select>
</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="onSave(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 { ElMessageBox, FormInstance } from 'element-plus';
import { updateSSH } from '@/api/modules/host';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
permitRootLogin: string;
}
const drawerVisiable = ref();
const loading = ref();
const form = reactive({
permitRootLogin: 'yes',
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.permitRootLogin = params.permitRootLogin;
drawerVisiable.value = true;
};
const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [
i18n.global.t('ssh.permitRootLogin'),
loadPermitLabel(form.permitRootLogin),
]),
i18n.global.t('ssh.sshChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
)
.then(async () => {
loading.value = true;
await updateSSH('PermitRootLogin', form.permitRootLogin)
.then(() => {
loading.value = false;
handleClose();
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {
emit('search');
});
});
};
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');
}
};
const handleClose = () => {
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -74,8 +74,10 @@ import { bindMFA, getMFA } from '@/api/modules/setting';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules'; import { Rules } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgError, MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus'; import { FormInstance } from 'element-plus';
import useClipboard from 'vue-clipboard3';
const { toClipboard } = useClipboard();
const loading = ref(); const loading = ref();
const qrImage = ref(); const qrImage = ref();
@ -93,14 +95,13 @@ const acceptParams = (): void => {
drawerVisiable.value = true; drawerVisiable.value = true;
}; };
const onCopy = () => { const onCopy = async () => {
let input = document.createElement('input'); try {
input.value = form.secret; await toClipboard(form.secret);
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
MsgSuccess(i18n.global.t('commons.msg.copySuccess')); MsgSuccess(i18n.global.t('commons.msg.copySuccess'));
} catch (e) {
MsgError(i18n.global.t('commons.msg.copyfailed'));
}
}; };
const loadMfaCode = async () => { const loadMfaCode = async () => {