diff --git a/client/.eslintrc.js b/client/.eslintrc.js new file mode 100644 index 0000000..a72394b --- /dev/null +++ b/client/.eslintrc.js @@ -0,0 +1,96 @@ +// 规则参见:https://cn.eslint.org/docs/rules/ +module.exports = { + root: true, // 当前配置文件不能往父级查找 + 'globals': { + 'consola': true + }, + env: { + node: true, + es6: true + }, + extends: [ + 'eslint:recommended' // 应用Eslint全部默认规则 + ], + 'parserOptions': { + 'ecmaVersion': 'latest', + 'sourceType': 'module' // 目标类型 Node项目得添加这个 + }, + // 自定义规则,可以覆盖 extends 的配置【安装Eslint插件可以静态检查本地文件是否符合以下规则】 + 'ignorePatterns': ['*.html', 'node-os-utils'], + rules: { + // 0: 关闭规则(允许) 1/2: 警告warning/错误error(不允许) + 'no-console': 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'template-curly-spacing': ['error', 'always'], // 模板字符串空格 + 'default-case': 0, + 'no-empty': 0, + 'object-curly-spacing': ['error', 'always'], + 'no-multi-spaces': ['error'], + indent: ['error', 2, { 'SwitchCase': 1 }], // 缩进:2 + quotes: ['error', 'single'], // 引号:single单引 double双引 + semi: ['error', 'never'], // 结尾分号:never禁止 always必须 + 'comma-dangle': ['error', 'never'], // 对象拖尾逗号 + 'no-redeclare': ['error', { builtinGlobals: true }], // 禁止重复对象声明 + 'no-multi-assign': 0, + 'no-restricted-globals': 0, + 'space-before-function-paren': 0, // 函数定义时括号前面空格 + 'one-var': 0, // 允许连续声明 + // 'no-undef': 0, // 允许未定义的变量【会使env配置无效】 + 'linebreak-style': 0, // 检测CRLF/LF检测【默认LF】 + 'no-extra-boolean-cast': 0, // 允许意外的Boolean值转换 + 'no-constant-condition': 0, // if语句中禁止常量表达式 + 'no-prototype-builtins': 0, // 允许使用Object.prototypes内置对象(如:xxx.hasOwnProperty) + 'no-regex-spaces': 0, // 允许正则匹配多个空格 + 'no-unexpected-multiline': 0, // 允许多行表达式 + 'no-fallthrough': 0, // 允许switch穿透 + 'no-delete-var': 0, // 允许 delete 删除对象属性 + 'no-mixed-spaces-and-tabs': 0, // 允许空格tab混用 + 'no-class-assign': 0, // 允许修改class类型 + 'no-param-reassign': 0, // 允许对函数params赋值 + 'max-len': 0, // 允许长行 + 'func-names': 0, // 允许命名函数 + 'import/no-unresolved': 0, // 不检测模块not fund + 'import/prefer-default-export': 0, // 允许单个导出 + 'no-const-assign': 1, // 警告:修改const命名的变量 + 'no-unused-vars': 1, // 警告:已声明未使用 + 'no-unsafe-negation': 1, // 警告:使用 in / instanceof 关系运算符时,左边表达式请勿使用 ! 否定操作符 + 'use-isnan': 1, // 警告:使用 isNaN() 检查 NaN + 'no-var': 2, // 禁止使用var声明 + 'no-empty-pattern': 2, // 空解构赋值 + 'eqeqeq': 2, // 必须使用 全等=== 或 非全等 !== + 'no-cond-assign': 2, // if语句中禁止赋值 + 'no-dupe-args': 2, // 禁止function重复参数 + 'no-dupe-keys': 2, // 禁止object重复key + 'no-duplicate-case': 2, + 'no-func-assign': 2, // 禁止重复声明函数 + 'no-inner-declarations': 2, // 禁止在嵌套的语句块中出现变量或 function 声明 + 'no-sparse-arrays': 2, // 禁止稀缺数组 + 'no-unreachable': 2, // 禁止非条件return、throw、continue 和 break 语句后出现代码 + 'no-unsafe-finally': 2, // 禁止finally出现控制流语句,如:return、throw等,因为这会导致try...catch捕获不到 + 'valid-typeof': 2, // 强制 typeof 表达式与有效的字符串进行比较 + // auto format options + 'prefer-const': 0, // 禁用声明自动化 + 'no-extra-parens': 0, // 允许函数周围出现不明括号 + 'no-extra-semi': 2, // 禁止不必要的分号 + // curly: ['error', 'multi'], // if、else、for、while 语句单行代码时不使用大括号 + 'dot-notation': 0, // 允许使用点号或方括号来访问对象属性 + 'dot-location': ['error', 'property'], // 点操作符位置,要求跟随下一行 + 'no-else-return': 2, // 禁止if中有return后又else + 'no-implicit-coercion': [2, { allow: ['!!', '~', '+'] }], // 禁止隐式转换,allow字段内符号允许 + 'no-trailing-spaces': 1, //一行结束后面不要有空格 + 'no-multiple-empty-lines': [1, { 'max': 1 }], // 空行最多不能超过1行 + 'no-useless-return': 2, + 'wrap-iife': 0, // 允许自调用函数 + 'yoda': 0, // 允许yoda语句 + 'strict': 0, // 允许strict + 'no-undef-init': 0, // 允许将变量初始化为undefined + 'prefer-promise-reject-errors': 0, // 允许使用非 Error 对象作为 Promise 拒绝的原因 + 'consistent-return': 0, // 允许函数不使用return + 'no-new': 0, // 允许单独new + 'no-restricted-syntax': 0, // 允许特定的语法 + 'no-plusplus': 0, + 'import/extensions': 0, // 忽略扩展名 + 'global-require': 0, + 'no-return-assign': 0 + } +} diff --git a/client/app/socket/monitor.js b/client/app/socket/monitor.js index 5b25115..a3c120a 100644 --- a/client/app/socket/monitor.js +++ b/client/app/socket/monitor.js @@ -25,13 +25,13 @@ function ipSchedule() { getIpInfo() }) - // 每日凌晨两点整,刷新ip信息(兼容动态ip服务器) + // 每日凌晨两点整,刷新ip信息 let rule2 = new schedule.RecurrenceRule() rule2.hour = 2 rule2.minute = 0 rule2.second = 0 schedule.scheduleJob(rule2, () => { - console.log('Task: refresh ip info', new Date()) + console.log('Task: refresh ip info: ', new Date()) getIpInfo() }) } diff --git a/client/package.json b/client/package.json index f6dd716..1dc58a0 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "axios": "^1.7.2", + "eslint": "^8.56.0", "koa": "^2.15.3", "node-os-utils": "^1.3.7", "node-schedule": "^2.1.1", diff --git a/server/app/controller/user.js b/server/app/controller/user.js index 469305b..b5a29cd 100644 --- a/server/app/controller/user.js +++ b/server/app/controller/user.js @@ -3,7 +3,7 @@ const { getNetIPInfo, readKey, writeKey, RSADecryptSync, AESEncryptSync, SHA1Enc const getpublicKey = async ({ res }) => { let { publicKey: data } = await readKey() - if(!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 }) + if (!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 }) res.success({ data }) } @@ -16,18 +16,16 @@ let loginCountDown = forbidTimer let forbidLogin = false const login = async ({ res, request }) => { - let { body: { ciphertext, jwtExpires }, ip: clientIp } = request - if(!ciphertext) return res.fail({ msg: '参数错误' }) - - if(forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` }) - + let { body: { loginName, ciphertext, jwtExpires }, ip: clientIp } = request + if (!loginName && !ciphertext) return res.fail({ msg: '请求非法!' }) + if (forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` }) loginErrCount++ loginErrTotal++ - if(loginErrCount >= allowErrCount) { + if (loginErrCount >= allowErrCount) { const { ip, country, city } = await getNetIPInfo(clientIp) // 发送通知&禁止登录 let sw = getNotifySwByType('err_login') - if(sw) sendEmailToConfList('登录错误提醒', `重新登录次数: ${ loginErrTotal }地点:${ country + city }IP: ${ ip }`) + if (sw) sendEmailToConfList('登录错误提醒', `重新登录次数: ${ loginErrTotal }地点:${ country + city }IP: ${ ip }`) forbidLogin = true loginErrCount = 0 @@ -38,8 +36,9 @@ const login = async ({ res, request }) => { // 计算登录倒计时 timer = setInterval(() => { - if(loginCountDown <= 0){ + if (loginCountDown <= 0) { clearInterval(timer) + timer = null loginCountDown = forbidTimer return } @@ -50,15 +49,15 @@ const login = async ({ res, request }) => { // 登录流程 try { // console.log('ciphertext', ciphertext) - let password = await RSADecryptSync(ciphertext) - console.log('Decrypt解密password:', password) - let { pwd } = await readKey() - if(password === 'admin' && pwd === 'admin') { + let loginPwd = await RSADecryptSync(ciphertext) + // console.log('Decrypt解密password:', loginPwd) + let { user, pwd } = await readKey() + if (loginName === user && loginPwd === 'admin' && pwd === 'admin') { const token = await beforeLoginHandler(clientIp, jwtExpires) - return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' }) + return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认用户名和密码' }) } - password = SHA1Encrypt(password) - if(password !== pwd) return res.fail({ msg: '密码错误' }) + loginPwd = SHA1Encrypt(loginPwd) + if (loginName !== user || loginPwd !== pwd) return res.fail({ msg: `用户名或密码错误 ${ loginErrTotal }/${ allowErrCount }` }) const token = await beforeLoginHandler(clientIp, jwtExpires) return res.success({ data: { token, jwtExpires }, msg: '登录成功' }) } catch (error) { @@ -83,26 +82,28 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => { // 邮件登录通知 let sw = getNotifySwByType('login') - if(sw) sendEmailToConfList('登录提醒', `地点:${ country + city }IP: ${ ip }`) + if (sw) sendEmailToConfList('登录提醒', `地点:${ country + city }IP: ${ ip }`) global.loginRecord.unshift(clientIPInfo) - if(global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10) + if (global.loginRecord.length > 10) global.loginRecord = global.loginRecord.slice(0, 10) return token } const updatePwd = async ({ res, request }) => { - let { body: { oldPwd, newPwd } } = request + let { body: { oldLoginName, oldPwd, newLoginName, newPwd } } = request let rsaOldPwd = await RSADecryptSync(oldPwd) oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd) let keyObj = await readKey() - if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' }) + let { user, pwd } = keyObj + if (oldLoginName !== user || oldPwd !== pwd) return res.fail({ data: false, msg: '原用户名或密码校验失败' }) // 旧密钥校验通过,加密保存新密码 newPwd = await RSADecryptSync(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(await RSADecryptSync(newPwd)) + keyObj.user = newLoginName keyObj.pwd = newPwd await writeKey(keyObj) let sw = getNotifySwByType('updatePwd') - if(sw) sendEmailToConfList('密码修改提醒', '面板登录密码已更改') + if (sw) sendEmailToConfList(`登录信息修改提醒, 新用户名: ${ newLoginName }`) res.success({ data: true, msg: 'success' }) } diff --git a/server/app/db.js b/server/app/db.js index 5d4034c..af121e4 100644 --- a/server/app/db.js +++ b/server/app/db.js @@ -12,6 +12,7 @@ function initKeyDB() { if (count === 0) { consola.log('初始化keyDB✔') const defaultData = { + user: 'admin', pwd: 'admin', commonKey: '', publicKey: '', diff --git a/server/app/main.js b/server/app/main.js index 9426ede..b5abc84 100644 --- a/server/app/main.js +++ b/server/app/main.js @@ -1,16 +1,15 @@ const consola = require('consola') global.consola = consola -const { httpServer, clientHttpServer } = require('./server') +const { httpServer } = require('./server') const initDB = require('./db') const initEncryptConf = require('./init') -// const scheduleJob = require('./schedule') +const scheduleJob = require('./schedule') async function main() { await initDB() await initEncryptConf() httpServer() - clientHttpServer() - // scheduleJob() + scheduleJob() } main() diff --git a/server/app/schedule/expired-notify.js b/server/app/schedule/expired-notify.js index ae77143..d17c336 100644 --- a/server/app/schedule/expired-notify.js +++ b/server/app/schedule/expired-notify.js @@ -25,5 +25,6 @@ const expiredNotifyJob = async () => { } module.exports = () => { + // 每天中午12点执行一次。 schedule.scheduleJob('0 0 12 1/1 * ?', expiredNotifyJob) } diff --git a/server/app/schedule/index.js b/server/app/schedule/index.js index 2710894..60c8347 100644 --- a/server/app/schedule/index.js +++ b/server/app/schedule/index.js @@ -1,7 +1,5 @@ -const offlineInspect = require('./offline-inspect') const expiredNotify = require('./expired-notify') module.exports = () => { - offlineInspect() expiredNotify() } diff --git a/server/app/schedule/offline-inspect.js b/server/app/schedule/offline-inspect.js deleted file mode 100644 index 4232c32..0000000 --- a/server/app/schedule/offline-inspect.js +++ /dev/null @@ -1,40 +0,0 @@ -const schedule = require('node-schedule') -const { clientPort } = require('../config') -const { readHostList, sendEmailToConfList, getNotifySwByType, formatTimestamp, isProd } = require('../utils') -const testConnectAsync = require('../utils/test-connect') - -let sendNotifyRecord = new Map() -const offlineJob = async () => { - let sw = getNotifySwByType('host_offline') - if(!sw) return - consola.info('=====开始检测服务器状态=====', new Date()) - const hostList = await readHostList() - for (const item of hostList) { - const { host, name } = item - // consola.info('start inpect:', host, name ) - testConnectAsync({ - port: clientPort , - host: `http://${ host }`, - timeout: 3000, - retryTimes: 20 // 尝试重连次数 - }) - .then(() => { - // consola.success('测试连接成功:', host, name) - }) - .catch((error) => { - consola.error('测试连接失败: ', host, name) - // 当前小时是否发送过通知 - let curHourIsSend = sendNotifyRecord.has(host) && (sendNotifyRecord.get(host).sendTime === formatTimestamp(Date.now(), 'hour')) - if(curHourIsSend) return consola.info('当前小时已发送过通知: ', sendNotifyRecord.get(host).sendTime) - sendEmailToConfList('服务器离线提醒', `别名: ${ name }IP: ${ host }错误信息:${ error.message }`) - .then(() => { - sendNotifyRecord.set(host, { 'sendTime': formatTimestamp(Date.now(), 'hour') }) - }) - }) - } -} - -module.exports = () => { - if(!isProd()) return consola.info('本地开发不检测服务器离线状态') - schedule.scheduleJob('0 0/5 12 1/1 * ?', offlineJob) -} diff --git a/server/app/server.js b/server/app/server.js index 4007f8c..f9b56a7 100644 --- a/server/app/server.js +++ b/server/app/server.js @@ -1,10 +1,8 @@ const Koa = require('koa') const compose = require('koa-compose') // 组合中间件,简化写法 const http = require('http') -const { clientPort } = require('./config') const { httpPort } = require('./config') const middlewares = require('./middlewares') -const wsMonitorOsInfo = require('./socket/monitor') const wsTerminal = require('./socket/terminal') const wsSftp = require('./socket/sftp') const wsHostStatus = require('./socket/host-status') @@ -21,15 +19,6 @@ const httpServer = () => { }) } -const clientHttpServer = () => { - const app = new Koa() - const server = http.createServer(app.callback()) - wsMonitorOsInfo(server) // 监控本机信息 - server.listen(clientPort, () => { - consola.success(`Client(http) is running on: http://localhost:${ clientPort }`) - }) -} - // 服务 function serverHandler(app, server) { app.proxy = true // 用于nginx反代时获取真实客户端ip @@ -50,6 +39,5 @@ function serverHandler(app, server) { } module.exports = { - httpServer, - clientHttpServer + httpServer } \ No newline at end of file diff --git a/server/app/socket/monitor.js b/server/app/socket/monitor.js deleted file mode 100644 index b1845a6..0000000 --- a/server/app/socket/monitor.js +++ /dev/null @@ -1,71 +0,0 @@ -const { Server } = require('socket.io') -const schedule = require('node-schedule') -const axios = require('axios') -let getOsData = require('../utils/os-data') -const consola = require('consola') - -let serverSockets = {}, ipInfo = {}, osData = {} - -async function getIpInfo() { - try { - let { data } = await axios.get('http://ip-api.com/json?lang=zh-CN') - consola.success('getIpInfo Success: ', new Date()) - ipInfo = data - } catch (error) { - consola.error('getIpInfo Error: ', new Date(), error.message) - } -} - -function ipSchedule() { - let rule1 = new schedule.RecurrenceRule() - rule1.second = [0] - schedule.scheduleJob(rule1, () => { - let { query, country, city } = ipInfo || {} - if(query && country && city) return - consola.success('Task: start getIpInfo', new Date()) - getIpInfo() - }) - - // 每日凌晨两点整,刷新ip信息(兼容动态ip服务器) - let rule2 = new schedule.RecurrenceRule() - rule2.hour = 2 - rule2.minute = 0 - rule2.second = 0 - schedule.scheduleJob(rule2, () => { - consola.info('Task: refresh ip info', new Date()) - getIpInfo() - }) -} - -ipSchedule() - -module.exports = (httpServer) => { - const serverIo = new Server(httpServer, { - path: '/client/os-info', - cors: { - origin: '*' - } - }) - - serverIo.on('connection', (socket) => { - // 存储对应websocket连接的定时器 - serverSockets[socket.id] = setInterval(async () => { - try { - osData = await getOsData() - socket && socket.emit('client_data', Object.assign(osData, { ipInfo })) - } catch (error) { - consola.error('客户端错误:', error) - socket && socket.emit('client_error', { error }) - } - }, 1000) - - socket.on('disconnect', () => { - // 断开时清除对应的websocket连接 - if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id]) - delete serverSockets[socket.id] - socket.close && socket.close() - socket = null - // console.log('断开socketId: ', socket.id, '剩余链接数: ', Object.keys(serverSockets).length) - }) - }) -} diff --git a/server/app/socket/sftp-cache/.gitkeep b/server/app/socket/sftp-cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/src/assets/scss/global.scss b/web/src/assets/scss/global.scss index 969b0c4..eb91448 100644 --- a/web/src/assets/scss/global.scss +++ b/web/src/assets/scss/global.scss @@ -29,12 +29,16 @@ html, body, div, ul, section, textarea { // 全局背景 body { - // background-color: #f4f6f9; - background-position: center center; - background-attachment: fixed; - background-size: cover; + // background-position: center center; + // background-attachment: fixed; + // background-size: cover; + // background-repeat: no-repeat; + // // background-image: url(../bg.jpg), linear-gradient(to bottom, #010179, #F5C4C1, #151799); + background-color: #f4f6f9; + background-image: url(https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg); background-repeat: no-repeat; - background-image: url(../bg.jpg), linear-gradient(to bottom, #010179, #F5C4C1, #151799); + background-position: center 110px; + background-size: 58%; } html, body { diff --git a/web/src/views/list/components/setting-tab/password.vue b/web/src/views/list/components/setting-tab/password.vue index 52f4514..5500caf 100644 --- a/web/src/views/list/components/setting-tab/password.vue +++ b/web/src/views/list/components/setting-tab/password.vue @@ -7,29 +7,41 @@ :hide-required-asterisk="true" label-suffix=":" label-width="90px" + :show-message="false" > - + + + + + + + - - - @@ -49,28 +61,30 @@ const { proxy: { $api, $message } } = getCurrentInstance() const loading = ref(false) const formRef = ref(null) const formData = reactive({ + oldLoginName: '', oldPwd: '', - newPwd: '', - confirmPwd: '' + newLoginName: '', + newPwd: '' }) const rules = reactive({ - oldPwd: { required: true, message: '输入旧密码', trigger: 'change' }, - newPwd: { required: true, message: '输入新密码', trigger: 'change' }, - confirmPwd: { required: true, message: '输入确认密码', trigger: 'change' } + oldLoginName: { required: true, message: '输入原用户名', trigger: 'change' }, + oldPwd: { required: true, message: '输入原密码', trigger: 'change' }, + newLoginName: { required: true, message: '输入新用户名', trigger: 'change' }, + newPwd: { required: true, message: '输入新密码', trigger: 'change' } }) const handleUpdate = () => { formRef.value.validate() .then(async () => { - let { oldPwd, newPwd, confirmPwd } = formData - if(newPwd !== confirmPwd) return $message.error({ center: true, message: '两次密码输入不一致' }) + let { oldLoginName, oldPwd, newLoginName, newPwd } = formData oldPwd = RSAEncrypt(oldPwd) newPwd = RSAEncrypt(newPwd) - let { msg } = await $api.updatePwd({ oldPwd, newPwd }) + let { msg } = await $api.updatePwd({ oldLoginName, oldPwd, newLoginName, newPwd }) $message({ type: 'success', center: true, message: msg }) + formData.oldLoginName = '' formData.oldPwd = '' + formData.newLoginName = '' formData.newPwd = '' - formData.confirmPwd = '' formRef.value.resetFields() }) } diff --git a/web/src/views/login/index.vue b/web/src/views/login/index.vue index cf43376..08a4209 100644 --- a/web/src/views/login/index.vue +++ b/web/src/views/login/index.vue @@ -1,70 +1,78 @@ - - - Error - LOGIN - - - - - - - - - - - - - - - 一次性会话 - 自定义(小时) - + + + EasyNode + + + + + + + + - - - + + + + + + + + + + 一次性会话 + 自定义(小时) + + + + + + - - - 登录 - - - +