mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-04 00:10:25 +08:00
refactor: 终端重构、系统参数配置调整
This commit is contained in:
@@ -32,7 +32,7 @@ FROM alpine:3.16
|
|||||||
RUN apk add --no-cache ca-certificates bash expat
|
RUN apk add --no-cache ca-certificates bash expat
|
||||||
|
|
||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
RUN ln -snf /usr/share/zoneinfo/\$TZ /etc/localtime && echo \$TZ > /etc/timezone
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
WORKDIR /mayfly
|
WORKDIR /mayfly
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.1.0",
|
"@element-plus/icons-vue": "^2.1.0",
|
||||||
"asciinema-player": "^3.5.0",
|
"asciinema-player": "^3.5.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.5.0",
|
||||||
"countup.js": "^2.7.0",
|
"countup.js": "^2.7.0",
|
||||||
"cropperjs": "^1.5.11",
|
"cropperjs": "^1.5.11",
|
||||||
"echarts": "^5.4.0",
|
"echarts": "^5.4.0",
|
||||||
@@ -32,7 +32,10 @@
|
|||||||
"vue-clipboard3": "^1.0.1",
|
"vue-clipboard3": "^1.0.1",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"xterm": "^5.2.1",
|
"xterm": "^5.2.1",
|
||||||
"xterm-addon-fit": "^0.7.0"
|
"xterm-addon-fit": "^0.7.0",
|
||||||
|
"xterm-addon-search": "^0.12.0",
|
||||||
|
"xterm-addon-web-links": "^0.8.0",
|
||||||
|
"xterm-addon-webgl": "^0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ export default {
|
|||||||
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
||||||
getPublicKey: () => request.get('/common/public-key'),
|
getPublicKey: () => request.get('/common/public-key'),
|
||||||
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
|
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
|
||||||
oauth2LoginConfig: () => request.get('/sys/configs/oauth2-login'),
|
oauth2LoginConfig: () => request.get('/auth/oauth2-config'),
|
||||||
changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
|
changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
|
||||||
captcha: () => request.get('/sys/captcha'),
|
captcha: () => request.get('/sys/captcha'),
|
||||||
logout: () => request.post('/auth/accounts/logout'),
|
logout: () => request.post('/auth/accounts/logout'),
|
||||||
getPermissions: () => request.get('/sys/accounts/permissions'),
|
getPermissions: () => request.get('/sys/accounts/permissions'),
|
||||||
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
|
oauth2Callback: (params: any) => request.get('/auth/oauth2/callback', params),
|
||||||
getLdapEnabled: () => request.get("/auth/ldap/enabled"),
|
getLdapEnabled: () => request.get('/auth/ldap/enabled'),
|
||||||
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
|
ldapLogin: (param: any) => request.post('/auth/ldap/login', param),
|
||||||
};
|
};
|
||||||
|
|||||||
286
mayfly_go_web/src/components/terminal/TerminalBody.vue
Normal file
286
mayfly_go_web/src/components/terminal/TerminalBody.vue
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }">
|
||||||
|
<div ref="terminalRef" class="terminal" />
|
||||||
|
|
||||||
|
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
import { SearchAddon } from 'xterm-addon-search';
|
||||||
|
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||||
|
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useThemeConfig } from '@/store/themeConfig';
|
||||||
|
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
|
import TerminalSearch from './TerminalSearch.vue';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { TerminalStatus } from './common';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* 初始化执行命令
|
||||||
|
*/
|
||||||
|
cmd: { type: String },
|
||||||
|
/**
|
||||||
|
* 连接url
|
||||||
|
*/
|
||||||
|
socketUrl: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 高度
|
||||||
|
*/
|
||||||
|
height: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['statusChange']);
|
||||||
|
|
||||||
|
const terminalRef: any = ref(null);
|
||||||
|
const terminalSearchRef: any = ref(null);
|
||||||
|
|
||||||
|
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||||
|
|
||||||
|
// 终端实例
|
||||||
|
let term: Terminal;
|
||||||
|
let socket: WebSocket;
|
||||||
|
let pingInterval: any;
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
// 插件
|
||||||
|
addon: {
|
||||||
|
fit: null as any,
|
||||||
|
search: null as any,
|
||||||
|
weblinks: null as any,
|
||||||
|
},
|
||||||
|
status: TerminalStatus.NoConnected,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.status,
|
||||||
|
() => {
|
||||||
|
emit('statusChange', state.status);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (term) {
|
||||||
|
console.log('重新连接...');
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
term = new Terminal({
|
||||||
|
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||||
|
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||||
|
fontFamily: 'JetBrainsMono, monaco, Consolas, Lucida Console, monospace',
|
||||||
|
cursorBlink: true,
|
||||||
|
disableStdin: false,
|
||||||
|
allowProposedApi: true,
|
||||||
|
theme: {
|
||||||
|
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
||||||
|
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
||||||
|
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
||||||
|
// cursorAccent: "red", // 光标停止颜色
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
term.open(terminalRef.value);
|
||||||
|
|
||||||
|
// 注册自适应组件
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
state.addon.fit = fitAddon;
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
fitTerminal();
|
||||||
|
|
||||||
|
// 注册搜索组件
|
||||||
|
const searchAddon = new SearchAddon();
|
||||||
|
state.addon.search = searchAddon;
|
||||||
|
term.loadAddon(searchAddon);
|
||||||
|
|
||||||
|
// 注册 url link组件
|
||||||
|
const weblinks = new WebLinksAddon();
|
||||||
|
state.addon.weblinks = weblinks;
|
||||||
|
term.loadAddon(weblinks);
|
||||||
|
|
||||||
|
// 初始化websocket
|
||||||
|
initSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接成功
|
||||||
|
*/
|
||||||
|
const onConnected = () => {
|
||||||
|
// 注册心跳
|
||||||
|
pingInterval = setInterval(sendPing, 15000);
|
||||||
|
|
||||||
|
// 注册 terminal 事件
|
||||||
|
term.onResize((event) => sendResize(event.cols, event.rows));
|
||||||
|
term.onData((event) => sendCmd(event));
|
||||||
|
|
||||||
|
// 注册自定义快捷键
|
||||||
|
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||||
|
// 注册搜索键 ctrl + f
|
||||||
|
if (event.key === 'f' && (event.ctrlKey || event.metaKey) && event.type === 'keydown') {
|
||||||
|
event.preventDefault();
|
||||||
|
terminalSearchRef.value.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// resize
|
||||||
|
sendResize(term.cols, term.rows);
|
||||||
|
// 注册窗口大小监听器
|
||||||
|
window.addEventListener('resize', debounce(fitTerminal, 400));
|
||||||
|
|
||||||
|
focus();
|
||||||
|
|
||||||
|
// 如果有初始要执行的命令,则发送执行命令
|
||||||
|
if (props.cmd) {
|
||||||
|
sendCmd(props.cmd + ' \r');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.status = TerminalStatus.Connected;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自适应终端
|
||||||
|
const fitTerminal = () => {
|
||||||
|
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
|
||||||
|
if (!dimensions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dimensions?.cols && dimensions?.rows) {
|
||||||
|
term.resize(dimensions.cols, dimensions.rows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const focus = () => {
|
||||||
|
setTimeout(() => term.focus(), 400);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
term.clear();
|
||||||
|
term.clearSelection();
|
||||||
|
term.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
function initSocket() {
|
||||||
|
if (props.socketUrl) {
|
||||||
|
socket = new WebSocket(props.socketUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听socket连接
|
||||||
|
socket.onopen = () => {
|
||||||
|
onConnected();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听socket错误信息
|
||||||
|
socket.onerror = (e: Event) => {
|
||||||
|
term.writeln('\r\n\x1b[31m提示: 连接错误...');
|
||||||
|
state.status = TerminalStatus.Error;
|
||||||
|
console.log('连接错误', e);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = (e: CloseEvent) => {
|
||||||
|
console.log('terminal socket close...', e.reason);
|
||||||
|
// 关闭窗口大小监听器
|
||||||
|
window.removeEventListener('resize', debounce(fitTerminal, 100));
|
||||||
|
// 清除 ping
|
||||||
|
pingInterval && clearInterval(pingInterval);
|
||||||
|
state.status = TerminalStatus.Disconnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听socket消息
|
||||||
|
socket.onmessage = getMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessage(msg: any) {
|
||||||
|
// msg.data是真正后端返回的数据
|
||||||
|
term.write(msg.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MsgType {
|
||||||
|
Resize = 1,
|
||||||
|
Data = 2,
|
||||||
|
Ping = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = (msg: any) => {
|
||||||
|
socket.send(JSON.stringify(msg));
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendResize = (cols: number, rows: number) => {
|
||||||
|
send({
|
||||||
|
type: MsgType.Resize,
|
||||||
|
Cols: cols,
|
||||||
|
Rows: rows,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendPing = () => {
|
||||||
|
send({
|
||||||
|
type: MsgType.Ping,
|
||||||
|
msg: 'ping',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendCmd(key: any) {
|
||||||
|
send({
|
||||||
|
type: MsgType.Data,
|
||||||
|
msg: key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSocket() {
|
||||||
|
// 关闭 websocket
|
||||||
|
socket && socket.readyState === 1 && socket.close();
|
||||||
|
// 清除 ping
|
||||||
|
pingInterval && clearInterval(pingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
console.log('in terminal body close');
|
||||||
|
closeSocket();
|
||||||
|
if (term) {
|
||||||
|
state.addon.search.dispose();
|
||||||
|
state.addon.fit.dispose();
|
||||||
|
state.addon.weblinks.dispose();
|
||||||
|
term.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatus = (): TerminalStatus => {
|
||||||
|
return state.status;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
#terminal-body {
|
||||||
|
background: #212529;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.xterm .xterm-viewport {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
309
mayfly_go_web/src/components/terminal/TerminalDialog.vue
Normal file
309
mayfly_go_web/src/components/terminal/TerminalDialog.vue
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="terminal-dialog-container" v-for="openTerminal of terminals" :key="openTerminal.terminalId">
|
||||||
|
<el-dialog
|
||||||
|
title="终端"
|
||||||
|
v-model="openTerminal.visible"
|
||||||
|
top="32px"
|
||||||
|
class="terminal-dialog"
|
||||||
|
width="75%"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:modal="true"
|
||||||
|
:show-close="false"
|
||||||
|
:fullscreen="openTerminal.fullscreen"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="terminal-title-wrapper">
|
||||||
|
<!-- 左侧 -->
|
||||||
|
<div class="title-left-fixed">
|
||||||
|
<!-- title信息 -->
|
||||||
|
<div>
|
||||||
|
<slot name="headerTitle" :terminalInfo="openTerminal">
|
||||||
|
{{ openTerminal.headerTitle }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧 -->
|
||||||
|
<div class="title-right-fixed">
|
||||||
|
<el-popconfirm @confirm="reConnect(openTerminal.terminalId)" title="确认重新连接?">
|
||||||
|
<template #reference>
|
||||||
|
<div class="mr15 pointer">
|
||||||
|
<el-tag v-if="openTerminal.status == TerminalStatus.Connected" type="success" effect="light" round> 已连接 </el-tag>
|
||||||
|
<el-tag v-else type="danger" effect="light" round> 未连接 </el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
|
||||||
|
<el-popover placement="bottom" :width="200" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon name="QuestionFilled" :size="20" class="pointer-icon mr10" />
|
||||||
|
</template>
|
||||||
|
<div>ctrl | command + f (搜索)</div>
|
||||||
|
<div class="mt5">点击连接状态可重连</div>
|
||||||
|
</el-popover>
|
||||||
|
|
||||||
|
<SvgIcon
|
||||||
|
name="ArrowDown"
|
||||||
|
v-if="props.visibleMinimize"
|
||||||
|
@click="minimize(openTerminal.terminalId)"
|
||||||
|
:size="20"
|
||||||
|
class="pointer-icon mr10"
|
||||||
|
title="最小化"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- <SvgIcon name="FullScreen" @click="handlerFullScreen(openTerminal)" :size="20" class="pointer-icon mr10" title="全屏|退出全屏" /> -->
|
||||||
|
|
||||||
|
<SvgIcon name="Close" class="pointer-icon" @click="close(openTerminal.terminalId)" title="关闭" :size="20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="terminal-wrapper" style="height: calc(100vh - 215px)">
|
||||||
|
<TerminalBody
|
||||||
|
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
|
||||||
|
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"
|
||||||
|
:cmd="openTerminal.cmd"
|
||||||
|
:socket-url="openTerminal.socketUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 终端最小化 -->
|
||||||
|
<div class="terminal-minimize-container">
|
||||||
|
<el-card
|
||||||
|
v-for="minimizeTerminal of minimizeTerminals"
|
||||||
|
:key="minimizeTerminal.terminalId"
|
||||||
|
:class="`terminal-minimize-item pointer ${minimizeTerminal.styleClass}`"
|
||||||
|
size="small"
|
||||||
|
@click="maximize(minimizeTerminal.terminalId)"
|
||||||
|
>
|
||||||
|
<el-tooltip effect="customized" :content="minimizeTerminal.desc" placement="top">
|
||||||
|
<span>
|
||||||
|
{{ minimizeTerminal.title }}
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<SvgIcon name="CloseBold" @click.stop="closeMinimizeTerminal(minimizeTerminal.terminalId)" class="ml10 pointer-icon fr" :size="20" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive } from 'vue';
|
||||||
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { TerminalStatus } from './common';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visibleMinimize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'minimize']);
|
||||||
|
|
||||||
|
const openTerminalRefs: any = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
terminal对象信息:
|
||||||
|
|
||||||
|
visible: false,
|
||||||
|
machineId: null as any,
|
||||||
|
terminalId: null as any,
|
||||||
|
machine: {} as any,
|
||||||
|
fullscreen: false,
|
||||||
|
*/
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
terminals: {} as any, // key -> terminalId value -> terminal
|
||||||
|
minimizeTerminals: {} as any, // key -> terminalId value -> 简易terminal
|
||||||
|
});
|
||||||
|
|
||||||
|
const { terminals, minimizeTerminals } = toRefs(state);
|
||||||
|
|
||||||
|
const setTerminalRef = (el: any, terminalId: any) => {
|
||||||
|
if (terminalId) {
|
||||||
|
openTerminalRefs[terminalId] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function open(terminalInfo: any, cmd: string = '') {
|
||||||
|
let terminalId = terminalInfo.terminalId;
|
||||||
|
if (!terminalId) {
|
||||||
|
terminalId = Date.now();
|
||||||
|
}
|
||||||
|
state.terminals[terminalId] = {
|
||||||
|
...terminalInfo,
|
||||||
|
terminalId,
|
||||||
|
visible: true,
|
||||||
|
cmd,
|
||||||
|
status: TerminalStatus.NoConnected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalStatusChange = (terminalId: string, status: TerminalStatus) => {
|
||||||
|
const terminal = state.terminals[terminalId];
|
||||||
|
if (terminal) {
|
||||||
|
terminal.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minTerminal = state.minimizeTerminals[terminalId];
|
||||||
|
if (!minTerminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
minTerminal.styleClass = getTerminalStatysStyleClass(terminalId, status);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTerminalStatysStyleClass = (terminalId: any, status: any = null) => {
|
||||||
|
if (status == null) {
|
||||||
|
status = openTerminalRefs[terminalId].getStatus();
|
||||||
|
}
|
||||||
|
if (status == TerminalStatus.Connected) {
|
||||||
|
return 'terminal-status-success';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == TerminalStatus.NoConnected) {
|
||||||
|
return 'terminal-status-no-connect';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'terminal-status-error';
|
||||||
|
};
|
||||||
|
|
||||||
|
const reConnect = (terminalId: any) => {
|
||||||
|
openTerminalRefs[terminalId].init();
|
||||||
|
};
|
||||||
|
|
||||||
|
function close(terminalId: any) {
|
||||||
|
console.log('in terminal dialog close');
|
||||||
|
delete state.terminals[terminalId];
|
||||||
|
|
||||||
|
// 关闭终端,并删除终端ref
|
||||||
|
const terminalRef = openTerminalRefs[terminalId];
|
||||||
|
terminalRef && terminalRef.close();
|
||||||
|
delete openTerminalRefs[terminalId];
|
||||||
|
|
||||||
|
emit('close', terminalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function minimize(terminalId: number) {
|
||||||
|
console.log('in terminal dialog minimize: ', terminalId);
|
||||||
|
|
||||||
|
const terminal = state.terminals[terminalId];
|
||||||
|
if (!terminal) {
|
||||||
|
console.warn('不存在该终端信息: ', terminalId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
terminal.visible = false;
|
||||||
|
|
||||||
|
const minTerminalInfo = {
|
||||||
|
terminalId: terminal.terminalId,
|
||||||
|
title: terminal.minTitle, // 截取terminalId最后两位区分多个terminal
|
||||||
|
desc: terminal.minDesc,
|
||||||
|
styleClass: getTerminalStatysStyleClass(terminalId),
|
||||||
|
};
|
||||||
|
state.minimizeTerminals[terminalId] = minTerminalInfo;
|
||||||
|
|
||||||
|
emit('minimize', minTerminalInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maximize(terminalId: any) {
|
||||||
|
console.log('in terminal dialog maximize: ', terminalId);
|
||||||
|
const minTerminal = state.minimizeTerminals[terminalId];
|
||||||
|
if (!minTerminal) {
|
||||||
|
console.log('no min terminal...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete state.minimizeTerminals[terminalId];
|
||||||
|
|
||||||
|
// 显示终端信息
|
||||||
|
state.terminals[terminalId].visible = true;
|
||||||
|
|
||||||
|
const terminalRef = openTerminalRefs[terminalId];
|
||||||
|
// fit
|
||||||
|
setTimeout(() => {
|
||||||
|
terminalRef.fitTerminal();
|
||||||
|
terminalRef.focus();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMinimizeTerminal = (terminalId: any) => {
|
||||||
|
delete state.minimizeTerminals[terminalId];
|
||||||
|
close(terminalId);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
minimize,
|
||||||
|
maximize,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.terminal-dialog-container {
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .terminal-dialog {
|
||||||
|
// height: calc(100vh - 200px) !important;
|
||||||
|
// }
|
||||||
|
|
||||||
|
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.title-right-fixed {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-minimize-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 16px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap-reverse;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.terminal-minimize-item {
|
||||||
|
min-width: 120px;
|
||||||
|
// box-shadow: 0 3px 4px #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 1px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-status-error {
|
||||||
|
box-shadow: 0 3px 4px var(--color-danger);
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-status-no-connect {
|
||||||
|
box-shadow: 0 3px 4px var(--color-warning);
|
||||||
|
border-color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-status-success {
|
||||||
|
box-shadow: 0 3px 4px var(--color-success);
|
||||||
|
border-color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card__body {
|
||||||
|
padding: 15px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
151
mayfly_go_web/src/components/terminal/TerminalSearch.vue
Normal file
151
mayfly_go_web/src/components/terminal/TerminalSearch.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div id="search-card" v-show="search.visible" @keydown.esc="closeSearch">
|
||||||
|
<el-card title="搜索" size="small">
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<el-input
|
||||||
|
class="search-input"
|
||||||
|
ref="searchInputRef"
|
||||||
|
placeholder="请输入查找内容,回车搜索"
|
||||||
|
v-model="search.value"
|
||||||
|
@keyup.enter.native="searchKeywords(true)"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
</el-input>
|
||||||
|
<!-- 选项 -->
|
||||||
|
<div class="search-options">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-checkbox class="usn" v-model="search.regex"> 正则匹配 </el-checkbox>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-checkbox class="usn" v-model="search.words"> 单词全匹配 </el-checkbox>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-checkbox class="usn" v-model="search.matchCase"> 区分大小写 </el-checkbox>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-checkbox class="usn" v-model="search.incremental"> 增量查找 </el-checkbox>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<div class="search-buttons">
|
||||||
|
<el-button class="terminal-search-button search-button-prev" type="primary" size="small" @click="searchKeywords(false)"> 上一个 </el-button>
|
||||||
|
<el-button class="terminal-search-button search-button-next" type="primary" size="small" @click="searchKeywords(true)"> 下一个 </el-button>
|
||||||
|
<el-button class="terminal-search-button search-button-next" type="primary" size="small" @click="closeSearch"> 关闭 </el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, nextTick, reactive } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
searchAddon: {
|
||||||
|
type: [SearchAddon],
|
||||||
|
require: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
search: {
|
||||||
|
visible: false,
|
||||||
|
value: '',
|
||||||
|
regex: false,
|
||||||
|
words: false,
|
||||||
|
matchCase: false,
|
||||||
|
incremental: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { search } = toRefs(state);
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const searchInputRef: any = ref(null);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
const visible = state.search.visible;
|
||||||
|
state.search.visible = !visible;
|
||||||
|
console.log(state.search.visible);
|
||||||
|
if (!visible) {
|
||||||
|
nextTick(() => {
|
||||||
|
searchInputRef.value.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSearch() {
|
||||||
|
state.search.visible = false;
|
||||||
|
state.search.value = '';
|
||||||
|
props.searchAddon?.clearDecorations();
|
||||||
|
// 取消查询关键字高亮
|
||||||
|
props.searchAddon?.clearActiveDecoration();
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchKeywords(direction: any) {
|
||||||
|
if (!state.search.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const option = {
|
||||||
|
regex: state.search.regex,
|
||||||
|
wholeWord: state.search.words,
|
||||||
|
caseSensitive: state.search.matchCase,
|
||||||
|
incremental: state.search.incremental,
|
||||||
|
};
|
||||||
|
let res;
|
||||||
|
if (direction) {
|
||||||
|
res = props.searchAddon?.findNext(state.search.value, getSearchOptions(option));
|
||||||
|
} else {
|
||||||
|
res = props.searchAddon?.findPrevious(state.search.value, getSearchOptions(option));
|
||||||
|
}
|
||||||
|
if (!res) {
|
||||||
|
ElMessage.info('未查询到匹配项');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchOptions = (searchOptions?: ISearchOptions): ISearchOptions => {
|
||||||
|
return {
|
||||||
|
...searchOptions,
|
||||||
|
decorations: {
|
||||||
|
matchOverviewRuler: '#888888',
|
||||||
|
activeMatchColorOverviewRuler: '#ffff00',
|
||||||
|
matchBackground: '#888888',
|
||||||
|
activeMatchBackground: '#ffff00',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#search-card {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1200;
|
||||||
|
width: 270px;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-options {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-search-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
mayfly_go_web/src/components/terminal/common.ts
Normal file
6
mayfly_go_web/src/components/terminal/common.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export enum TerminalStatus {
|
||||||
|
Error = -1,
|
||||||
|
NoConnected = 0,
|
||||||
|
Connected = 1,
|
||||||
|
Disconnected = 2,
|
||||||
|
}
|
||||||
@@ -345,3 +345,15 @@ body,
|
|||||||
.f12 {
|
.f12 {
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
.pointer-icon:hover {
|
||||||
|
color: var(--color-primary); /* 鼠标移动到图标时的颜色 */
|
||||||
|
}
|
||||||
@@ -54,7 +54,10 @@
|
|||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<span v-auth="'machine:terminal'">
|
<span v-auth="'machine:terminal'">
|
||||||
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data)" link>终端</el-button>
|
<el-tooltip effect="customized" content="按住ctrl则为新标签打开" placement="top">
|
||||||
|
<el-button :disabled="data.status == -1" type="primary" @click="showTerminal(data, $event)" link>终端</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -126,6 +129,16 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<terminal-dialog ref="terminalDialogRef" :visibleMinimize="true">
|
||||||
|
<template #headerTitle="{ terminalInfo }">
|
||||||
|
{{ `${(terminalInfo.terminalId + '').slice(-2)}` }}
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
{{ `${terminalInfo.meta.username}@${terminalInfo.meta.ip}:${terminalInfo.meta.port}` }}
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
{{ terminalInfo.meta.name }}
|
||||||
|
</template>
|
||||||
|
</terminal-dialog>
|
||||||
|
|
||||||
<machine-edit
|
<machine-edit
|
||||||
:title="machineEditDialog.title"
|
:title="machineEditDialog.title"
|
||||||
v-model:visible="machineEditDialog.visible"
|
v-model:visible="machineEditDialog.visible"
|
||||||
@@ -146,10 +159,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
|
import { ref, toRefs, reactive, onMounted, defineAsyncComponent, nextTick } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { machineApi } from './api';
|
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
import TagInfo from '../component/TagInfo.vue';
|
import TagInfo from '../component/TagInfo.vue';
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
@@ -157,6 +170,7 @@ import { TableColumn, TableQuery } from '@/components/pagetable';
|
|||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
|
|
||||||
// 组件
|
// 组件
|
||||||
|
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
|
||||||
const MachineEdit = defineAsyncComponent(() => import('./MachineEdit.vue'));
|
const MachineEdit = defineAsyncComponent(() => import('./MachineEdit.vue'));
|
||||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||||
const FileManage = defineAsyncComponent(() => import('./FileManage.vue'));
|
const FileManage = defineAsyncComponent(() => import('./FileManage.vue'));
|
||||||
@@ -166,6 +180,7 @@ const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pageTableRef: any = ref(null);
|
const pageTableRef: any = ref(null);
|
||||||
|
const terminalDialogRef: any = ref(null);
|
||||||
|
|
||||||
const perms = {
|
const perms = {
|
||||||
addMachine: 'machine:add',
|
addMachine: 'machine:add',
|
||||||
@@ -180,12 +195,13 @@ const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), Tabl
|
|||||||
const columns = ref([
|
const columns = ref([
|
||||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||||
TableColumn.new('name', '名称'),
|
TableColumn.new('name', '名称'),
|
||||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(35),
|
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(45),
|
||||||
TableColumn.new('username', '用户名'),
|
TableColumn.new('username', '用户名'),
|
||||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||||
TableColumn.new('remark', '备注'),
|
TableColumn.new('remark', '备注'),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||||
|
|
||||||
@@ -275,7 +291,9 @@ const handleCommand = (commond: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showTerminal = (row: any) => {
|
const showTerminal = (row: any, event: PointerEvent) => {
|
||||||
|
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
const { href } = router.resolve({
|
const { href } = router.resolve({
|
||||||
path: `/machine/terminal`,
|
path: `/machine/terminal`,
|
||||||
query: {
|
query: {
|
||||||
@@ -284,6 +302,17 @@ const showTerminal = (row: any) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.open(href, '_blank');
|
window.open(href, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalId = Date.now();
|
||||||
|
terminalDialogRef.value.open({
|
||||||
|
terminalId,
|
||||||
|
socketUrl: getMachineTerminalSocketUrl(row.id),
|
||||||
|
minTitle: `${row.name} [${(terminalId + '').slice(-2)}]`, // 截取terminalId最后两位区分多个terminal
|
||||||
|
minDesc: `${row.username}@${row.ip}:${row.port} (${row.name})`,
|
||||||
|
meta: row,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeCli = async (row: any) => {
|
const closeCli = async (row: any) => {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
:show-close="true"
|
:show-close="true"
|
||||||
:before-close="handleClose"
|
:before-close="handleClose"
|
||||||
width="55%"
|
width="55%"
|
||||||
|
draggable
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
@@ -89,8 +91,10 @@
|
|||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:modal="false"
|
:modal="false"
|
||||||
@close="closeTermnial"
|
@close="closeTermnial"
|
||||||
|
draggable
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="560px" />
|
<TerminalBody ref="terminal" :cmd="terminalDialog.cmd" :socket-url="getMachineTerminalSocketUrl(terminalDialog.machineId)" height="560px" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<script-edit
|
<script-edit
|
||||||
@@ -107,8 +111,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRefs, reactive, watch } from 'vue';
|
import { ref, toRefs, reactive, watch } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import SshTerminal from './SshTerminal.vue';
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
import { machineApi } from './api';
|
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||||
import { ScriptResultEnum, ScriptTypeEnum } from './enums';
|
import { ScriptResultEnum, ScriptTypeEnum } from './enums';
|
||||||
import ScriptEdit from './ScriptEdit.vue';
|
import ScriptEdit from './ScriptEdit.vue';
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
@@ -313,4 +317,4 @@ const handleClose = () => {
|
|||||||
state.scriptParamsDialog.paramsFormItem = [];
|
state.scriptParamsDialog.paramsFormItem = [];
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="sass"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :style="{ height: props.height }" id="xterm" class="xterm" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import 'xterm/css/xterm.css';
|
|
||||||
import { Terminal } from 'xterm';
|
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
|
||||||
import { getSession } from '@/common/utils/storage';
|
|
||||||
import config from '@/common/config';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useThemeConfig } from '@/store/themeConfig';
|
|
||||||
import { nextTick, reactive, onMounted, onBeforeUnmount } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
machineId: { type: Number },
|
|
||||||
cmd: { type: String },
|
|
||||||
height: { type: [String, Number] },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
|
||||||
const state = reactive({
|
|
||||||
cmd: '',
|
|
||||||
term: null as any,
|
|
||||||
socket: null as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resize = 1;
|
|
||||||
const data = 2;
|
|
||||||
const ping = 3;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
state.cmd = props.cmd as any;
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
initXterm();
|
|
||||||
initSocket();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
closeAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
function initXterm() {
|
|
||||||
const term: any = new Terminal({
|
|
||||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
|
||||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
|
||||||
fontFamily: 'JetBrainsMono, monaco, Consolas, Lucida Console, monospace',
|
|
||||||
cursorBlink: true,
|
|
||||||
disableStdin: false,
|
|
||||||
theme: {
|
|
||||||
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
|
||||||
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
|
||||||
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
|
||||||
// cursorAccent: "red", // 光标停止颜色
|
|
||||||
} as any,
|
|
||||||
});
|
|
||||||
const fitAddon = new FitAddon();
|
|
||||||
term.loadAddon(fitAddon);
|
|
||||||
term.open(document.getElementById('xterm'));
|
|
||||||
fitAddon.fit();
|
|
||||||
term.focus();
|
|
||||||
state.term = term;
|
|
||||||
|
|
||||||
// 监听窗口resize
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
try {
|
|
||||||
// 窗口大小改变时,触发xterm的resize方法使自适应
|
|
||||||
fitAddon.fit();
|
|
||||||
if (state.term) {
|
|
||||||
state.term.focus();
|
|
||||||
send({
|
|
||||||
type: resize,
|
|
||||||
Cols: parseInt(state.term.cols),
|
|
||||||
Rows: parseInt(state.term.rows),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// / **
|
|
||||||
// *添加事件监听器,用于按下键时的事件。事件值包含
|
|
||||||
// *将在data事件以及DOM事件中发送的字符串
|
|
||||||
// *触发了它。
|
|
||||||
// * @返回一个IDisposable停止监听。
|
|
||||||
// * /
|
|
||||||
// / ** 更新:xterm 4.x(新增)
|
|
||||||
// *为数据事件触发时添加事件侦听器。发生这种情况
|
|
||||||
// *用户输入或粘贴到终端时的示例。事件值
|
|
||||||
// *是`string`结果的结果,在典型的设置中,应该通过
|
|
||||||
// *到支持pty。
|
|
||||||
// * @返回一个IDisposable停止监听。
|
|
||||||
// * /
|
|
||||||
// 支持输入与粘贴方法
|
|
||||||
term.onData((key: any) => {
|
|
||||||
sendCmd(key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let pingInterval: any;
|
|
||||||
function initSocket() {
|
|
||||||
state.socket = new WebSocket(
|
|
||||||
`${config.baseWsUrl}/machines/${props.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${state.term.rows}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// 监听socket连接
|
|
||||||
state.socket.onopen = () => {
|
|
||||||
// 如果有初始要执行的命令,则发送执行命令
|
|
||||||
if (state.cmd) {
|
|
||||||
sendCmd(state.cmd + ' \r');
|
|
||||||
}
|
|
||||||
// 开启心跳
|
|
||||||
pingInterval = setInterval(() => {
|
|
||||||
send({ type: ping, msg: 'ping' });
|
|
||||||
}, 8000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听socket错误信息
|
|
||||||
state.socket.onerror = (e: any) => {
|
|
||||||
console.log('连接错误', e);
|
|
||||||
};
|
|
||||||
|
|
||||||
state.socket.onclose = () => {
|
|
||||||
if (state.term) {
|
|
||||||
state.term.writeln('\r\n\x1b[31m提示: 连接已关闭...');
|
|
||||||
}
|
|
||||||
if (pingInterval) {
|
|
||||||
clearInterval(pingInterval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 发送socket消息
|
|
||||||
state.socket.onsend = send;
|
|
||||||
|
|
||||||
// 监听socket消息
|
|
||||||
state.socket.onmessage = getMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMessage(msg: any) {
|
|
||||||
// msg.data是真正后端返回的数据
|
|
||||||
state.term.write(msg.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(msg: any) {
|
|
||||||
state.socket.send(JSON.stringify(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendCmd(key: any) {
|
|
||||||
send({
|
|
||||||
type: data,
|
|
||||||
msg: key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
if (state.socket) {
|
|
||||||
state.socket.close();
|
|
||||||
console.log('socket关闭');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAll() {
|
|
||||||
close();
|
|
||||||
if (state.term) {
|
|
||||||
state.term.dispose();
|
|
||||||
state.term = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style lang="scss">
|
|
||||||
#xterm {
|
|
||||||
.xterm-viewport {
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,25 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="terminal-wrapper">
|
||||||
<ssh-terminal ref="terminal" :machineId="machineId" :height="height + 'px'" />
|
<TerminalBody :socket-url="getMachineTerminalSocketUrl(route.query.id)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import SshTerminal from './SshTerminal.vue';
|
|
||||||
import { reactive, toRefs, onMounted } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
|
import { getMachineTerminalSocketUrl } from './api';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const state = reactive({
|
|
||||||
machineId: 0,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { machineId, height } = toRefs(state);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
state.height = window.innerHeight + 5;
|
|
||||||
state.machineId = Number.parseInt(route.query.id as string);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss">
|
||||||
|
.terminal-wrapper {
|
||||||
|
height: calc(100vh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import Api from '@/common/Api';
|
import Api from '@/common/Api';
|
||||||
|
import config from '@/common/config';
|
||||||
|
import { getSession } from '@/common/utils/storage';
|
||||||
|
|
||||||
export const machineApi = {
|
export const machineApi = {
|
||||||
// 获取权限列表
|
// 获取权限列表
|
||||||
@@ -56,3 +58,7 @@ export const cronJobApi = {
|
|||||||
delete: Api.newDelete('/machine-cronjobs/{id}'),
|
delete: Api.newDelete('/machine-cronjobs/{id}'),
|
||||||
execList: Api.newGet('/machine-cronjobs/execs'),
|
execList: Api.newGet('/machine-cronjobs/execs'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getMachineTerminalSocketUrl(machineId: any) {
|
||||||
|
return `${config.baseWsUrl}/machines/${machineId}/terminal?token=${getSession('token')}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -601,10 +601,10 @@ asynckit@^0.4.0:
|
|||||||
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
|
resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz"
|
||||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||||
|
|
||||||
axios@^1.4.0:
|
axios@^1.5.0:
|
||||||
version "1.4.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
|
resolved "https://registry.npmmirror.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267"
|
||||||
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
|
integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "^1.15.0"
|
follow-redirects "^1.15.0"
|
||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
@@ -1949,6 +1949,21 @@ xterm-addon-fit@^0.7.0:
|
|||||||
resolved "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz#b8ade6d96e63b47443862088f6670b49fb752c6a"
|
resolved "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz#b8ade6d96e63b47443862088f6670b49fb752c6a"
|
||||||
integrity sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==
|
integrity sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==
|
||||||
|
|
||||||
|
xterm-addon-search@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.npmmirror.com/xterm-addon-search/-/xterm-addon-search-0.12.0.tgz#2ef8f56aecf699a3989223a1260f1e079d7c74e2"
|
||||||
|
integrity sha512-hXAuO7Ts2+Jf9K8mZrUx8IFd7c/Flgks/jyqA1L4reymyfmXtcsd+WDLel8R9Tgy2CLyKABVBP09/Ua/FmXcvg==
|
||||||
|
|
||||||
|
xterm-addon-web-links@^0.8.0:
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.npmmirror.com/xterm-addon-web-links/-/xterm-addon-web-links-0.8.0.tgz#2cb1d57129271022569208578b0bf4774e7e6ea9"
|
||||||
|
integrity sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==
|
||||||
|
|
||||||
|
xterm-addon-webgl@^0.15.0:
|
||||||
|
version "0.15.0"
|
||||||
|
resolved "https://registry.npmmirror.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0.tgz#c10f93ca619524f5a470eaac44258bab0ae8e3c7"
|
||||||
|
integrity sha512-ZLcqogMFHr4g/YRhcCh3xE8tTklnyut/M+O/XhVsFBRB/YCvYhPdLQ5/AQk54V0wjWAQpa8CF3W8DVR9OqyMCg==
|
||||||
|
|
||||||
xterm@^5.2.1:
|
xterm@^5.2.1:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.npmmirror.com/xterm/-/xterm-5.2.1.tgz#b3fea7bdb55b9be1d4b31f4cd1091f26ac42afb8"
|
resolved "https://registry.npmmirror.com/xterm/-/xterm-5.2.1.tgz#b3fea7bdb55b9be1d4b31f4cd1091f26ac42afb8"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mayfly-go/internal/auth/api/form"
|
"mayfly-go/internal/auth/api/form"
|
||||||
|
"mayfly-go/internal/auth/config"
|
||||||
"mayfly-go/internal/common/utils"
|
"mayfly-go/internal/common/utils"
|
||||||
msgapp "mayfly-go/internal/msg/application"
|
msgapp "mayfly-go/internal/msg/application"
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
@@ -22,7 +23,6 @@ import (
|
|||||||
type AccountLogin struct {
|
type AccountLogin struct {
|
||||||
AccountApp sysapp.Account
|
AccountApp sysapp.Account
|
||||||
MsgApp msgapp.Msg
|
MsgApp msgapp.Msg
|
||||||
ConfigApp sysapp.Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用户账号密码登录 **/
|
/** 用户账号密码登录 **/
|
||||||
@@ -31,7 +31,7 @@ type AccountLogin struct {
|
|||||||
func (a *AccountLogin) Login(rc *req.Ctx) {
|
func (a *AccountLogin) Login(rc *req.Ctx) {
|
||||||
loginForm := ginx.BindJsonAndValid(rc.GinCtx, new(form.LoginForm))
|
loginForm := ginx.BindJsonAndValid(rc.GinCtx, new(form.LoginForm))
|
||||||
|
|
||||||
accountLoginSecurity := a.ConfigApp.GetConfig(sysentity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity()
|
accountLoginSecurity := config.GetAccountLoginSecurity()
|
||||||
// 判断是否有开启登录验证码校验
|
// 判断是否有开启登录验证码校验
|
||||||
if accountLoginSecurity.UseCaptcha {
|
if accountLoginSecurity.UseCaptcha {
|
||||||
// 校验验证码
|
// 校验验证码
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mayfly-go/internal/auth/config"
|
||||||
msgapp "mayfly-go/internal/msg/application"
|
msgapp "mayfly-go/internal/msg/application"
|
||||||
msgentity "mayfly-go/internal/msg/domain/entity"
|
msgentity "mayfly-go/internal/msg/domain/entity"
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
@@ -24,7 +25,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 最后的登录校验(共用)。校验通过返回登录成功响应结果map
|
// 最后的登录校验(共用)。校验通过返回登录成功响应结果map
|
||||||
func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *sysentity.AccountLoginSecurity, loginIp string) map[string]any {
|
func LastLoginCheck(account *sysentity.Account, accountLoginSecurity *config.AccountLoginSecurity, loginIp string) map[string]any {
|
||||||
biz.IsTrue(account.IsEnable(), "该账号不可用")
|
biz.IsTrue(account.IsEnable(), "该账号不可用")
|
||||||
username := account.Username
|
username := account.Username
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-ldap/ldap/v3"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"mayfly-go/internal/auth/api/form"
|
"mayfly-go/internal/auth/api/form"
|
||||||
|
"mayfly-go/internal/auth/config"
|
||||||
msgapp "mayfly-go/internal/msg/application"
|
msgapp "mayfly-go/internal/msg/application"
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||||
@@ -19,17 +17,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LdapLogin struct {
|
type LdapLogin struct {
|
||||||
AccountApp sysapp.Account
|
AccountApp sysapp.Account
|
||||||
MsgApp msgapp.Msg
|
MsgApp msgapp.Msg
|
||||||
ConfigApp sysapp.Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @router /auth/ldap/enabled [get]
|
// @router /auth/ldap/enabled [get]
|
||||||
func (a *LdapLogin) GetLdapEnabled(rc *req.Ctx) {
|
func (a *LdapLogin) GetLdapEnabled(rc *req.Ctx) {
|
||||||
ldapLoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyLdapLogin).ToLdapLogin()
|
ldapLoginConfig := config.GetLdapLogin()
|
||||||
rc.ResData = ldapLoginConfig.Enable
|
rc.ResData = ldapLoginConfig.Enable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ func (a *LdapLogin) GetLdapEnabled(rc *req.Ctx) {
|
|||||||
func (a *LdapLogin) Login(rc *req.Ctx) {
|
func (a *LdapLogin) Login(rc *req.Ctx) {
|
||||||
loginForm := ginx.BindJsonAndValid(rc.GinCtx, new(form.LoginForm))
|
loginForm := ginx.BindJsonAndValid(rc.GinCtx, new(form.LoginForm))
|
||||||
|
|
||||||
accountLoginSecurity := a.ConfigApp.GetConfig(sysentity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity()
|
accountLoginSecurity := config.GetAccountLoginSecurity()
|
||||||
// 判断是否有开启登录验证码校验
|
// 判断是否有开启登录验证码校验
|
||||||
if accountLoginSecurity.UseCaptcha {
|
if accountLoginSecurity.UseCaptcha {
|
||||||
// 校验验证码
|
// 校验验证码
|
||||||
@@ -115,7 +116,7 @@ type UserInfo struct {
|
|||||||
|
|
||||||
// Authenticate 通过 LDAP 验证用户名密码
|
// Authenticate 通过 LDAP 验证用户名密码
|
||||||
func Authenticate(username, password string) (*UserInfo, error) {
|
func Authenticate(username, password string) (*UserInfo, error) {
|
||||||
ldapConf := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyLdapLogin).ToLdapLogin()
|
ldapConf := config.GetLdapLogin()
|
||||||
if !ldapConf.Enable {
|
if !ldapConf.Enable {
|
||||||
return nil, errors.Errorf("未启用 LDAP 登录")
|
return nil, errors.Errorf("未启用 LDAP 登录")
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,7 @@ func Authenticate(username, password string) (*UserInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect 创建 LDAP 连接
|
// Connect 创建 LDAP 连接
|
||||||
func Connect(ldapConf *sysentity.ConfigLdapLogin) (*ldap.Conn, error) {
|
func Connect(ldapConf *config.LdapLogin) (*ldap.Conn, error) {
|
||||||
conn, err := dial(ldapConf)
|
conn, err := dial(ldapConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -178,7 +179,7 @@ func Connect(ldapConf *sysentity.ConfigLdapLogin) (*ldap.Conn, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dial(ldapConf *sysentity.ConfigLdapLogin) (*ldap.Conn, error) {
|
func dial(ldapConf *config.LdapLogin) (*ldap.Conn, error) {
|
||||||
addr := fmt.Sprintf("%s:%s", ldapConf.Host, ldapConf.Port)
|
addr := fmt.Sprintf("%s:%s", ldapConf.Host, ldapConf.Port)
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
ServerName: ldapConf.Host,
|
ServerName: ldapConf.Host,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mayfly-go/internal/auth/api/vo"
|
"mayfly-go/internal/auth/api/vo"
|
||||||
"mayfly-go/internal/auth/application"
|
"mayfly-go/internal/auth/application"
|
||||||
|
"mayfly-go/internal/auth/config"
|
||||||
"mayfly-go/internal/auth/domain/entity"
|
"mayfly-go/internal/auth/domain/entity"
|
||||||
msgapp "mayfly-go/internal/msg/application"
|
msgapp "mayfly-go/internal/msg/application"
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
@@ -25,7 +26,6 @@ import (
|
|||||||
|
|
||||||
type Oauth2Login struct {
|
type Oauth2Login struct {
|
||||||
Oauth2App application.Oauth2
|
Oauth2App application.Oauth2
|
||||||
ConfigApp sysapp.Config
|
|
||||||
AccountApp sysapp.Account
|
AccountApp sysapp.Account
|
||||||
MsgApp msgapp.Msg
|
MsgApp msgapp.Msg
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 指定登录操作
|
// 指定登录操作
|
||||||
func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *sysentity.ConfigOauth2Login) {
|
func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oauth2Login) {
|
||||||
// 查询用户是否存在
|
// 查询用户是否存在
|
||||||
oauthAccount := &entity.Oauth2Account{Identity: userId}
|
oauthAccount := &entity.Oauth2Account{Identity: userId}
|
||||||
err := a.Oauth2App.GetOAuthAccount(oauthAccount, "account_id", "identity")
|
err := a.Oauth2App.GetOAuthAccount(oauthAccount, "account_id", "identity")
|
||||||
@@ -175,14 +175,14 @@ func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *sysentity
|
|||||||
clientIp := getIpAndRegion(rc)
|
clientIp := getIpAndRegion(rc)
|
||||||
rc.ReqParam = fmt.Sprintf("oauth2 login username: %s | ip: %s", account.Username, clientIp)
|
rc.ReqParam = fmt.Sprintf("oauth2 login username: %s | ip: %s", account.Username, clientIp)
|
||||||
|
|
||||||
res := LastLoginCheck(account, a.ConfigApp.GetConfig(sysentity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity(), clientIp)
|
res := LastLoginCheck(account, config.GetAccountLoginSecurity(), clientIp)
|
||||||
res["action"] = "oauthLogin"
|
res["action"] = "oauthLogin"
|
||||||
res["isFirstOauth2Login"] = isFirst
|
res["isFirstOauth2Login"] = isFirst
|
||||||
rc.ResData = res
|
rc.ResData = res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *sysentity.ConfigOauth2Login) {
|
func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *config.Oauth2Login) {
|
||||||
oath2LoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyOauth2Login).ToOauth2Login()
|
oath2LoginConfig := config.GetOauth2Login()
|
||||||
biz.IsTrue(oath2LoginConfig.Enable, "请先配置oauth2或启用oauth2登录")
|
biz.IsTrue(oath2LoginConfig.Enable, "请先配置oauth2或启用oauth2登录")
|
||||||
biz.IsTrue(oath2LoginConfig.ClientId != "", "oauth2 clientId不能为空")
|
biz.IsTrue(oath2LoginConfig.ClientId != "", "oauth2 clientId不能为空")
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *sysentity.ConfigOauth2L
|
|||||||
|
|
||||||
func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
|
func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
|
||||||
res := &vo.Oauth2Status{}
|
res := &vo.Oauth2Status{}
|
||||||
oauth2LoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyOauth2Login).ToOauth2Login()
|
oauth2LoginConfig := config.GetOauth2Login()
|
||||||
res.Enable = oauth2LoginConfig.Enable
|
res.Enable = oauth2LoginConfig.Enable
|
||||||
if res.Enable {
|
if res.Enable {
|
||||||
err := a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
|
err := a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
|
||||||
@@ -216,3 +216,12 @@ func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
|
|||||||
func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
|
func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
|
||||||
a.Oauth2App.Unbind(rc.LoginAccount.Id)
|
a.Oauth2App.Unbind(rc.LoginAccount.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取oauth2登录配置信息,因为有些字段是敏感字段,故单独使用接口获取
|
||||||
|
func (c *Oauth2Login) Oauth2Config(rc *req.Ctx) {
|
||||||
|
oauth2LoginConfig := config.GetOauth2Login()
|
||||||
|
rc.ResData = map[string]any{
|
||||||
|
"enable": oauth2LoginConfig.Enable,
|
||||||
|
"name": oauth2LoginConfig.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
105
server/internal/auth/config/config.go
Normal file
105
server/internal/auth/config/config.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
|
"mayfly-go/pkg/utils/stringx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigKeyAccountLoginSecurity string = "AccountLoginSecurity" // 账号登录安全配置
|
||||||
|
ConfigKeyOauth2Login string = "Oauth2Login" // oauth2认证登录配置
|
||||||
|
ConfigKeyLdapLogin string = "LdapLogin" // ldap登录配置
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountLoginSecurity struct {
|
||||||
|
UseCaptcha bool // 是否使用登录验证码
|
||||||
|
UseOtp bool // 是否双因素校验
|
||||||
|
OtpIssuer string // otp发行人
|
||||||
|
LoginFailCount int // 允许失败次数
|
||||||
|
LoginFailMin int // 登录失败指定次数后禁止的分钟数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取账号登录安全相关配置
|
||||||
|
func GetAccountLoginSecurity() *AccountLoginSecurity {
|
||||||
|
c := sysapp.GetConfigApp().GetConfig(ConfigKeyAccountLoginSecurity)
|
||||||
|
jm := c.GetJsonMap()
|
||||||
|
als := new(AccountLoginSecurity)
|
||||||
|
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
|
||||||
|
als.UseOtp = c.ConvBool(jm["useOtp"], false)
|
||||||
|
als.LoginFailCount = c.ConvInt(jm["loginFailCount"], 5)
|
||||||
|
als.LoginFailMin = c.ConvInt(jm["loginFailMin"], 10)
|
||||||
|
otpIssuer := jm["otpIssuer"]
|
||||||
|
if otpIssuer == "" {
|
||||||
|
otpIssuer = "mayfly-go"
|
||||||
|
}
|
||||||
|
als.OtpIssuer = otpIssuer
|
||||||
|
return als
|
||||||
|
}
|
||||||
|
|
||||||
|
type Oauth2Login struct {
|
||||||
|
Enable bool // 是否启用
|
||||||
|
Name string
|
||||||
|
ClientId string `json:"clientId"`
|
||||||
|
ClientSecret string `json:"clientSecret"`
|
||||||
|
AuthorizationURL string `json:"authorizationURL"`
|
||||||
|
AccessTokenURL string `json:"accessTokenURL"`
|
||||||
|
RedirectURL string `json:"redirectURL"`
|
||||||
|
Scopes string `json:"scopes"`
|
||||||
|
ResourceURL string `json:"resourceURL"`
|
||||||
|
UserIdentifier string `json:"userIdentifier"`
|
||||||
|
AutoRegister bool `json:"autoRegister"` // 是否自动注册
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Oauth2登录相关配置
|
||||||
|
func GetOauth2Login() *Oauth2Login {
|
||||||
|
c := sysapp.GetConfigApp().GetConfig(ConfigKeyOauth2Login)
|
||||||
|
jm := c.GetJsonMap()
|
||||||
|
ol := new(Oauth2Login)
|
||||||
|
ol.Enable = c.ConvBool(jm["enable"], false)
|
||||||
|
ol.Name = jm["name"]
|
||||||
|
ol.ClientId = jm["clientId"]
|
||||||
|
ol.ClientSecret = jm["clientSecret"]
|
||||||
|
ol.AuthorizationURL = jm["authorizationURL"]
|
||||||
|
ol.AccessTokenURL = jm["accessTokenURL"]
|
||||||
|
ol.RedirectURL = jm["redirectURL"]
|
||||||
|
ol.Scopes = stringx.Trim(jm["scopes"])
|
||||||
|
ol.ResourceURL = jm["resourceURL"]
|
||||||
|
ol.UserIdentifier = jm["userIdentifier"]
|
||||||
|
ol.AutoRegister = c.ConvBool(jm["autoRegister"], true)
|
||||||
|
return ol
|
||||||
|
}
|
||||||
|
|
||||||
|
type LdapLogin struct {
|
||||||
|
Enable bool // 是否启用
|
||||||
|
Host string
|
||||||
|
Port string `json:"port"`
|
||||||
|
SkipTLSVerify bool `json:"skipTLSVerify"` // 客户端是否跳过 TLS 证书验证
|
||||||
|
SecurityProtocol string `json:"securityProtocol"` // 安全协议(为Null不使用安全协议),如: StartTLS, LDAPS
|
||||||
|
BindDN string `json:"bindDn"` // LDAP 服务的管理员账号,如: "cn=admin,dc=example,dc=com"
|
||||||
|
BindPwd string `json:"bindPwd"` // LDAP 服务的管理员密码
|
||||||
|
BaseDN string `json:"baseDN"` // 用户所在的 base DN, 如: "ou=users,dc=example,dc=com"
|
||||||
|
UserFilter string `json:"userFilter"` // 过滤用户的方式, 如: "(uid=%s)"
|
||||||
|
UidMap string `json:"UidMap"` // 用户id和 LDAP 字段名之间的映射关系
|
||||||
|
UdnMap string `json:"UdnMap"` // 用户姓名(dispalyName)和 LDAP 字段名之间的映射关系
|
||||||
|
EmailMap string `json:"emailMap"` // 用户email和 LDAP 字段名之间的映射关系
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取LdapLogin相关配置
|
||||||
|
func GetLdapLogin() *LdapLogin {
|
||||||
|
c := sysapp.GetConfigApp().GetConfig(ConfigKeyLdapLogin)
|
||||||
|
jm := c.GetJsonMap()
|
||||||
|
ll := new(LdapLogin)
|
||||||
|
ll.Enable = c.ConvBool(jm["enable"], false)
|
||||||
|
ll.Host = jm["host"]
|
||||||
|
ll.Port = jm["port"]
|
||||||
|
ll.SkipTLSVerify = c.ConvBool(jm["skipTLSVerify"], true)
|
||||||
|
ll.SecurityProtocol = jm["securityProtocol"]
|
||||||
|
ll.BindDN = stringx.Trim(jm["bindDN"])
|
||||||
|
ll.BindPwd = stringx.Trim(jm["bindPwd"])
|
||||||
|
ll.BaseDN = stringx.Trim(jm["baseDN"])
|
||||||
|
ll.UserFilter = stringx.Trim(jm["userFilter"])
|
||||||
|
ll.UidMap = stringx.Trim(jm["uidMap"])
|
||||||
|
ll.UdnMap = stringx.Trim(jm["udnMap"])
|
||||||
|
ll.EmailMap = stringx.Trim(jm["emailMap"])
|
||||||
|
return ll
|
||||||
|
}
|
||||||
@@ -12,20 +12,17 @@ import (
|
|||||||
|
|
||||||
func Init(router *gin.RouterGroup) {
|
func Init(router *gin.RouterGroup) {
|
||||||
accountLogin := &api.AccountLogin{
|
accountLogin := &api.AccountLogin{
|
||||||
ConfigApp: sysapp.GetConfigApp(),
|
|
||||||
AccountApp: sysapp.GetAccountApp(),
|
AccountApp: sysapp.GetAccountApp(),
|
||||||
MsgApp: msgapp.GetMsgApp(),
|
MsgApp: msgapp.GetMsgApp(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ldapLogin := &api.LdapLogin{
|
ldapLogin := &api.LdapLogin{
|
||||||
ConfigApp: sysapp.GetConfigApp(),
|
|
||||||
AccountApp: sysapp.GetAccountApp(),
|
AccountApp: sysapp.GetAccountApp(),
|
||||||
MsgApp: msgapp.GetMsgApp(),
|
MsgApp: msgapp.GetMsgApp(),
|
||||||
}
|
}
|
||||||
|
|
||||||
oauth2Login := &api.Oauth2Login{
|
oauth2Login := &api.Oauth2Login{
|
||||||
Oauth2App: application.GetAuthApp(),
|
Oauth2App: application.GetAuthApp(),
|
||||||
ConfigApp: sysapp.GetConfigApp(),
|
|
||||||
AccountApp: sysapp.GetAccountApp(),
|
AccountApp: sysapp.GetAccountApp(),
|
||||||
MsgApp: msgapp.GetMsgApp(),
|
MsgApp: msgapp.GetMsgApp(),
|
||||||
}
|
}
|
||||||
@@ -45,6 +42,8 @@ func Init(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
/*--------oauth2登录相关----------*/
|
/*--------oauth2登录相关----------*/
|
||||||
|
|
||||||
|
req.NewGet("/oauth2-config", oauth2Login.Oauth2Config).DontNeedToken(),
|
||||||
|
|
||||||
// oauth2登录
|
// oauth2登录
|
||||||
req.NewGet("/oauth2/login", oauth2Login.OAuth2Login).DontNeedToken(),
|
req.NewGet("/oauth2/login", oauth2Login.OAuth2Login).DontNeedToken(),
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ package application
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mayfly-go/internal/db/config"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
|
||||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
|
||||||
"mayfly-go/pkg/biz"
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -90,7 +89,7 @@ func (d *dbSqlExecAppImpl) Exec(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error)
|
|||||||
isSelect := strings.HasPrefix(lowerSql, "select")
|
isSelect := strings.HasPrefix(lowerSql, "select")
|
||||||
if isSelect {
|
if isSelect {
|
||||||
// 如果配置为0,则不校验分页参数
|
// 如果配置为0,则不校验分页参数
|
||||||
maxCount := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbQueryMaxCount).IntValue(200)
|
maxCount := config.GetDbQueryMaxCount()
|
||||||
if maxCount != 0 {
|
if maxCount != 0 {
|
||||||
biz.IsTrue(strings.Contains(lowerSql, "limit"), "请完善分页信息后执行")
|
biz.IsTrue(strings.Contains(lowerSql, "limit"), "请完善分页信息后执行")
|
||||||
}
|
}
|
||||||
@@ -140,7 +139,7 @@ func (d *dbSqlExecAppImpl) saveSqlExecLog(isQuery bool, dbSqlExecRecord *entity.
|
|||||||
d.dbSqlExecRepo.Insert(dbSqlExecRecord)
|
d.dbSqlExecRepo.Insert(dbSqlExecRecord)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbSaveQuerySQL).BoolValue(false) {
|
if config.GetDbSaveQuerySql() {
|
||||||
dbSqlExecRecord.Table = "-"
|
dbSqlExecRecord.Table = "-"
|
||||||
dbSqlExecRecord.OldValue = "-"
|
dbSqlExecRecord.OldValue = "-"
|
||||||
dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery
|
dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery
|
||||||
@@ -161,7 +160,7 @@ func doSelect(selectStmt *sqlparser.Select, execSqlReq *DbSqlExecReq) (*DbSqlExe
|
|||||||
if selectExprsStr == "*" || strings.Contains(selectExprsStr, ".*") ||
|
if selectExprsStr == "*" || strings.Contains(selectExprsStr, ".*") ||
|
||||||
len(strings.Split(selectExprsStr, ",")) > 1 {
|
len(strings.Split(selectExprsStr, ",")) > 1 {
|
||||||
// 如果配置为0,则不校验分页参数
|
// 如果配置为0,则不校验分页参数
|
||||||
maxCount := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbQueryMaxCount).IntValue(200)
|
maxCount := config.GetDbQueryMaxCount()
|
||||||
if maxCount != 0 {
|
if maxCount != 0 {
|
||||||
limit := selectStmt.Limit
|
limit := selectStmt.Limit
|
||||||
biz.NotNil(limit, "请完善分页信息后执行")
|
biz.NotNil(limit, "请完善分页信息后执行")
|
||||||
|
|||||||
18
server/internal/db/config/config.go
Normal file
18
server/internal/db/config/config.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import sysapp "mayfly-go/internal/sys/application"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigKeyDbSaveQuerySQL string = "DbSaveQuerySQL" // 数据库是否记录查询相关sql
|
||||||
|
ConfigKeyDbQueryMaxCount string = "DbQueryMaxCount" // 数据库查询的最大数量
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取数据库最大查询数量配置
|
||||||
|
func GetDbQueryMaxCount() int {
|
||||||
|
return sysapp.GetConfigApp().GetConfig(ConfigKeyDbQueryMaxCount).IntValue(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库是否记录查询相关sql配置
|
||||||
|
func GetDbSaveQuerySql() bool {
|
||||||
|
return sysapp.GetConfigApp().GetConfig(ConfigKeyDbSaveQuerySQL).BoolValue(false)
|
||||||
|
}
|
||||||
@@ -166,7 +166,7 @@ func (ts *TerminalSession) receiveWsMsg() {
|
|||||||
case Data:
|
case Data:
|
||||||
_, err := ts.terminal.Write([]byte(msgObj.Msg))
|
_, err := ts.terminal.Write([]byte(msgObj.Msg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.Log.Debug("机器ssh终端写入消息失败: %s", err)
|
global.Log.Debugf("机器ssh终端写入消息失败: %s", err)
|
||||||
}
|
}
|
||||||
case Ping:
|
case Ping:
|
||||||
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
||||||
|
|||||||
@@ -41,12 +41,3 @@ func (c *Config) SaveConfig(rc *req.Ctx) {
|
|||||||
config.SetBaseInfo(rc.LoginAccount)
|
config.SetBaseInfo(rc.LoginAccount)
|
||||||
c.ConfigApp.Save(config)
|
c.ConfigApp.Save(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取oauth2登录配置信息,因为有些字段是敏感字段,故单独使用接口获取
|
|
||||||
func (c *Config) Oauth2Config(rc *req.Ctx) {
|
|
||||||
oauth2LoginConfig := c.ConfigApp.GetConfig(entity.ConfigKeyOauth2Login).ToOauth2Login()
|
|
||||||
rc.ResData = map[string]any{
|
|
||||||
"enable": oauth2LoginConfig.Enable,
|
|
||||||
"name": oauth2LoginConfig.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,16 +3,10 @@ package entity
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
"mayfly-go/pkg/utils/stringx"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ConfigKeyAccountLoginSecurity string = "AccountLoginSecurity" // 账号登录安全配置
|
|
||||||
ConfigKeyOauth2Login string = "Oauth2Login" // oauth2认证登录配置
|
|
||||||
ConfigKeyLdapLogin string = "LdapLogin" // ldap登录配置
|
|
||||||
ConfigKeyDbQueryMaxCount string = "DbQueryMaxCount" // 数据库查询的最大数量
|
|
||||||
ConfigKeyDbSaveQuerySQL string = "DbSaveQuerySQL" // 数据库是否记录查询相关sql
|
|
||||||
ConfigUseWartermark string = "UseWartermark" // 是否使用水印
|
ConfigUseWartermark string = "UseWartermark" // 是否使用水印
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,96 +52,6 @@ func (c *Config) IntValue(defaultValue int) int {
|
|||||||
return c.ConvInt(c.Value, defaultValue)
|
return c.ConvInt(c.Value, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountLoginSecurity struct {
|
|
||||||
UseCaptcha bool // 是否使用登录验证码
|
|
||||||
UseOtp bool // 是否双因素校验
|
|
||||||
OtpIssuer string // otp发行人
|
|
||||||
LoginFailCount int // 允许失败次数
|
|
||||||
LoginFailMin int // 登录失败指定次数后禁止的分钟数
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为AccountLoginSecurity结构体
|
|
||||||
func (c *Config) ToAccountLoginSecurity() *AccountLoginSecurity {
|
|
||||||
jm := c.GetJsonMap()
|
|
||||||
als := new(AccountLoginSecurity)
|
|
||||||
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
|
|
||||||
als.UseOtp = c.ConvBool(jm["useOtp"], false)
|
|
||||||
als.LoginFailCount = c.ConvInt(jm["loginFailCount"], 5)
|
|
||||||
als.LoginFailMin = c.ConvInt(jm["loginFailMin"], 10)
|
|
||||||
otpIssuer := jm["otpIssuer"]
|
|
||||||
if otpIssuer == "" {
|
|
||||||
otpIssuer = "mayfly-go"
|
|
||||||
}
|
|
||||||
als.OtpIssuer = otpIssuer
|
|
||||||
return als
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigOauth2Login struct {
|
|
||||||
Enable bool // 是否启用
|
|
||||||
Name string
|
|
||||||
ClientId string `json:"clientId"`
|
|
||||||
ClientSecret string `json:"clientSecret"`
|
|
||||||
AuthorizationURL string `json:"authorizationURL"`
|
|
||||||
AccessTokenURL string `json:"accessTokenURL"`
|
|
||||||
RedirectURL string `json:"redirectURL"`
|
|
||||||
Scopes string `json:"scopes"`
|
|
||||||
ResourceURL string `json:"resourceURL"`
|
|
||||||
UserIdentifier string `json:"userIdentifier"`
|
|
||||||
AutoRegister bool `json:"autoRegister"` // 是否自动注册
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为Oauth2Login结构体
|
|
||||||
func (c *Config) ToOauth2Login() *ConfigOauth2Login {
|
|
||||||
jm := c.GetJsonMap()
|
|
||||||
ol := new(ConfigOauth2Login)
|
|
||||||
ol.Enable = c.ConvBool(jm["enable"], false)
|
|
||||||
ol.Name = jm["name"]
|
|
||||||
ol.ClientId = jm["clientId"]
|
|
||||||
ol.ClientSecret = jm["clientSecret"]
|
|
||||||
ol.AuthorizationURL = jm["authorizationURL"]
|
|
||||||
ol.AccessTokenURL = jm["accessTokenURL"]
|
|
||||||
ol.RedirectURL = jm["redirectURL"]
|
|
||||||
ol.Scopes = stringx.Trim(jm["scopes"])
|
|
||||||
ol.ResourceURL = jm["resourceURL"]
|
|
||||||
ol.UserIdentifier = jm["userIdentifier"]
|
|
||||||
ol.AutoRegister = c.ConvBool(jm["autoRegister"], true)
|
|
||||||
return ol
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigLdapLogin struct {
|
|
||||||
Enable bool // 是否启用
|
|
||||||
Host string
|
|
||||||
Port string `json:"port"`
|
|
||||||
SkipTLSVerify bool `json:"skipTLSVerify"` // 客户端是否跳过 TLS 证书验证
|
|
||||||
SecurityProtocol string `json:"securityProtocol"` // 安全协议(为Null不使用安全协议),如: StartTLS, LDAPS
|
|
||||||
BindDN string `json:"bindDn"` // LDAP 服务的管理员账号,如: "cn=admin,dc=example,dc=com"
|
|
||||||
BindPwd string `json:"bindPwd"` // LDAP 服务的管理员密码
|
|
||||||
BaseDN string `json:"baseDN"` // 用户所在的 base DN, 如: "ou=users,dc=example,dc=com"
|
|
||||||
UserFilter string `json:"userFilter"` // 过滤用户的方式, 如: "(uid=%s)"
|
|
||||||
UidMap string `json:"UidMap"` // 用户id和 LDAP 字段名之间的映射关系
|
|
||||||
UdnMap string `json:"UdnMap"` // 用户姓名(dispalyName)和 LDAP 字段名之间的映射关系
|
|
||||||
EmailMap string `json:"emailMap"` // 用户email和 LDAP 字段名之间的映射关系
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为LdapLogin结构体
|
|
||||||
func (c *Config) ToLdapLogin() *ConfigLdapLogin {
|
|
||||||
jm := c.GetJsonMap()
|
|
||||||
ll := new(ConfigLdapLogin)
|
|
||||||
ll.Enable = c.ConvBool(jm["enable"], false)
|
|
||||||
ll.Host = jm["host"]
|
|
||||||
ll.Port = jm["port"]
|
|
||||||
ll.SkipTLSVerify = c.ConvBool(jm["skipTLSVerify"], true)
|
|
||||||
ll.SecurityProtocol = jm["securityProtocol"]
|
|
||||||
ll.BindDN = stringx.Trim(jm["bindDN"])
|
|
||||||
ll.BindPwd = stringx.Trim(jm["bindPwd"])
|
|
||||||
ll.BaseDN = stringx.Trim(jm["baseDN"])
|
|
||||||
ll.UserFilter = stringx.Trim(jm["userFilter"])
|
|
||||||
ll.UidMap = stringx.Trim(jm["uidMap"])
|
|
||||||
ll.UdnMap = stringx.Trim(jm["udnMap"])
|
|
||||||
ll.EmailMap = stringx.Trim(jm["emailMap"])
|
|
||||||
return ll
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换配置中的值为bool类型(默认"1"或"true"为true,其他为false)
|
// 转换配置中的值为bool类型(默认"1"或"true"为true,其他为false)
|
||||||
func (c *Config) ConvBool(value string, defaultValue bool) bool {
|
func (c *Config) ConvBool(value string, defaultValue bool) bool {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ func InitSysConfigRouter(router *gin.RouterGroup) {
|
|||||||
// 获取指定配置key对应的值
|
// 获取指定配置key对应的值
|
||||||
req.NewGet("/value", r.GetConfigValueByKey).DontNeedToken(),
|
req.NewGet("/value", r.GetConfigValueByKey).DontNeedToken(),
|
||||||
|
|
||||||
req.NewGet("/oauth2-login", r.Oauth2Config).DontNeedToken(),
|
|
||||||
|
|
||||||
req.NewPost("", r.SaveConfig).Log(req.NewLogSave("保存系统配置信息")).
|
req.NewPost("", r.SaveConfig).Log(req.NewLogSave("保存系统配置信息")).
|
||||||
RequiredPermissionCode("config:save"),
|
RequiredPermissionCode("config:save"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package config
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"mayfly-go/pkg/utils/assert"
|
"mayfly-go/pkg/utils/assert"
|
||||||
"mayfly-go/pkg/utils/ymlx"
|
"mayfly-go/pkg/utils/ymlx"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 配置文件映射对象
|
// 配置文件映射对象
|
||||||
@@ -21,7 +23,9 @@ func Init() {
|
|||||||
// 读取配置文件信息
|
// 读取配置文件信息
|
||||||
yc := &Config{}
|
yc := &Config{}
|
||||||
if err := ymlx.LoadYml(startConfigParam.ConfigFilePath, yc); err != nil {
|
if err := ymlx.LoadYml(startConfigParam.ConfigFilePath, yc); err != nil {
|
||||||
panic(fmt.Sprintf("读取配置文件[%s]失败: %s", startConfigParam.ConfigFilePath, err.Error()))
|
slog.Warn(fmt.Sprintf("读取配置文件[%s]失败: %s, 使用系统默认配置或环境变量配置", startConfigParam.ConfigFilePath, err.Error()))
|
||||||
|
// 设置默认信息,主要方便后续的系统环境变量替换
|
||||||
|
yc.SetDefaultConfig()
|
||||||
}
|
}
|
||||||
// 校验配置文件内容信息
|
// 校验配置文件内容信息
|
||||||
yc.Valid()
|
yc.Valid()
|
||||||
@@ -56,6 +60,15 @@ func (c *Config) Valid() {
|
|||||||
|
|
||||||
// 替换系统环境变量,如果环境变量中存在该值,则优秀使用环境变量设定的值
|
// 替换系统环境变量,如果环境变量中存在该值,则优秀使用环境变量设定的值
|
||||||
func (c *Config) ReplaceOsEnv() {
|
func (c *Config) ReplaceOsEnv() {
|
||||||
|
serverPort := os.Getenv("MAYFLY_SERVER_PORT")
|
||||||
|
if serverPort != "" {
|
||||||
|
if num, err := strconv.Atoi(serverPort); err != nil {
|
||||||
|
panic("环境变量-[MAYFLY_SERVER_PORT]-服务端口号需为数字")
|
||||||
|
} else {
|
||||||
|
c.Server.Port = num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dbHost := os.Getenv("MAYFLY_DB_HOST")
|
dbHost := os.Getenv("MAYFLY_DB_HOST")
|
||||||
if dbHost != "" {
|
if dbHost != "" {
|
||||||
c.Mysql.Host = dbHost
|
c.Mysql.Host = dbHost
|
||||||
@@ -86,3 +99,29 @@ func (c *Config) ReplaceOsEnv() {
|
|||||||
c.Jwt.Key = jwtKey
|
c.Jwt.Key = jwtKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetDefaultConfig() {
|
||||||
|
c.Server = &Server{
|
||||||
|
Model: "release",
|
||||||
|
Port: 8888,
|
||||||
|
MachineRecPath: "./rec",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Jwt = &Jwt{
|
||||||
|
ExpireTime: 1440,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Aes = &Aes{
|
||||||
|
Key: "1111111111111111",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Mysql = &Mysql{
|
||||||
|
Host: "localhost:3306",
|
||||||
|
Config: "charset=utf8&loc=Local&parseTime=true",
|
||||||
|
MaxIdleConns: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Log = &Log{
|
||||||
|
Level: "info",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user