mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30: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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"asciinema-player": "^3.5.0",
|
||||
"axios": "^1.4.0",
|
||||
"axios": "^1.5.0",
|
||||
"countup.js": "^2.7.0",
|
||||
"cropperjs": "^1.5.11",
|
||||
"echarts": "^5.4.0",
|
||||
@@ -32,7 +32,10 @@
|
||||
"vue-clipboard3": "^1.0.1",
|
||||
"vue-router": "^4.2.4",
|
||||
"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": {
|
||||
"@types/lodash": "^4.14.178",
|
||||
|
||||
@@ -5,12 +5,12 @@ export default {
|
||||
otpVerify: (param: any) => request.post('/auth/accounts/otp-verify', param),
|
||||
getPublicKey: () => request.get('/common/public-key'),
|
||||
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),
|
||||
captcha: () => request.get('/sys/captcha'),
|
||||
logout: () => request.post('/auth/accounts/logout'),
|
||||
getPermissions: () => request.get('/sys/accounts/permissions'),
|
||||
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),
|
||||
};
|
||||
|
||||
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,
|
||||
}
|
||||
@@ -344,4 +344,16 @@ body,
|
||||
|
||||
.f12 {
|
||||
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 }">
|
||||
<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" />
|
||||
</span>
|
||||
|
||||
@@ -126,6 +129,16 @@
|
||||
</el-descriptions>
|
||||
</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
|
||||
:title="machineEditDialog.title"
|
||||
v-model:visible="machineEditDialog.visible"
|
||||
@@ -146,10 +159,10 @@
|
||||
</template>
|
||||
|
||||
<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 { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { machineApi } from './api';
|
||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import TagInfo from '../component/TagInfo.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -157,6 +170,7 @@ import { TableColumn, TableQuery } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
|
||||
// 组件
|
||||
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
|
||||
const MachineEdit = defineAsyncComponent(() => import('./MachineEdit.vue'));
|
||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||
const FileManage = defineAsyncComponent(() => import('./FileManage.vue'));
|
||||
@@ -166,6 +180,7 @@ const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||
|
||||
const router = useRouter();
|
||||
const pageTableRef: any = ref(null);
|
||||
const terminalDialogRef: any = ref(null);
|
||||
|
||||
const perms = {
|
||||
addMachine: 'machine:add',
|
||||
@@ -180,12 +195,13 @@ const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), Tabl
|
||||
const columns = ref([
|
||||
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(35),
|
||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(45),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||
TableColumn.new('remark', '备注'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||
|
||||
@@ -275,15 +291,28 @@ const handleCommand = (commond: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const showTerminal = (row: any) => {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal`,
|
||||
query: {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
},
|
||||
const showTerminal = (row: any, event: PointerEvent) => {
|
||||
// 按住ctrl点击,则新建标签页打开, metaKey对应mac command键
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal`,
|
||||
query: {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
},
|
||||
});
|
||||
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,
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
};
|
||||
|
||||
const closeCli = async (row: any) => {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
:show-close="true"
|
||||
:before-close="handleClose"
|
||||
width="55%"
|
||||
draggable
|
||||
append-to-body
|
||||
>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
@@ -89,8 +91,10 @@
|
||||
:close-on-click-modal="false"
|
||||
:modal="false"
|
||||
@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>
|
||||
|
||||
<script-edit
|
||||
@@ -107,8 +111,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import SshTerminal from './SshTerminal.vue';
|
||||
import { machineApi } from './api';
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
||||
import { ScriptResultEnum, ScriptTypeEnum } from './enums';
|
||||
import ScriptEdit from './ScriptEdit.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -313,4 +317,4 @@ const handleClose = () => {
|
||||
state.scriptParamsDialog.paramsFormItem = [];
|
||||
};
|
||||
</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>
|
||||
<div>
|
||||
<ssh-terminal ref="terminal" :machineId="machineId" :height="height + 'px'" />
|
||||
<div class="terminal-wrapper">
|
||||
<TerminalBody :socket-url="getMachineTerminalSocketUrl(route.query.id)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SshTerminal from './SshTerminal.vue';
|
||||
import { reactive, toRefs, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { getMachineTerminalSocketUrl } from './api';
|
||||
|
||||
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>
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
.terminal-wrapper {
|
||||
height: calc(100vh);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Api from '@/common/Api';
|
||||
import config from '@/common/config';
|
||||
import { getSession } from '@/common/utils/storage';
|
||||
|
||||
export const machineApi = {
|
||||
// 获取权限列表
|
||||
@@ -56,3 +58,7 @@ export const cronJobApi = {
|
||||
delete: Api.newDelete('/machine-cronjobs/{id}'),
|
||||
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"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
|
||||
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
|
||||
axios@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmmirror.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267"
|
||||
integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.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"
|
||||
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:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmmirror.com/xterm/-/xterm-5.2.1.tgz#b3fea7bdb55b9be1d4b31f4cd1091f26ac42afb8"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mayfly-go/internal/auth/api/form"
|
||||
"mayfly-go/internal/auth/config"
|
||||
"mayfly-go/internal/common/utils"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
@@ -22,7 +23,6 @@ import (
|
||||
type AccountLogin struct {
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
ConfigApp sysapp.Config
|
||||
}
|
||||
|
||||
/** 用户账号密码登录 **/
|
||||
@@ -31,7 +31,7 @@ type AccountLogin struct {
|
||||
func (a *AccountLogin) Login(rc *req.Ctx) {
|
||||
loginForm := ginx.BindJsonAndValid(rc.GinCtx, new(form.LoginForm))
|
||||
|
||||
accountLoginSecurity := a.ConfigApp.GetConfig(sysentity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity()
|
||||
accountLoginSecurity := config.GetAccountLoginSecurity()
|
||||
// 判断是否有开启登录验证码校验
|
||||
if accountLoginSecurity.UseCaptcha {
|
||||
// 校验验证码
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/auth/config"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
msgentity "mayfly-go/internal/msg/domain/entity"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
@@ -24,7 +25,7 @@ const (
|
||||
)
|
||||
|
||||
// 最后的登录校验(共用)。校验通过返回登录成功响应结果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(), "该账号不可用")
|
||||
username := account.Username
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@ package api
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/auth/api/form"
|
||||
"mayfly-go/internal/auth/config"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
@@ -19,17 +17,20 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LdapLogin struct {
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
ConfigApp sysapp.Config
|
||||
}
|
||||
|
||||
// @router /auth/ldap/enabled [get]
|
||||
func (a *LdapLogin) GetLdapEnabled(rc *req.Ctx) {
|
||||
ldapLoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyLdapLogin).ToLdapLogin()
|
||||
ldapLoginConfig := config.GetLdapLogin()
|
||||
rc.ResData = ldapLoginConfig.Enable
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ func (a *LdapLogin) GetLdapEnabled(rc *req.Ctx) {
|
||||
func (a *LdapLogin) Login(rc *req.Ctx) {
|
||||
loginForm := ginx.BindJsonAndValid(rc.GinCtx, new(form.LoginForm))
|
||||
|
||||
accountLoginSecurity := a.ConfigApp.GetConfig(sysentity.ConfigKeyAccountLoginSecurity).ToAccountLoginSecurity()
|
||||
accountLoginSecurity := config.GetAccountLoginSecurity()
|
||||
// 判断是否有开启登录验证码校验
|
||||
if accountLoginSecurity.UseCaptcha {
|
||||
// 校验验证码
|
||||
@@ -115,7 +116,7 @@ type UserInfo struct {
|
||||
|
||||
// Authenticate 通过 LDAP 验证用户名密码
|
||||
func Authenticate(username, password string) (*UserInfo, error) {
|
||||
ldapConf := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyLdapLogin).ToLdapLogin()
|
||||
ldapConf := config.GetLdapLogin()
|
||||
if !ldapConf.Enable {
|
||||
return nil, errors.Errorf("未启用 LDAP 登录")
|
||||
}
|
||||
@@ -163,7 +164,7 @@ func Authenticate(username, password string) (*UserInfo, error) {
|
||||
}
|
||||
|
||||
// Connect 创建 LDAP 连接
|
||||
func Connect(ldapConf *sysentity.ConfigLdapLogin) (*ldap.Conn, error) {
|
||||
func Connect(ldapConf *config.LdapLogin) (*ldap.Conn, error) {
|
||||
conn, err := dial(ldapConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -178,7 +179,7 @@ func Connect(ldapConf *sysentity.ConfigLdapLogin) (*ldap.Conn, error) {
|
||||
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)
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: ldapConf.Host,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"mayfly-go/internal/auth/api/vo"
|
||||
"mayfly-go/internal/auth/application"
|
||||
"mayfly-go/internal/auth/config"
|
||||
"mayfly-go/internal/auth/domain/entity"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
|
||||
type Oauth2Login struct {
|
||||
Oauth2App application.Oauth2
|
||||
ConfigApp sysapp.Config
|
||||
AccountApp sysapp.Account
|
||||
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}
|
||||
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)
|
||||
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["isFirstOauth2Login"] = isFirst
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *sysentity.ConfigOauth2Login) {
|
||||
oath2LoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyOauth2Login).ToOauth2Login()
|
||||
func (a *Oauth2Login) getOAuthClient() (*oauth2.Config, *config.Oauth2Login) {
|
||||
oath2LoginConfig := config.GetOauth2Login()
|
||||
biz.IsTrue(oath2LoginConfig.Enable, "请先配置oauth2或启用oauth2登录")
|
||||
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) {
|
||||
res := &vo.Oauth2Status{}
|
||||
oauth2LoginConfig := a.ConfigApp.GetConfig(sysentity.ConfigKeyOauth2Login).ToOauth2Login()
|
||||
oauth2LoginConfig := config.GetOauth2Login()
|
||||
res.Enable = oauth2LoginConfig.Enable
|
||||
if res.Enable {
|
||||
err := a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
|
||||
@@ -216,3 +216,12 @@ func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
|
||||
func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
|
||||
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) {
|
||||
accountLogin := &api.AccountLogin{
|
||||
ConfigApp: sysapp.GetConfigApp(),
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
|
||||
ldapLogin := &api.LdapLogin{
|
||||
ConfigApp: sysapp.GetConfigApp(),
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
|
||||
oauth2Login := &api.Oauth2Login{
|
||||
Oauth2App: application.GetAuthApp(),
|
||||
ConfigApp: sysapp.GetConfigApp(),
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
@@ -45,6 +42,8 @@ func Init(router *gin.RouterGroup) {
|
||||
|
||||
/*--------oauth2登录相关----------*/
|
||||
|
||||
req.NewGet("/oauth2-config", oauth2Login.Oauth2Config).DontNeedToken(),
|
||||
|
||||
// oauth2登录
|
||||
req.NewGet("/oauth2/login", oauth2Login.OAuth2Login).DontNeedToken(),
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ package application
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/config"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"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/model"
|
||||
"strconv"
|
||||
@@ -90,7 +89,7 @@ func (d *dbSqlExecAppImpl) Exec(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error)
|
||||
isSelect := strings.HasPrefix(lowerSql, "select")
|
||||
if isSelect {
|
||||
// 如果配置为0,则不校验分页参数
|
||||
maxCount := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbQueryMaxCount).IntValue(200)
|
||||
maxCount := config.GetDbQueryMaxCount()
|
||||
if maxCount != 0 {
|
||||
biz.IsTrue(strings.Contains(lowerSql, "limit"), "请完善分页信息后执行")
|
||||
}
|
||||
@@ -140,7 +139,7 @@ func (d *dbSqlExecAppImpl) saveSqlExecLog(isQuery bool, dbSqlExecRecord *entity.
|
||||
d.dbSqlExecRepo.Insert(dbSqlExecRecord)
|
||||
return
|
||||
}
|
||||
if sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbSaveQuerySQL).BoolValue(false) {
|
||||
if config.GetDbSaveQuerySql() {
|
||||
dbSqlExecRecord.Table = "-"
|
||||
dbSqlExecRecord.OldValue = "-"
|
||||
dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery
|
||||
@@ -161,7 +160,7 @@ func doSelect(selectStmt *sqlparser.Select, execSqlReq *DbSqlExecReq) (*DbSqlExe
|
||||
if selectExprsStr == "*" || strings.Contains(selectExprsStr, ".*") ||
|
||||
len(strings.Split(selectExprsStr, ",")) > 1 {
|
||||
// 如果配置为0,则不校验分页参数
|
||||
maxCount := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbQueryMaxCount).IntValue(200)
|
||||
maxCount := config.GetDbQueryMaxCount()
|
||||
if maxCount != 0 {
|
||||
limit := selectStmt.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:
|
||||
_, err := ts.terminal.Write([]byte(msgObj.Msg))
|
||||
if err != nil {
|
||||
global.Log.Debug("机器ssh终端写入消息失败: %s", err)
|
||||
global.Log.Debugf("机器ssh终端写入消息失败: %s", err)
|
||||
}
|
||||
case Ping:
|
||||
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
||||
|
||||
@@ -41,12 +41,3 @@ func (c *Config) SaveConfig(rc *req.Ctx) {
|
||||
config.SetBaseInfo(rc.LoginAccount)
|
||||
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,17 +3,11 @@ package entity
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
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" // 是否使用水印
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -58,96 +52,6 @@ func (c *Config) IntValue(defaultValue int) int {
|
||||
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)
|
||||
func (c *Config) ConvBool(value string, defaultValue bool) bool {
|
||||
if value == "" {
|
||||
|
||||
@@ -20,8 +20,6 @@ func InitSysConfigRouter(router *gin.RouterGroup) {
|
||||
// 获取指定配置key对应的值
|
||||
req.NewGet("/value", r.GetConfigValueByKey).DontNeedToken(),
|
||||
|
||||
req.NewGet("/oauth2-login", r.Oauth2Config).DontNeedToken(),
|
||||
|
||||
req.NewPost("", r.SaveConfig).Log(req.NewLogSave("保存系统配置信息")).
|
||||
RequiredPermissionCode("config:save"),
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package config
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"mayfly-go/pkg/utils/assert"
|
||||
"mayfly-go/pkg/utils/ymlx"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// 配置文件映射对象
|
||||
@@ -21,7 +23,9 @@ func Init() {
|
||||
// 读取配置文件信息
|
||||
yc := &Config{}
|
||||
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()
|
||||
@@ -56,6 +60,15 @@ func (c *Config) Valid() {
|
||||
|
||||
// 替换系统环境变量,如果环境变量中存在该值,则优秀使用环境变量设定的值
|
||||
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")
|
||||
if dbHost != "" {
|
||||
c.Mysql.Host = dbHost
|
||||
@@ -86,3 +99,29 @@ func (c *Config) ReplaceOsEnv() {
|
||||
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