✨ update db
This commit is contained in:
parent
873e20cbcf
commit
b8e08666a6
6
.gitignore
vendored
6
.gitignore
vendored
@ -4,4 +4,8 @@ dist
|
||||
easynode-server.zip
|
||||
server/app/static/upload/*
|
||||
server/app/socket/temp/*
|
||||
server/app/logs/*
|
||||
app/socket/.sftp-cache/*
|
||||
server/app/logs/*
|
||||
server/app/db/*
|
||||
!server/app/db/README.md
|
||||
plan.md
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,3 +1,13 @@
|
||||
## [1.3.0](https://github.com/chaos-zhu/easynode/releases) (2024-07-10)
|
||||
|
||||
### Features
|
||||
|
||||
* 重构文件储存方式 ✔
|
||||
* 修复不同ssh密钥算法登录失败的bug ✔
|
||||
* 移除上一次IP登录校验的判断 ✔
|
||||
* 前端工程迁移至项目根目录 ✔
|
||||
* 添加ssh密钥保存至本地功能 ✔
|
||||
|
||||
## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12)
|
||||
|
||||
### Features
|
||||
|
18
Q&A.md
18
Q&A.md
@ -1,5 +1,23 @@
|
||||
# Q&A
|
||||
|
||||
## ssh连接失败
|
||||
|
||||
首先确定用户名/密码/密钥没错,接着排查服务端ssh登录日志,例如Debian12 `journalctl -u ssh -f`
|
||||
|
||||
如果出现类似以下日志:
|
||||
```shell
|
||||
Jul 10 12:29:11 iZ2ze5f4ne9xf8n3h5Z sshd[8020]: userauth_pubkey: signature algorithm ssh-rsa not in PubkeyAcceptedAlgorithms [preauth]
|
||||
```
|
||||
|
||||
说明客户端 `ssh-rsa` 签名算法不在 `PubkeyAcceptedAlgorithms` 列表中,目标服务器不接受 ssh-rsa 签名算法的公钥认证。
|
||||
|
||||
**解决: **
|
||||
编辑 /etc/ssh/sshd_config 文件,添加或修改以下配置
|
||||
```shell
|
||||
PubkeyAcceptedAlgorithms +ssh-rsa
|
||||
```
|
||||
重新启动 SSH 服务: `sudo systemctl restart sshd`
|
||||
|
||||
## CentOS7/8启动服务失败
|
||||
|
||||
> 先关闭SELinux
|
||||
|
@ -19,6 +19,7 @@
|
||||
- [客户端](#客户端)
|
||||
- [版本日志](#版本日志)
|
||||
- [安全与说明](#安全与说明)
|
||||
- [开发](#开发)
|
||||
- [Q&A](#qa)
|
||||
- [感谢Star](#感谢star)
|
||||
- [License](#license)
|
||||
@ -160,6 +161,13 @@ wget -qO- --no-check-certificate https://ghproxy.com/https://raw.githubuserconte
|
||||
|
||||
> webssh功能需要的密钥信息全部保存在服务端服务器的`app\storage\ssh-record.json`中. 在保存ssh密钥信息到服务器储存与传输过程皆已加密,`不过最好还是套https使用`
|
||||
|
||||
## 开发
|
||||
|
||||
1. 拉取代码,准备nodejs环境>=16
|
||||
2. cd到项目根目录,yarn执行安装依赖
|
||||
3. cd web 启动前端 npm run dev
|
||||
4. cd server 启动服务端 npm run local
|
||||
|
||||
## Q&A
|
||||
|
||||
- [Q&A](./Q%26A.md)
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easynode",
|
||||
"version": "1.0.0",
|
||||
"description": "easy to manage the server",
|
||||
"description": "web ssh",
|
||||
"private": true,
|
||||
"workspaces": ["server", "client"],
|
||||
"repository": {
|
||||
|
7
server/.gitignore
vendored
7
server/.gitignore
vendored
@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
app/static/upload/*
|
||||
app/socket/temp/*
|
||||
app/socket/.sftp-cache/*
|
||||
app/logs/*
|
||||
!.gitkeep
|
||||
dist
|
@ -2,19 +2,19 @@ const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
httpPort: 8082,
|
||||
clientPort: 22022, // 勿更改
|
||||
clientPort: 22022, // 暂不支持更改
|
||||
uploadDir: path.join(process.cwd(),'app/static/upload'),
|
||||
staticDir: path.join(process.cwd(),'app/static'),
|
||||
sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'),
|
||||
sshRecordPath: path.join(process.cwd(),'app/storage/ssh-record.json'),
|
||||
keyPath: path.join(process.cwd(),'app/storage/key.json'),
|
||||
hostListPath: path.join(process.cwd(),'app/storage/host-list.json'),
|
||||
emailPath: path.join(process.cwd(),'app/storage/email.json'),
|
||||
notifyPath: path.join(process.cwd(),'app/storage/notify.json'),
|
||||
groupPath: path.join(process.cwd(),'app/storage/group.json'),
|
||||
sshRecordDBPath: path.join(process.cwd(),'app/db/ssh-record.db'),
|
||||
keyDBPath: path.join(process.cwd(),'app/db/key.db'),
|
||||
hostListDBPath: path.join(process.cwd(),'app/db/host-list.db'),
|
||||
notifyConfDBPath: path.join(process.cwd(),'app/db/notify.db'),
|
||||
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
|
||||
emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'),
|
||||
apiPrefix: '/api/v1',
|
||||
logConfig: {
|
||||
outDir: path.join(process.cwd(),'./app/logs'),
|
||||
recordLog: false // 是否记录日志
|
||||
recordLog: true // 是否记录日志
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,51 @@
|
||||
const { readGroupList, writeGroupList, readHostList, writeHostList,randomStr } = require('../utils')
|
||||
const { readGroupList, writeGroupList, readHostList, writeHostList, randomStr } = require('../utils')
|
||||
|
||||
function getGroupList({ res }) {
|
||||
const data = readGroupList()
|
||||
async function getGroupList({ res }) {
|
||||
const data = await readGroupList()
|
||||
data?.sort((a, b) => a.index - b.index)
|
||||
res.success({ data })
|
||||
}
|
||||
|
||||
const addGroupList = async ({ res, request }) => {
|
||||
let { body: { name, index } } = request
|
||||
if(!name) return res.fail({ data: false, msg: '参数错误' })
|
||||
let groupList = readGroupList()
|
||||
if (!name) return res.fail({ data: false, msg: '参数错误' })
|
||||
let groupList = await readGroupList()
|
||||
let group = { id: randomStr(), name, index }
|
||||
groupList.push(group)
|
||||
groupList.sort((a, b) => a.index - b.index)
|
||||
writeGroupList(groupList)
|
||||
await writeGroupList(groupList)
|
||||
res.success({ data: '新增成功' })
|
||||
}
|
||||
|
||||
const updateGroupList = async ({ res, request }) => {
|
||||
let { params: { id } } = request
|
||||
let { body: { name, index } } = request
|
||||
if(!id || !name) return res.fail({ data: false, msg: '参数错误' })
|
||||
let groupList = readGroupList()
|
||||
if (!id || !name) return res.fail({ data: false, msg: '参数错误' })
|
||||
let groupList = await readGroupList()
|
||||
let idx = groupList.findIndex(item => item.id === id)
|
||||
let group = { id, name, index }
|
||||
if(idx === -1) return res.fail({ data: false, msg: `分组ID${ id }不存在` })
|
||||
let group = { id, name, index: Number(index) || 0 }
|
||||
if (idx === -1) return res.fail({ data: false, msg: `分组ID${id}不存在` })
|
||||
groupList.splice(idx, 1, group)
|
||||
groupList.sort((a, b) => a.index - b.index)
|
||||
writeGroupList(groupList)
|
||||
await writeGroupList(groupList)
|
||||
res.success({ data: '修改成功' })
|
||||
}
|
||||
|
||||
const removeGroup = async ({ res, request }) => {
|
||||
let { params: { id } } = request
|
||||
if(id ==='default') return res.fail({ data: false, msg: '保留分组, 禁止删除' })
|
||||
let groupList = readGroupList()
|
||||
if (id === 'default') return res.fail({ data: false, msg: '保留分组, 禁止删除' })
|
||||
let groupList = await readGroupList()
|
||||
let idx = groupList.findIndex(item => item.id === id)
|
||||
if(idx === -1) return res.fail({ msg: '分组不存在' })
|
||||
if (idx === -1) return res.fail({ msg: '分组不存在' })
|
||||
|
||||
// 移除分组将所有该分组下host分配到default中去
|
||||
let hostList = readHostList()
|
||||
hostList = hostList.map((item) => {
|
||||
if(item.group === groupList[idx].id) item.group = 'default'
|
||||
let hostList = await readHostList()
|
||||
hostList = hostList?.map((item) => {
|
||||
if (item.group === groupList[idx].id) item.group = 'default'
|
||||
return item
|
||||
})
|
||||
writeHostList(hostList)
|
||||
await writeHostList(hostList)
|
||||
|
||||
groupList.splice(idx, 1)
|
||||
writeGroupList(groupList)
|
||||
await writeGroupList(groupList)
|
||||
|
||||
res.success({ data: '移除成功' })
|
||||
}
|
||||
|
@ -1,58 +1,61 @@
|
||||
const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils')
|
||||
|
||||
function getHostList({ res }) {
|
||||
const data = readHostList()
|
||||
async function getHostList({ res }) {
|
||||
console.log('get - host - list')
|
||||
const data = await readHostList()
|
||||
res.success({ data })
|
||||
}
|
||||
|
||||
function saveHost({ res, request }) {
|
||||
async function saveHost({ res, request }) {
|
||||
let { body: { host: newHost, name, expired, expiredNotify, group, consoleUrl, remark } } = request
|
||||
if(!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
||||
let hostList = readHostList()
|
||||
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` })
|
||||
console.log(request)
|
||||
if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
|
||||
let hostList = await readHostList()
|
||||
if (hostList?.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${newHost}已存在` })
|
||||
if (!Array.isArray(hostList)) hostList = []
|
||||
hostList.push({ host: newHost, name, expired, expiredNotify, group, consoleUrl, remark })
|
||||
writeHostList(hostList)
|
||||
await writeHostList(hostList)
|
||||
res.success()
|
||||
}
|
||||
|
||||
function updateHost({ res, request }) {
|
||||
async function updateHost({ res, request }) {
|
||||
let { body: { host: newHost, name: newName, oldHost, expired, expiredNotify, group, consoleUrl, remark } } = request
|
||||
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||
let hostList = readHostList()
|
||||
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` })
|
||||
if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
|
||||
let hostList = await readHostList()
|
||||
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${newHost}不存在` })
|
||||
let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
|
||||
hostList.splice(targetIdx, 1, { name: newName, host: newHost, expired, expiredNotify, group, consoleUrl, remark })
|
||||
writeHostList(hostList)
|
||||
res.success()
|
||||
}
|
||||
|
||||
function removeHost({ res, request }) {
|
||||
async function removeHost({ res, request }) {
|
||||
let { body: { host } } = request
|
||||
let hostList = readHostList()
|
||||
let hostList = await readHostList()
|
||||
let hostIdx = hostList.findIndex(item => item.host === host)
|
||||
if(hostIdx === -1) return res.fail({ msg: `${ host }不存在` })
|
||||
if (hostIdx === -1) return res.fail({ msg: `${host}不存在` })
|
||||
hostList.splice(hostIdx, 1)
|
||||
writeHostList(hostList)
|
||||
// 查询是否存在ssh记录
|
||||
let sshRecord = readSSHRecord()
|
||||
let sshRecord = await readSSHRecord()
|
||||
let sshIdx = sshRecord.findIndex(item => item.host === host)
|
||||
let flag = sshIdx !== -1
|
||||
if(flag) sshRecord.splice(sshIdx, 1)
|
||||
if (flag) sshRecord.splice(sshIdx, 1)
|
||||
writeSSHRecord(sshRecord)
|
||||
|
||||
res.success({ data: `${ host }已移除, ${ flag ? '并移除ssh记录' : '' }` })
|
||||
res.success({ data: `${host}已移除, ${flag ? '并移除ssh记录' : ''}` })
|
||||
}
|
||||
|
||||
function updateHostSort({ res, request }) {
|
||||
async function updateHostSort({ res, request }) {
|
||||
let { body: { list } } = request
|
||||
if(!list) return res.fail({ msg: '参数错误' })
|
||||
let hostList = readHostList()
|
||||
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
|
||||
if (!list) return res.fail({ msg: '参数错误' })
|
||||
let hostList = await readHostList()
|
||||
if (hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
|
||||
let sortResult = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const curHost = list[i]
|
||||
let temp = hostList.find(({ host }) => curHost.host === host)
|
||||
if(!temp) return res.fail({ msg: `查找失败: ${ curHost.name }` })
|
||||
if (!temp) return res.fail({ msg: `查找失败: ${curHost.name}` })
|
||||
sortResult.push(temp)
|
||||
}
|
||||
writeHostList(sortResult)
|
||||
|
@ -7,14 +7,14 @@ const {
|
||||
writeNotifyList } = require('../utils')
|
||||
const commonTemp = require('../template/commonTemp')
|
||||
|
||||
function getSupportEmailList({ res }) {
|
||||
const data = readSupportEmailList()
|
||||
async function getSupportEmailList({ res }) {
|
||||
const data = await readSupportEmailList()
|
||||
res.success({ data })
|
||||
}
|
||||
|
||||
function getUserEmailList({ res }) {
|
||||
const userEmailList = readUserEmailList().map(({ target, auth: { user } }) => ({ target, user }))
|
||||
const supportEmailList = readSupportEmailList()
|
||||
async function getUserEmailList({ res }) {
|
||||
const userEmailList = (await readUserEmailList()).map(({ target, auth: { user } }) => ({ target, user }))
|
||||
const supportEmailList = await readSupportEmailList()
|
||||
const data = userEmailList.map(({ target: userTarget, user: email }) => {
|
||||
let name = supportEmailList.find(({ target: supportTarget }) => supportTarget === userTarget).name
|
||||
return { name, email }
|
||||
@ -24,57 +24,57 @@ function getUserEmailList({ res }) {
|
||||
|
||||
async function pushEmail({ res, request }) {
|
||||
let { body: { toEmail, isTest } } = request
|
||||
if(!isTest) return res.fail({ msg: '此接口暂时只做测试邮件使用, 需传递参数isTest: true' })
|
||||
if (!isTest) return res.fail({ msg: '此接口暂时只做测试邮件使用, 需传递参数isTest: true' })
|
||||
consola.info('发送测试邮件:', toEmail)
|
||||
let { code, msg } = await emailTransporter({ toEmail, title: '测试邮件', html: commonTemp('邮件通知测试邮件') })
|
||||
msg = msg && msg.message || msg
|
||||
if(code === 0) return res.success({ msg })
|
||||
if (code === 0) return res.success({ msg })
|
||||
return res.fail({ msg })
|
||||
}
|
||||
|
||||
function updateUserEmailList({ res, request }) {
|
||||
async function updateUserEmailList({ res, request }) {
|
||||
let { body: { target, auth } } = request
|
||||
const supportList = readSupportEmailList()
|
||||
const supportList = await readSupportEmailList()
|
||||
let flag = supportList.some((item) => item.target === target)
|
||||
if(!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` })
|
||||
if(!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' })
|
||||
if (!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` })
|
||||
if (!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' })
|
||||
|
||||
let newUserEmail = { target, auth }
|
||||
let userEmailList = readUserEmailList()
|
||||
let userEmailList = await readUserEmailList()
|
||||
let idx = userEmailList.findIndex(({ auth: { user } }) => auth.user === user)
|
||||
if(idx !== -1) userEmailList.splice(idx, 1, newUserEmail)
|
||||
if (idx !== -1) userEmailList.splice(idx, 1, newUserEmail)
|
||||
else userEmailList.unshift(newUserEmail)
|
||||
|
||||
const { code, msg } = writeUserEmailList(userEmailList)
|
||||
if(code === 0) return res.success()
|
||||
const { code, msg } = await writeUserEmailList(userEmailList)
|
||||
if (code === 0) return res.success()
|
||||
return res.fail({ msg })
|
||||
}
|
||||
|
||||
function removeUserEmail({ res, request }) {
|
||||
async function removeUserEmail({ res, request }) {
|
||||
let { params: { email } } = request
|
||||
const userEmailList = readUserEmailList()
|
||||
const userEmailList = await readUserEmailList()
|
||||
let idx = userEmailList.findIndex(({ auth: { user } }) => user === email)
|
||||
if(idx === -1) return res.fail({ msg: `删除失败, 不存在该邮箱:${ email }` })
|
||||
if (idx === -1) return res.fail({ msg: `删除失败, 不存在该邮箱:${ email }` })
|
||||
userEmailList.splice(idx, 1)
|
||||
const { code, msg } = writeUserEmailList(userEmailList)
|
||||
if(code === 0) return res.success()
|
||||
const { code, msg } = await writeUserEmailList(userEmailList)
|
||||
if (code === 0) return res.success()
|
||||
return res.fail({ msg })
|
||||
}
|
||||
|
||||
function getNotifyList({ res }) {
|
||||
const data = readNotifyList()
|
||||
async function getNotifyList({ res }) {
|
||||
const data = await readNotifyList()
|
||||
res.success({ data })
|
||||
}
|
||||
|
||||
function updateNotifyList({ res, request }) {
|
||||
async function updateNotifyList({ res, request }) {
|
||||
let { body: { type, sw } } = request
|
||||
if(!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` })
|
||||
const notifyList = readNotifyList()
|
||||
if (!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw:${ sw }, must be Boolean` })
|
||||
const notifyList = await readNotifyList()
|
||||
let target = notifyList.find((item) => item.type === type)
|
||||
if(!target) return res.fail({ msg: `更新失败, 不存在该通知类型:${ type }` })
|
||||
if (!target) return res.fail({ msg: `更新失败, 不存在该通知类型:${ type }` })
|
||||
target.sw = sw
|
||||
console.log(notifyList)
|
||||
writeNotifyList(notifyList)
|
||||
// console.log(notifyList)
|
||||
await writeNotifyList(notifyList)
|
||||
res.success()
|
||||
}
|
||||
|
||||
|
@ -1,47 +1,47 @@
|
||||
const { readSSHRecord, writeSSHRecord, AESEncrypt } = require('../utils')
|
||||
const { readSSHRecord, writeSSHRecord, AESEncryptSync } = require('../utils')
|
||||
|
||||
const updateSSH = async ({ res, request }) => {
|
||||
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
|
||||
let record = { host, port, username, type, password, privateKey, randomKey, command }
|
||||
if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' })
|
||||
// 再做一次对称加密(方便ssh连接时解密)
|
||||
record.randomKey = AESEncrypt(randomKey)
|
||||
let sshRecord = readSSHRecord()
|
||||
record.randomKey = await AESEncryptSync(randomKey)
|
||||
let sshRecord = await readSSHRecord()
|
||||
let idx = sshRecord.findIndex(item => item.host === host)
|
||||
if(idx === -1)
|
||||
sshRecord.push(record)
|
||||
else
|
||||
sshRecord.splice(idx, 1, record)
|
||||
writeSSHRecord(sshRecord)
|
||||
await writeSSHRecord(sshRecord)
|
||||
consola.info('新增凭证:', host)
|
||||
res.success({ data: '保存成功' })
|
||||
}
|
||||
|
||||
const removeSSH = async ({ res, request }) => {
|
||||
let { body: { host } } = request
|
||||
let sshRecord = readSSHRecord()
|
||||
let sshRecord = await readSSHRecord()
|
||||
let idx = sshRecord.findIndex(item => item.host === host)
|
||||
if(idx === -1) return res.fail({ msg: '凭证不存在' })
|
||||
sshRecord.splice(idx, 1)
|
||||
consola.info('移除凭证:', host)
|
||||
writeSSHRecord(sshRecord)
|
||||
await writeSSHRecord(sshRecord)
|
||||
res.success({ data: '移除成功' })
|
||||
}
|
||||
|
||||
const existSSH = async ({ res, request }) => {
|
||||
let { body: { host } } = request
|
||||
let sshRecord = readSSHRecord()
|
||||
let idx = sshRecord.findIndex(item => item.host === host)
|
||||
let sshRecord = await readSSHRecord()
|
||||
let isExist = sshRecord?.some(item => item.host === host)
|
||||
consola.info('查询凭证:', host)
|
||||
if(idx === -1) return res.success({ data: false }) // host不存在
|
||||
if(!isExist) return res.success({ data: false }) // host不存在
|
||||
res.success({ data: true }) // 存在
|
||||
}
|
||||
|
||||
const getCommand = async ({ res, request }) => {
|
||||
let { host } = request.query
|
||||
if(!host) return res.fail({ data: false, msg: '参数错误' })
|
||||
let sshRecord = readSSHRecord()
|
||||
let record = sshRecord.find(item => item.host === host)
|
||||
let sshRecord = await readSSHRecord()
|
||||
let record = sshRecord?.find(item => item.host === host)
|
||||
consola.info('查询登录后执行的指令:', host)
|
||||
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
|
||||
const { command } = record
|
||||
|
@ -1,8 +1,8 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { getNetIPInfo, readKey, writeKey, RSADecrypt, AESEncrypt, SHA1Encrypt, sendEmailToConfList, getNotifySwByType } = require('../utils')
|
||||
const { getNetIPInfo, readKey, writeKey, RSADecryptSync, AESEncryptSync, SHA1Encrypt, sendEmailToConfList, getNotifySwByType } = require('../utils')
|
||||
|
||||
const getpublicKey = ({ res }) => {
|
||||
let { publicKey: data } = readKey()
|
||||
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 })
|
||||
res.success({ data })
|
||||
}
|
||||
@ -50,9 +50,9 @@ const login = async ({ res, request }) => {
|
||||
// 登录流程
|
||||
try {
|
||||
// console.log('ciphertext', ciphertext)
|
||||
let password = RSADecrypt(ciphertext)
|
||||
let password = await RSADecryptSync(ciphertext)
|
||||
// console.log('Decrypt解密password:', password)
|
||||
let { pwd } = readKey()
|
||||
let { pwd } = await readKey()
|
||||
if(password === 'admin' && pwd === 'admin') {
|
||||
const token = await beforeLoginHandler(clientIp, jwtExpires)
|
||||
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
|
||||
@ -72,13 +72,14 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => {
|
||||
|
||||
// consola.success('登录成功, 准备生成token', new Date())
|
||||
// 生产token
|
||||
let { commonKey } = readKey()
|
||||
let { commonKey } = await readKey()
|
||||
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
|
||||
token = AESEncrypt(token) // 对称加密token后再传输给前端
|
||||
token = await AESEncryptSync(token) // 对称加密token后再传输给前端
|
||||
|
||||
// 记录客户端登录IP(用于判断是否异地且只保留最近10条)
|
||||
const clientIPInfo = await getNetIPInfo(clientIp)
|
||||
const { ip, country, city } = clientIPInfo || {}
|
||||
consola.info('登录成功:', new Date(), { ip, country, city })
|
||||
|
||||
// 邮件登录通知
|
||||
let sw = getNotifySwByType('login')
|
||||
@ -91,14 +92,14 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => {
|
||||
|
||||
const updatePwd = async ({ res, request }) => {
|
||||
let { body: { oldPwd, newPwd } } = request
|
||||
let rsaOldPwd = RSADecrypt(oldPwd)
|
||||
let rsaOldPwd = await RSADecryptSync(oldPwd)
|
||||
oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd)
|
||||
let keyObj = readKey()
|
||||
let keyObj = await readKey()
|
||||
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' })
|
||||
// 旧密钥校验通过,加密保存新密码
|
||||
newPwd = RSADecrypt(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(RSADecrypt(newPwd))
|
||||
newPwd = await RSADecryptSync(newPwd) === 'admin' ? 'admin' : SHA1Encrypt(await RSADecryptSync(newPwd))
|
||||
keyObj.pwd = newPwd
|
||||
writeKey(keyObj)
|
||||
await writeKey(keyObj)
|
||||
|
||||
let sw = getNotifySwByType('updatePwd')
|
||||
if(sw) sendEmailToConfList('密码修改提醒', '面板登录密码已更改')
|
||||
|
154
server/app/db.js
Normal file
154
server/app/db.js
Normal file
@ -0,0 +1,154 @@
|
||||
const Datastore = require('@seald-io/nedb')
|
||||
const { resolvePath } = require('./utils/tools')
|
||||
const { writeKey, writeNotifyList, writeGroupList } = require('./utils/storage')
|
||||
const { KeyDB, NotifyDB, GroupDB, EmailNotifyDB } = require('./utils/db-class')
|
||||
|
||||
function initKeyDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const keyDB = new KeyDB().getInstance()
|
||||
keyDB.count({}, async (err, count) => {
|
||||
if (err) {
|
||||
consola.log('初始化keyDB错误:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
if (count === 0) {
|
||||
consola.log('初始化keyDB✔')
|
||||
const defaultData = {
|
||||
pwd: "admin",
|
||||
commonKey: "",
|
||||
publicKey: "",
|
||||
privateKey: ""
|
||||
}
|
||||
await writeKey(defaultData)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initNotifyDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const notifyDB = new NotifyDB().getInstance()
|
||||
notifyDB.count({}, async (err, count) => {
|
||||
if (err) {
|
||||
consola.log('初始化notifyDB错误:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
if (count === 0) {
|
||||
consola.log('初始化notifyDB✔')
|
||||
const defaultData = [{
|
||||
"type": "login",
|
||||
"desc": "登录面板提醒",
|
||||
"sw": true
|
||||
},
|
||||
{
|
||||
"type": "err_login",
|
||||
"desc": "登录错误提醒(连续5次)",
|
||||
"sw": true
|
||||
},
|
||||
{
|
||||
"type": "updatePwd",
|
||||
"desc": "修改密码提醒",
|
||||
"sw": true
|
||||
},
|
||||
{
|
||||
"type": "host_offline",
|
||||
"desc": "客户端离线提醒(每小时最多发送一次提醒)",
|
||||
"sw": true
|
||||
}]
|
||||
await writeNotifyList(defaultData)
|
||||
}
|
||||
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initGroupDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const groupDB = new GroupDB().getInstance()
|
||||
groupDB.count({}, async (err, count) => {
|
||||
if (err) {
|
||||
consola.log('初始化groupDB错误:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
if (count === 0) {
|
||||
consola.log('初始化groupDB✔')
|
||||
const defaultData = [{ "id": "default", "name": "默认分组", "index": 0 }]
|
||||
await writeGroupList(defaultData)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initEmailNotifyDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const emailNotifyDB = new EmailNotifyDB().getInstance()
|
||||
emailNotifyDB.count({}, async (err, count) => {
|
||||
if (err) {
|
||||
consola.log('初始化emailNotifyDB错误:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
if (count === 0) {
|
||||
consola.log('初始化emailNotifyDB✔')
|
||||
const defaultData = {
|
||||
"support": [
|
||||
{
|
||||
"name": "QQ邮箱",
|
||||
"target": "qq",
|
||||
"host": "smtp.qq.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "网易126",
|
||||
"target": "wangyi126",
|
||||
"host": "smtp.126.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "网易163",
|
||||
"target": "wangyi163",
|
||||
"host": "smtp.163.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"user": [
|
||||
]
|
||||
}
|
||||
emailNotifyDB.update({}, { $set: defaultData }, { upsert: true }, (err, numReplaced) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
emailNotifyDB.compactDatafile()
|
||||
resolve(numReplaced)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
module.exports = async () => {
|
||||
await initKeyDB()
|
||||
await initNotifyDB()
|
||||
await initGroupDB()
|
||||
await initEmailNotifyDB()
|
||||
}
|
@ -1,38 +1,40 @@
|
||||
# host-list.json
|
||||
|
||||
> 存储服务器基本信息
|
||||
|
||||
# key.json
|
||||
|
||||
> 用于加密的密钥相关
|
||||
|
||||
# ssh-record.json
|
||||
|
||||
> ssh密钥记录(加密存储)
|
||||
|
||||
# email.json
|
||||
|
||||
> 邮件配置
|
||||
|
||||
- port: 587 --> secure: false
|
||||
```json
|
||||
// Gmail调试不通过, 暂缓
|
||||
{
|
||||
"name": "Google邮箱",
|
||||
"target": "google",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# notify.json
|
||||
|
||||
> 通知配置
|
||||
|
||||
# group.json
|
||||
|
||||
> 服务器分组配置
|
||||
db目录,初始化后自动生成
|
||||
|
||||
**host-list.db**
|
||||
|
||||
> 存储服务器基本信息
|
||||
|
||||
**key.db**
|
||||
|
||||
> 用于加密的密钥相关
|
||||
|
||||
**ssh-record.db**
|
||||
|
||||
> ssh密钥记录(加密存储)
|
||||
|
||||
**email.db**
|
||||
|
||||
> 邮件配置
|
||||
|
||||
- port: 587 --> secure: false
|
||||
```db
|
||||
// Gmail调试不通过, 暂缓
|
||||
{
|
||||
"name": "Google邮箱",
|
||||
"target": "google",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**notify.db**
|
||||
|
||||
> 通知配置
|
||||
|
||||
**group.db**
|
||||
|
||||
> 服务器分组配置
|
@ -1,5 +1,5 @@
|
||||
const NodeRSA = require('node-rsa')
|
||||
const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncrypt } = require('./utils')
|
||||
const { getNetIPInfo, readHostList, writeHostList, readKey, writeKey, randomStr, isProd, AESEncryptSync } = require('./utils')
|
||||
|
||||
const isDev = !isProd()
|
||||
|
||||
@ -7,7 +7,7 @@ const isDev = !isProd()
|
||||
async function initLocalIp() {
|
||||
if(isDev) return consola.info('非生产环境不初始化保存本地IP')
|
||||
const localNetIPInfo = await getNetIPInfo()
|
||||
let vpsList = readHostList()
|
||||
let vpsList = await readHostList()
|
||||
let { ip: localNetIP } = localNetIPInfo
|
||||
if(vpsList.some(({ host }) => host === localNetIP)) return consola.info('本机IP已储存: ', localNetIP)
|
||||
vpsList.unshift({ name: 'server-side-host', host: localNetIP, group: 'default' })
|
||||
@ -17,33 +17,32 @@ async function initLocalIp() {
|
||||
|
||||
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
|
||||
async function initRsa() {
|
||||
let keyObj = readKey()
|
||||
let keyObj = await readKey()
|
||||
if(keyObj.privateKey && keyObj.publicKey) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
|
||||
|
||||
let key = new NodeRSA({ b: 1024 })
|
||||
key.setOptions({ encryptionScheme: 'pkcs1' })
|
||||
let privateKey = key.exportKey('pkcs1-private-pem')
|
||||
let publicKey = key.exportKey('pkcs8-public-pem')
|
||||
keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥
|
||||
keyObj.privateKey = await AESEncryptSync(privateKey) // 加密私钥
|
||||
keyObj.publicKey = publicKey // 公开公钥
|
||||
writeKey(keyObj)
|
||||
await writeKey(keyObj)
|
||||
consola.info('Task: 已生成新的非对称加密公私钥')
|
||||
}
|
||||
|
||||
// 随机的commonKey secret
|
||||
function randomJWTSecret() {
|
||||
let keyObj = readKey()
|
||||
if(keyObj.commonKey) return consola.info('commonKey密钥已存在')
|
||||
async function randomJWTSecret() {
|
||||
let keyObj = await readKey()
|
||||
if(keyObj?.commonKey) return consola.info('commonKey密钥已存在')
|
||||
|
||||
keyObj.commonKey = randomStr(16)
|
||||
writeKey(keyObj)
|
||||
await writeKey(keyObj)
|
||||
consola.info('Task: 已生成新的随机commonKey密钥')
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
randomJWTSecret() // 先生成全局唯一密钥
|
||||
initLocalIp()
|
||||
initRsa()
|
||||
module.exports = async () => {
|
||||
await randomJWTSecret() // 全局密钥
|
||||
await initRsa() // 全局公钥密钥
|
||||
// initLocalIp() // :TODO: 默认添加服务端vps
|
||||
// 用于记录客户端登录IP的列表
|
||||
global.loginRecord = []
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
const consola = require('consola')
|
||||
global.consola = consola
|
||||
const { httpServer, clientHttpServer } = require('./server')
|
||||
const initLocal = require('./init')
|
||||
const scheduleJob = require('./schedule')
|
||||
const initDB = require('./db')
|
||||
const initEncryptConf = require('./init')
|
||||
// const scheduleJob = require('./schedule')
|
||||
|
||||
scheduleJob()
|
||||
async function main() {
|
||||
await initDB()
|
||||
await initEncryptConf()
|
||||
httpServer()
|
||||
clientHttpServer()
|
||||
// scheduleJob()
|
||||
}
|
||||
|
||||
initLocal()
|
||||
|
||||
httpServer()
|
||||
|
||||
clientHttpServer()
|
||||
main()
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { verifyAuth } = require('../utils')
|
||||
const { verifyAuthSync } = require('../utils')
|
||||
const { apiPrefix } = require('../config')
|
||||
|
||||
let whitePath = [
|
||||
@ -13,7 +13,7 @@ const useAuth = async ({ request, res }, next) => {
|
||||
if(whitePath.includes(path)) return next()
|
||||
if(!token) return res.fail({ msg: '未登录', status: 403 })
|
||||
// 验证token
|
||||
const { code, msg } = verifyAuth(token, request.ip)
|
||||
const { code, msg } = await verifyAuthSync(token, request.ip)
|
||||
switch(code) {
|
||||
case 1:
|
||||
return await next()
|
||||
|
@ -1,3 +1,4 @@
|
||||
// 响应压缩模块,自适应头部压缩方式
|
||||
const compress = require('koa-compress')
|
||||
|
||||
const options = { threshold: 2048 }
|
||||
|
@ -88,13 +88,13 @@ const notify = [
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/push-email',
|
||||
controller: pushEmail
|
||||
path: '/user-email',
|
||||
controller: updateUserEmailList
|
||||
},
|
||||
{
|
||||
method: 'post',
|
||||
path: '/user-email',
|
||||
controller: updateUserEmailList
|
||||
path: '/push-email',
|
||||
controller: pushEmail
|
||||
},
|
||||
{
|
||||
method: 'delete',
|
||||
|
@ -3,7 +3,7 @@ const { readHostList, sendEmailToConfList, formatTimestamp } = require('../utils
|
||||
|
||||
const expiredNotifyJob = () => {
|
||||
consola.info('=====开始检测服务器到期时间=====', new Date())
|
||||
const hostList = readHostList()
|
||||
const hostList = await readHostList()
|
||||
for (const item of hostList) {
|
||||
if(!item.expiredNotify) continue
|
||||
const { host, name, expired, consoleUrl } = item
|
||||
|
@ -4,11 +4,12 @@ const { readHostList, sendEmailToConfList, getNotifySwByType, formatTimestamp, i
|
||||
const testConnectAsync = require('../utils/test-connect')
|
||||
|
||||
let sendNotifyRecord = new Map()
|
||||
const offlineJob = () => {
|
||||
const offlineJob = async () => {
|
||||
let sw = getNotifySwByType('host_offline')
|
||||
if(!sw) return
|
||||
consola.info('=====开始检测服务器状态=====', new Date())
|
||||
for (const item of readHostList()) {
|
||||
const hostList = await readHostList()
|
||||
for (const item of hostList) {
|
||||
const { host, name } = item
|
||||
// consola.info('start inpect:', host, name )
|
||||
testConnectAsync({
|
||||
|
@ -2,15 +2,15 @@ const { Server: ServerIO } = require('socket.io')
|
||||
const { io: ClientIO } = require('socket.io-client')
|
||||
const { readHostList } = require('../utils')
|
||||
const { clientPort } = require('../config')
|
||||
const { verifyAuth } = require('../utils')
|
||||
const { verifyAuthSync } = require('../utils')
|
||||
|
||||
let clientSockets = {}, clientsData = {}
|
||||
|
||||
function getClientsInfo(socketId) {
|
||||
let hostList = readHostList()
|
||||
async function getClientsInfo(socketId) {
|
||||
let hostList = await readHostList()
|
||||
hostList
|
||||
.map(({ host, name }) => {
|
||||
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, {
|
||||
?.map(({ host, name }) => {
|
||||
let clientSocket = ClientIO(`http://${host}:${clientPort}`, {
|
||||
path: '/client/os-info',
|
||||
forceNew: true,
|
||||
timeout: 5000,
|
||||
@ -58,10 +58,10 @@ module.exports = (httpServer) => {
|
||||
serverIo.on('connection', (socket) => {
|
||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||
socket.on('init_clients_data', ({ token }) => {
|
||||
socket.on('init_clients_data', async ({ token }) => {
|
||||
// 校验登录态
|
||||
const { code, msg } = verifyAuth(token, clientIp)
|
||||
if(code !== 1) {
|
||||
const { code, msg } = await verifyAuthSync(token, clientIp)
|
||||
if (code !== 1) {
|
||||
socket.emit('token_verify_fail', msg || '鉴权失败')
|
||||
socket.disconnect()
|
||||
return
|
||||
@ -86,7 +86,7 @@ module.exports = (httpServer) => {
|
||||
// 关闭连接
|
||||
socket.on('disconnect', () => {
|
||||
// 防止内存泄漏
|
||||
if(timer) clearInterval(timer)
|
||||
if (timer) clearInterval(timer)
|
||||
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
|
||||
clientSockets[socket.id].forEach(socket => socket.close && socket.close())
|
||||
delete clientSockets[socket.id]
|
||||
|
@ -1,7 +1,7 @@
|
||||
const { Server: ServerIO } = require('socket.io')
|
||||
const { io: ClientIO } = require('socket.io-client')
|
||||
const { clientPort } = require('../config')
|
||||
const { verifyAuth } = require('../utils')
|
||||
const { verifyAuthSync } = require('../utils')
|
||||
|
||||
let hostSockets = {}
|
||||
|
||||
@ -47,9 +47,9 @@ module.exports = (httpServer) => {
|
||||
serverIo.on('connection', (serverSocket) => {
|
||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address
|
||||
serverSocket.on('init_host_data', ({ token, host }) => {
|
||||
serverSocket.on('init_host_data', async ({ token, host }) => {
|
||||
// 校验登录态
|
||||
const { code, msg } = verifyAuth(token, clientIp)
|
||||
const { code, msg } = await verifyAuthSync(token, clientIp)
|
||||
if(code !== 1) {
|
||||
serverSocket.emit('token_verify_fail', msg || '鉴权失败')
|
||||
serverSocket.disconnect()
|
||||
|
@ -60,7 +60,7 @@ module.exports = (httpServer) => {
|
||||
}, 1000)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
// 断开时清楚对应的websocket连接
|
||||
// 断开时清除对应的websocket连接
|
||||
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
|
||||
delete serverSockets[socket.id]
|
||||
socket.close && socket.close()
|
||||
|
@ -2,7 +2,7 @@ const { Server } = require('socket.io')
|
||||
const SFTPClient = require('ssh2-sftp-client')
|
||||
const rawPath = require('path')
|
||||
const fs = require('fs')
|
||||
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
|
||||
const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
|
||||
const { sftpCacheDir } = require('../config')
|
||||
const CryptoJS = require('crypto-js')
|
||||
|
||||
@ -205,21 +205,21 @@ module.exports = (httpServer) => {
|
||||
let sftpClient = new SFTPClient()
|
||||
consola.success('terminal websocket 已连接')
|
||||
|
||||
socket.on('create', ({ host: ip, token }) => {
|
||||
const { code } = verifyAuth(token, clientIp)
|
||||
socket.on('create', async ({ host: ip, token }) => {
|
||||
const { code } = await verifyAuthSync(token, clientIp)
|
||||
if(code !== 1) {
|
||||
socket.emit('token_verify_fail')
|
||||
socket.disconnect()
|
||||
return
|
||||
}
|
||||
const sshRecord = readSSHRecord()
|
||||
const sshRecord = await readSSHRecord()
|
||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
||||
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
||||
let { type, host, port, username, randomKey } = loginInfo
|
||||
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
||||
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
||||
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
||||
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
||||
randomKey = await AESDecryptSync(randomKey) // 先对称解密key
|
||||
randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
|
||||
loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
|
||||
consola.info('准备连接Sftp:', host)
|
||||
const authInfo = { host, port, username, [type]: loginInfo[type] }
|
||||
sftpClient.connect(authInfo)
|
||||
|
@ -1,6 +1,6 @@
|
||||
const { Server } = require('socket.io')
|
||||
const { Client: SSHClient } = require('ssh2')
|
||||
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils')
|
||||
const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
|
||||
|
||||
function createTerminal(socket, sshClient) {
|
||||
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
||||
@ -16,7 +16,7 @@ function createTerminal(socket, sshClient) {
|
||||
})
|
||||
// web端输入
|
||||
socket.on('input', key => {
|
||||
if(sshClient._sock.writable === false) return consola.info('终端连接已关闭')
|
||||
if (sshClient._sock.writable === false) return consola.info('终端连接已关闭')
|
||||
stream.write(key)
|
||||
})
|
||||
socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了
|
||||
@ -42,35 +42,40 @@ module.exports = (httpServer) => {
|
||||
let sshClient = new SSHClient()
|
||||
consola.success('terminal websocket 已连接')
|
||||
|
||||
socket.on('create', ({ host: ip, token }) => {
|
||||
const { code } = verifyAuth(token, clientIp)
|
||||
if(code !== 1) {
|
||||
socket.on('create', async ({ host: ip, token }) => {
|
||||
const { code } = await verifyAuthSync(token, clientIp)
|
||||
if (code !== 1) {
|
||||
socket.emit('token_verify_fail')
|
||||
socket.disconnect()
|
||||
return
|
||||
}
|
||||
const sshRecord = readSSHRecord()
|
||||
const sshRecord = await readSSHRecord()
|
||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
||||
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
||||
if (!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ip}】凭证`)
|
||||
let { type, host, port, username, randomKey } = loginInfo
|
||||
try {
|
||||
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
||||
randomKey = AESDecrypt(randomKey) // 先对称解密key
|
||||
randomKey = RSADecrypt(randomKey) // 再非对称解密key
|
||||
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥
|
||||
randomKey = await AESDecryptSync(randomKey) // 先对称解密key
|
||||
randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
|
||||
loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
|
||||
consola.info('准备连接终端:', host)
|
||||
const authInfo = { host, port, username, [type]: loginInfo[type] }
|
||||
const authInfo = { host, port, username, [type]: loginInfo[type] } // .replace(/\n/g, '')
|
||||
// console.log(authInfo)
|
||||
sshClient
|
||||
.on('ready', () => {
|
||||
consola.success('已连接到终端:', host)
|
||||
socket.emit('connect_success', `已连接到终端:${ host }`)
|
||||
socket.emit('connect_success', `已连接到终端:${host}`)
|
||||
createTerminal(socket, sshClient)
|
||||
})
|
||||
.on('error', (err) => {
|
||||
console.log(err)
|
||||
consola.error('连接终端失败:', err.level)
|
||||
socket.emit('connect_fail', err.message)
|
||||
})
|
||||
.connect(authInfo)
|
||||
.connect({
|
||||
...authInfo,
|
||||
// debug: (info) => console.log(info)
|
||||
})
|
||||
} catch (err) {
|
||||
consola.error('创建终端失败:', err.message)
|
||||
socket.emit('create_fail', err.message)
|
||||
|
@ -1,36 +0,0 @@
|
||||
{
|
||||
"support": [
|
||||
{
|
||||
"name": "QQ邮箱",
|
||||
"target": "qq",
|
||||
"host": "smtp.qq.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "网易126",
|
||||
"target": "wangyi126",
|
||||
"host": "smtp.126.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "网易163",
|
||||
"target": "wangyi163",
|
||||
"host": "smtp.163.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"tls": {
|
||||
"rejectUnauthorized": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"user": [
|
||||
]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "default",
|
||||
"name": "默认分组",
|
||||
"index": 0
|
||||
}
|
||||
]
|
@ -1 +0,0 @@
|
||||
[]
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"pwd": "admin",
|
||||
"commonKey": "",
|
||||
"publicKey": "",
|
||||
"privateKey": ""
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
[
|
||||
{
|
||||
"type": "login",
|
||||
"desc": "登录面板提醒",
|
||||
"sw": true
|
||||
},
|
||||
{
|
||||
"type": "err_login",
|
||||
"desc": "登录错误提醒(连续5次)",
|
||||
"sw": true
|
||||
},
|
||||
{
|
||||
"type": "updatePwd",
|
||||
"desc": "修改密码提醒",
|
||||
"sw": true
|
||||
},
|
||||
{
|
||||
"type": "host_offline",
|
||||
"desc": "客户端离线提醒(每小时最多发送一次提醒)",
|
||||
"sw": true
|
||||
}
|
||||
]
|
@ -1 +0,0 @@
|
||||
[]
|
68
server/app/utils/db-class.js
Normal file
68
server/app/utils/db-class.js
Normal file
@ -0,0 +1,68 @@
|
||||
const Datastore = require('@seald-io/nedb')
|
||||
const { sshRecordDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath } = require('../config')
|
||||
|
||||
module.exports.KeyDB = class KeyDB {
|
||||
constructor() {
|
||||
if (!KeyDB.instance) {
|
||||
KeyDB.instance = new Datastore({ filename: keyDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return KeyDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.HostListDB = class HostListDB {
|
||||
constructor() {
|
||||
if (!HostListDB.instance) {
|
||||
HostListDB.instance = new Datastore({ filename: hostListDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return HostListDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.SshRecordDB = class SshRecordDB {
|
||||
constructor() {
|
||||
if (!SshRecordDB.instance) {
|
||||
SshRecordDB.instance = new Datastore({ filename: sshRecordDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return SshRecordDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.NotifyDB = class NotifyDB {
|
||||
constructor() {
|
||||
if (!NotifyDB.instance) {
|
||||
NotifyDB.instance = new Datastore({ filename: notifyConfDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return NotifyDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.GroupDB = class GroupDB {
|
||||
constructor() {
|
||||
if (!GroupDB.instance) {
|
||||
GroupDB.instance = new Datastore({ filename: groupConfDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return GroupDB.instance
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.EmailNotifyDB = class EmailNotifyDB {
|
||||
constructor() {
|
||||
if (!EmailNotifyDB.instance) {
|
||||
EmailNotifyDB.instance = new Datastore({ filename: emailNotifyDBPath, autoload: true })
|
||||
}
|
||||
}
|
||||
getInstance() {
|
||||
return EmailNotifyDB.instance
|
||||
}
|
||||
}
|
@ -11,12 +11,12 @@ const emailTransporter = async (params = {}) => {
|
||||
let { toEmail, title, html } = params
|
||||
try {
|
||||
if(!toEmail) throw Error('missing params: toEmail')
|
||||
let userEmail = readUserEmailList().find(({ auth }) => auth.user === toEmail)
|
||||
let userEmail = (await readUserEmailList()).find(({ auth }) => auth.user === toEmail)
|
||||
if(!userEmail) throw Error(`${ toEmail } 不存在已保存的配置文件中, 请移除后重新添加`)
|
||||
let { target } = userEmail
|
||||
let emailServerConf = readSupportEmailList().find((item) => item.target === target)
|
||||
let emailServerConf = (await readSupportEmailList()).find((item) => item.target === target)
|
||||
if(!emailServerConf) throw Error(`邮箱类型不支持:${ target }`)
|
||||
const timeout = 1000*6
|
||||
const timeout = 1000*5
|
||||
let options = Object.assign({}, userEmail, emailServerConf, { greetingTimeout: timeout, connectionTimeout: timeout })
|
||||
let transporter = nodemailer.createTransport(options)
|
||||
let info = await transporter.sendMail({
|
||||
@ -36,7 +36,7 @@ const emailTransporter = async (params = {}) => {
|
||||
const sendEmailToConfList = (title, content) => {
|
||||
// eslint-disable-next-line
|
||||
return new Promise(async (res, rej) => {
|
||||
let emailList = readUserEmailList()
|
||||
let emailList = await readUserEmailList()
|
||||
if(Array.isArray(emailList) && emailList.length >= 1) {
|
||||
for (const item of emailList) {
|
||||
const toEmail = item.auth.user
|
||||
|
@ -4,10 +4,10 @@ const NodeRSA = require('node-rsa')
|
||||
const { readKey } = require('./storage.js')
|
||||
|
||||
// rsa非对称 私钥解密
|
||||
const RSADecrypt = (ciphertext) => {
|
||||
if(!ciphertext) return
|
||||
let { privateKey } = readKey()
|
||||
privateKey = AESDecrypt(privateKey) // 先解密私钥
|
||||
const RSADecryptSync = async (ciphertext) => {
|
||||
if (!ciphertext) return
|
||||
let { privateKey } = await readKey()
|
||||
privateKey = await AESDecryptSync(privateKey) // 先解密私钥
|
||||
const rsakey = new NodeRSA(privateKey)
|
||||
rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt
|
||||
const plaintext = rsakey.decrypt(ciphertext, 'utf8')
|
||||
@ -15,17 +15,17 @@ const RSADecrypt = (ciphertext) => {
|
||||
}
|
||||
|
||||
// aes对称 加密(default commonKey)
|
||||
const AESEncrypt = (text, key) => {
|
||||
const AESEncryptSync = async (text, key) => {
|
||||
if(!text) return
|
||||
let { commonKey } = readKey()
|
||||
let { commonKey } = await readKey()
|
||||
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
// aes对称 解密(default commonKey)
|
||||
const AESDecrypt = (ciphertext, key) => {
|
||||
const AESDecryptSync = async (ciphertext, key) => {
|
||||
if(!ciphertext) return
|
||||
let { commonKey } = readKey()
|
||||
let { commonKey } = await readKey()
|
||||
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
|
||||
let originalText = bytes.toString(CryptoJS.enc.Utf8)
|
||||
return originalText
|
||||
@ -37,8 +37,8 @@ const SHA1Encrypt = (clearText) => {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RSADecrypt,
|
||||
AESEncrypt,
|
||||
AESDecrypt,
|
||||
RSADecryptSync,
|
||||
AESEncryptSync,
|
||||
AESDecryptSync,
|
||||
SHA1Encrypt
|
||||
}
|
@ -12,9 +12,10 @@ const {
|
||||
getNotifySwByType,
|
||||
writeNotifyList,
|
||||
readGroupList,
|
||||
writeGroupList } = require('./storage')
|
||||
const { RSADecrypt, AESEncrypt, AESDecrypt, SHA1Encrypt } = require('./encrypt')
|
||||
const { verifyAuth, isProd } = require('./verify-auth')
|
||||
writeGroupList
|
||||
} = require('./storage')
|
||||
const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt')
|
||||
const { verifyAuthSync, isProd } = require('./verify-auth')
|
||||
const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools')
|
||||
const { emailTransporter, sendEmailToConfList } = require('./email')
|
||||
|
||||
@ -25,11 +26,11 @@ module.exports = {
|
||||
randomStr,
|
||||
getUTCDate,
|
||||
formatTimestamp,
|
||||
verifyAuth,
|
||||
verifyAuthSync,
|
||||
isProd,
|
||||
RSADecrypt,
|
||||
AESEncrypt,
|
||||
AESDecrypt,
|
||||
RSADecryptSync,
|
||||
AESEncryptSync,
|
||||
AESDecryptSync,
|
||||
SHA1Encrypt,
|
||||
readSSHRecord,
|
||||
writeSSHRecord,
|
||||
|
@ -1,124 +1,239 @@
|
||||
const fs = require('fs')
|
||||
const { sshRecordPath, hostListPath, keyPath, emailPath, notifyPath, groupPath } = require('../config')
|
||||
const { sshRecordDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath } = require('../config')
|
||||
const { KeyDB, HostListDB, SshRecordDB, NotifyDB, GroupDB, EmailNotifyDB } = require('./db-class')
|
||||
|
||||
const readSSHRecord = () => {
|
||||
let list
|
||||
const readKey = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const keyDB = new KeyDB().getInstance()
|
||||
keyDB.findOne({}, (err, doc) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(doc)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const writeKey = async (keyObj = {}) => {
|
||||
const keyDB = new KeyDB().getInstance()
|
||||
return new Promise((resolve, reject) => {
|
||||
keyDB.update({}, { $set: keyObj }, { upsert: true }, (err, numReplaced) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
keyDB.compactDatafile()
|
||||
resolve(numReplaced)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const readSSHRecord = async () => {
|
||||
const sshRecordDB = new SshRecordDB().getInstance()
|
||||
return new Promise((resolve, reject) => {
|
||||
sshRecordDB.find({}, (err, docs) => {
|
||||
if (err) {
|
||||
consola.error('读取ssh-record-db错误: ', err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(docs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const writeSSHRecord = async (record = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sshRecordDB = new SshRecordDB().getInstance()
|
||||
sshRecordDB.remove({}, { multi: true }, (err, numRemoved) => {
|
||||
if (err) {
|
||||
consola.error('清空SSHRecord出错:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
sshRecordDB.insert(record, (err, newDocs) => {
|
||||
if (err) {
|
||||
consola.error('写入新的ssh记录出错:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
sshRecordDB.compactDatafile()
|
||||
resolve(newDocs)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const readHostList = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hostListDB = new HostListDB().getInstance()
|
||||
hostListDB.find({}, (err, docs) => {
|
||||
if (err) {
|
||||
consola.error('读取host-list-db错误:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(docs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const writeHostList = async (record = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hostListDB = new HostListDB().getInstance()
|
||||
hostListDB.remove({}, { multi: true }, (err, numRemoved) => {
|
||||
if (err) {
|
||||
consola.error('清空HostList出错:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
// 插入新的数据列表
|
||||
hostListDB.insert(record, (err, newDocs) => {
|
||||
if (err) {
|
||||
consola.error('写入新的HostList出错:', err)
|
||||
reject(err);
|
||||
} else {
|
||||
hostListDB.compactDatafile()
|
||||
resolve(newDocs)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const readEmailNotifyConf = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const emailNotifyDB = new EmailNotifyDB().getInstance()
|
||||
emailNotifyDB.findOne({}, (err, docs) => {
|
||||
if (err) {
|
||||
consola.error('读取email-notify-conf-db错误:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(docs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
const writeUserEmailList = (user) => {
|
||||
const emailNotifyDB = new EmailNotifyDB().getInstance()
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let support = await readSupportEmailList()
|
||||
const emailConf = { support, user }
|
||||
emailNotifyDB.update({}, { $set: emailConf }, { upsert: true }, (err, numReplaced) => {
|
||||
if (err) {
|
||||
reject({ code: -1, msg: err.message || err })
|
||||
} else {
|
||||
emailNotifyDB.compactDatafile()
|
||||
resolve({ code: 0 })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const readSupportEmailList = async () => {
|
||||
let support = []
|
||||
try {
|
||||
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8'))
|
||||
} catch (error) {
|
||||
consola.error('读取ssh-record错误, 即将重置ssh列表: ', error)
|
||||
writeSSHRecord([])
|
||||
}
|
||||
return list || []
|
||||
}
|
||||
|
||||
const writeSSHRecord = (record = []) => {
|
||||
fs.writeFileSync(sshRecordPath, JSON.stringify(record, null, 2))
|
||||
}
|
||||
|
||||
const readHostList = () => {
|
||||
let list
|
||||
try {
|
||||
list = JSON.parse(fs.readFileSync(hostListPath, 'utf8'))
|
||||
} catch (error) {
|
||||
consola.error('读取host-list错误, 即将重置host列表: ', error)
|
||||
writeHostList([])
|
||||
}
|
||||
return list || []
|
||||
}
|
||||
|
||||
const writeHostList = (record = []) => {
|
||||
fs.writeFileSync(hostListPath, JSON.stringify(record, null, 2))
|
||||
}
|
||||
|
||||
const readKey = () => {
|
||||
let keyObj = JSON.parse(fs.readFileSync(keyPath, 'utf8'))
|
||||
return keyObj
|
||||
}
|
||||
|
||||
const writeKey = (keyObj = {}) => {
|
||||
fs.writeFileSync(keyPath, JSON.stringify(keyObj, null, 2))
|
||||
}
|
||||
|
||||
const readEmailJson = () => {
|
||||
let emailJson = {}
|
||||
try {
|
||||
emailJson = JSON.parse(fs.readFileSync(emailPath, 'utf8'))
|
||||
} catch (error) {
|
||||
consola.error('读取email.json错误: ', error)
|
||||
}
|
||||
return emailJson
|
||||
}
|
||||
|
||||
const readSupportEmailList = () => {
|
||||
let supportEmailList = []
|
||||
try {
|
||||
supportEmailList = readEmailJson().support
|
||||
support = (await readEmailNotifyConf()).support
|
||||
} catch (error) {
|
||||
consola.error('读取email support错误: ', error)
|
||||
}
|
||||
return supportEmailList
|
||||
return support
|
||||
}
|
||||
|
||||
const readUserEmailList = () => {
|
||||
let configEmailList = []
|
||||
const readUserEmailList = async () => {
|
||||
let user = []
|
||||
try {
|
||||
configEmailList = readEmailJson().user
|
||||
user = (await readEmailNotifyConf()).user
|
||||
} catch (error) {
|
||||
consola.error('读取email config错误: ', error)
|
||||
}
|
||||
return configEmailList
|
||||
return user
|
||||
}
|
||||
|
||||
const writeUserEmailList = (user) => {
|
||||
let support = readSupportEmailList()
|
||||
const emailJson = { support, user }
|
||||
try {
|
||||
fs.writeFileSync(emailPath, JSON.stringify(emailJson, null, 2))
|
||||
return { code: 0 }
|
||||
} catch (error) {
|
||||
return { code: -1, msg: error.message || error }
|
||||
}
|
||||
}
|
||||
|
||||
const readNotifyList = () => {
|
||||
let notifyList = []
|
||||
const getNotifySwByType = async (type) => {
|
||||
if (!type) throw Error('missing params: type')
|
||||
try {
|
||||
notifyList = JSON.parse(fs.readFileSync(notifyPath, 'utf8'))
|
||||
} catch (error) {
|
||||
consola.error('读取notify list错误: ', error)
|
||||
}
|
||||
return notifyList
|
||||
}
|
||||
|
||||
const getNotifySwByType = (type) => {
|
||||
if(!type) throw Error('missing params: type')
|
||||
try {
|
||||
let { sw } = readNotifyList().find((item) => item.type === type)
|
||||
let notifyList = await readNotifyList()
|
||||
let { sw } = notifyList.find((item) => item.type === type)
|
||||
return sw
|
||||
} catch (error) {
|
||||
consola.error(`通知类型[${ type }]不存在`)
|
||||
consola.error(`通知类型[${type}]不存在`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const writeNotifyList = (notifyList) => {
|
||||
fs.writeFileSync(notifyPath, JSON.stringify(notifyList, null, 2))
|
||||
const readNotifyList = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const notifyDB = new NotifyDB().getInstance()
|
||||
notifyDB.find({}, (err, docs) => {
|
||||
if (err) {
|
||||
consola.error('读取notify list错误: ', err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(docs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const readGroupList = () => {
|
||||
let list
|
||||
try {
|
||||
list = JSON.parse(fs.readFileSync(groupPath, 'utf8'))
|
||||
} catch (error) {
|
||||
consola.error('读取group-list错误, 即将重置group列表: ', error)
|
||||
writeSSHRecord([])
|
||||
}
|
||||
return list || []
|
||||
const writeNotifyList = async (notifyList) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const notifyDB = new NotifyDB().getInstance()
|
||||
notifyDB.remove({}, { multi: true }, (err, numRemoved) => {
|
||||
if (err) {
|
||||
consola.error('清空notify list出错:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
notifyDB.insert(notifyList, (err, newDocs) => {
|
||||
if (err) {
|
||||
consola.error('写入新的notify list出错:', err);
|
||||
reject(err)
|
||||
} else {
|
||||
notifyDB.compactDatafile()
|
||||
resolve(newDocs);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const writeGroupList = (list = []) => {
|
||||
fs.writeFileSync(groupPath, JSON.stringify(list, null, 2))
|
||||
const readGroupList = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const groupDB = new GroupDB().getInstance()
|
||||
groupDB.find({}, (err, docs) => {
|
||||
if (err) {
|
||||
consola.error('读取group list错误: ', err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(docs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const writeGroupList = async (list = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const groupDB = new GroupDB().getInstance()
|
||||
groupDB.remove({}, { multi: true }, (err, numRemoved) => {
|
||||
if (err) {
|
||||
consola.error('清空group list出错:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
groupDB.insert(list, (err, newDocs) => {
|
||||
if (err) {
|
||||
consola.error('写入新的group list出错:', err)
|
||||
reject(err)
|
||||
} else {
|
||||
groupDB.compactDatafile()
|
||||
resolve(newDocs)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -128,12 +243,12 @@ module.exports = {
|
||||
writeHostList,
|
||||
readKey,
|
||||
writeKey,
|
||||
readSupportEmailList,
|
||||
readUserEmailList,
|
||||
writeUserEmailList,
|
||||
readNotifyList,
|
||||
getNotifySwByType,
|
||||
writeNotifyList,
|
||||
readGroupList,
|
||||
writeGroupList
|
||||
writeGroupList,
|
||||
readSupportEmailList,
|
||||
readUserEmailList,
|
||||
writeUserEmailList
|
||||
}
|
@ -1,10 +1,19 @@
|
||||
const fs = require('fs')
|
||||
const net = require('net')
|
||||
const axios = require('axios')
|
||||
const request = axios.create({ timeout: 3000 })
|
||||
|
||||
// 为空时请求本地IP
|
||||
const getNetIPInfo = async (searchIp = '') => {
|
||||
searchIp = searchIp.replace(/::ffff:/g, '') || '' // fix: nginx反代
|
||||
if(['::ffff:', '::1'].includes(searchIp)) searchIp = '127.0.0.1'
|
||||
console.log('searchIp:', searchIp)
|
||||
if (isLocalIP(searchIp)) {
|
||||
return {
|
||||
ip: searchIp,
|
||||
country: '本地',
|
||||
city: '局域网',
|
||||
error: null
|
||||
}
|
||||
}
|
||||
try {
|
||||
let date = Date.now()
|
||||
let ipUrls = [
|
||||
@ -63,7 +72,7 @@ const getNetIPInfo = async (searchIp = '') => {
|
||||
let { origip: ip, location: country, city = '', regionName = '' } = res || {}
|
||||
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
|
||||
}
|
||||
// console.log(searchResult)
|
||||
console.log(searchResult)
|
||||
let validInfo = searchResult.find(item => Boolean(item.country))
|
||||
consola.info('查询IP信息:', validInfo)
|
||||
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date }
|
||||
@ -78,6 +87,64 @@ const getNetIPInfo = async (searchIp = '') => {
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalIP(ip) {
|
||||
// Check if IPv4 or IPv6 address
|
||||
const isIPv4 = net.isIPv4(ip)
|
||||
const isIPv6 = net.isIPv6(ip)
|
||||
|
||||
// Local IPv4 ranges
|
||||
const localIPv4Ranges = [
|
||||
{ start: '10.0.0.0', end: '10.255.255.255' },
|
||||
{ start: '172.16.0.0', end: '172.31.255.255' },
|
||||
{ start: '192.168.0.0', end: '192.168.255.255' },
|
||||
{ start: '127.0.0.0', end: '127.255.255.255' } // Loopback
|
||||
]
|
||||
|
||||
// Local IPv6 ranges
|
||||
const localIPv6Ranges = [
|
||||
'::1', // Loopback
|
||||
'fc00::', // Unique local address
|
||||
'fd00::' // Unique local address
|
||||
];
|
||||
|
||||
function isInRange(ip, start, end) {
|
||||
const ipNum = ipToNumber(ip)
|
||||
return ipNum >= ipToNumber(start) && ipNum <= ipToNumber(end)
|
||||
}
|
||||
|
||||
function ipToNumber(ip) {
|
||||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0)
|
||||
}
|
||||
|
||||
if (isIPv4) {
|
||||
for (const range of localIPv4Ranges) {
|
||||
if (isInRange(ip, range.start, range.end)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isIPv6) {
|
||||
if (localIPv6Ranges.includes(ip)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle IPv4-mapped IPv6 addresses (e.g., ::ffff:192.168.1.1)
|
||||
if (ip.startsWith('::ffff:')) {
|
||||
const ipv4Part = ip.split('::ffff:')[1]
|
||||
if (ipv4Part && net.isIPv4(ipv4Part)) {
|
||||
for (const range of localIPv4Ranges) {
|
||||
if (isInRange(ipv4Part, range.start, range.end)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
|
||||
const err = new Error(msg)
|
||||
err.status = status // 主动抛错
|
||||
@ -134,11 +201,16 @@ const formatTimestamp = (timestamp = Date.now(), format = 'time') => {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePath(dir, path) {
|
||||
return path.resolve(dir, path)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNetIPInfo,
|
||||
throwError,
|
||||
isIP,
|
||||
randomStr,
|
||||
getUTCDate,
|
||||
formatTimestamp
|
||||
formatTimestamp,
|
||||
resolvePath
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
|
||||
const { AESDecrypt } = require('./encrypt')
|
||||
const { AESDecryptSync } = require('./encrypt')
|
||||
const { readKey } = require('./storage')
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
@ -10,21 +10,13 @@ const enumLoginCode = {
|
||||
}
|
||||
|
||||
// 校验token与登录IP
|
||||
const verifyAuth = (token, clientIp) =>{
|
||||
if(['::ffff:', '::1'].includes(clientIp)) clientIp = '127.0.0.1'
|
||||
token = AESDecrypt(token) // 先aes解密
|
||||
const { commonKey } = readKey()
|
||||
const verifyAuthSync = async (token, clientIp) => {
|
||||
consola.info('verifyAuthSync IP:', clientIp)
|
||||
token = await AESDecryptSync(token) // 先aes解密
|
||||
const { commonKey } = await readKey()
|
||||
try {
|
||||
const { exp } = jwt.verify(token, commonKey)
|
||||
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
||||
|
||||
let lastLoginIp = global.loginRecord[0] ? global.loginRecord[0].ip : ''
|
||||
consola.info('校验客户端IP:', clientIp)
|
||||
consola.info('最后登录的IP:', lastLoginIp)
|
||||
// 判断: (生产环境)clientIp与上次登录成功IP不一致
|
||||
if(isProd() && (!lastLoginIp || !clientIp || !clientIp.includes(lastLoginIp))) {
|
||||
return { code: enumLoginCode.EXPIRES, msg: '登录IP发生变化, 需重新登录' } // IP与上次登录访问的不一致
|
||||
}
|
||||
if (Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期
|
||||
return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功
|
||||
} catch (error) {
|
||||
return { code: enumLoginCode.ERROR_TOKEN, msg: error } // token错误, 验证失败
|
||||
@ -37,6 +29,6 @@ const isProd = () => {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyAuth,
|
||||
verifyAuthSync,
|
||||
isProd
|
||||
}
|
@ -26,6 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@seald-io/nedb": "^4.0.4",
|
||||
"axios": "^0.21.4",
|
||||
"consola": "^2.15.3",
|
||||
"cross-env": "^7.0.3",
|
||||
|
189
yarn.lock
189
yarn.lock
@ -93,6 +93,20 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@seald-io/binary-search-tree@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz#165a9a456eaa30d15885b25db83861bcce2c6a74"
|
||||
integrity sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA==
|
||||
|
||||
"@seald-io/nedb@^4.0.4":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@seald-io/nedb/-/nedb-4.0.4.tgz#a6f5dd63a2dde0e141f1862da1e0806141791732"
|
||||
integrity sha512-CUNcMio7QUHTA+sIJ/DC5JzVNNsHe743TPmC4H5Gij9zDLMbmrCT2li3eVB72/gF63BPS8pWEZrjlAMRKA8FDw==
|
||||
dependencies:
|
||||
"@seald-io/binary-search-tree" "^1.0.3"
|
||||
localforage "^1.9.0"
|
||||
util "^0.12.4"
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
@ -281,6 +295,13 @@ at-least-node@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
available-typed-arrays@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
|
||||
integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==
|
||||
dependencies:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^0.21.4:
|
||||
version "0.21.4"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
||||
@ -410,6 +431,17 @@ call-bind@^1.0.0:
|
||||
function-bind "^1.1.1"
|
||||
get-intrinsic "^1.0.2"
|
||||
|
||||
call-bind@^1.0.2, call-bind@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
|
||||
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
|
||||
dependencies:
|
||||
es-define-property "^1.0.0"
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
get-intrinsic "^1.2.4"
|
||||
set-function-length "^1.2.1"
|
||||
|
||||
callsites@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
@ -714,6 +746,15 @@ defer-to-connect@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
|
||||
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
|
||||
|
||||
define-data-property@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
|
||||
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
|
||||
dependencies:
|
||||
es-define-property "^1.0.0"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.0.1"
|
||||
|
||||
define-properties@^1.1.3:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
|
||||
@ -851,6 +892,18 @@ err-code@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
|
||||
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
|
||||
|
||||
es-define-property@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
|
||||
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
|
||||
dependencies:
|
||||
get-intrinsic "^1.2.4"
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
@ -1072,6 +1125,13 @@ follow-redirects@^1.14.0:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4"
|
||||
integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||
integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
|
||||
dependencies:
|
||||
is-callable "^1.1.3"
|
||||
|
||||
formidable@^1.1.1:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
|
||||
@ -1129,6 +1189,11 @@ function-bind@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
@ -1162,6 +1227,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
|
||||
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
has-proto "^1.0.1"
|
||||
has-symbols "^1.0.3"
|
||||
hasown "^2.0.0"
|
||||
|
||||
get-stream@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||
@ -1234,6 +1310,13 @@ globby@^11.0.4:
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
gopd@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
|
||||
integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
got@^9.6.0:
|
||||
version "9.6.0"
|
||||
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
|
||||
@ -1273,7 +1356,19 @@ has-property-descriptors@^1.0.0:
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.1"
|
||||
|
||||
has-symbols@^1.0.1, has-symbols@^1.0.2:
|
||||
has-property-descriptors@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
|
||||
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
|
||||
dependencies:
|
||||
es-define-property "^1.0.0"
|
||||
|
||||
has-proto@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
|
||||
integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
|
||||
|
||||
has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
|
||||
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
|
||||
@ -1285,6 +1380,13 @@ has-tostringtag@^1.0.0:
|
||||
dependencies:
|
||||
has-symbols "^1.0.2"
|
||||
|
||||
has-tostringtag@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
@ -1302,6 +1404,13 @@ has@^1.0.3:
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hasown@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
http-assert@^1.3.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f"
|
||||
@ -1382,6 +1491,11 @@ ignore@^5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
|
||||
|
||||
import-fresh@^3.0.0, import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
@ -1446,6 +1560,14 @@ into-stream@^6.0.0:
|
||||
from2 "^2.3.0"
|
||||
p-is-promise "^3.0.0"
|
||||
|
||||
is-arguments@^1.0.4:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
|
||||
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
@ -1453,6 +1575,11 @@ is-binary-path@~2.1.0:
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-callable@^1.1.3:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
|
||||
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
|
||||
|
||||
is-ci@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
|
||||
@ -1534,6 +1661,13 @@ is-path-inside@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
||||
|
||||
is-typed-array@^1.1.3:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229"
|
||||
integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==
|
||||
dependencies:
|
||||
which-typed-array "^1.1.14"
|
||||
|
||||
is-typedarray@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
@ -1801,6 +1935,20 @@ levn@~0.3.0:
|
||||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
lie@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
localforage@^1.9.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
|
||||
integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
|
||||
dependencies:
|
||||
lie "3.1.1"
|
||||
|
||||
lodash.includes@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
||||
@ -2268,6 +2416,11 @@ pkg@5.6:
|
||||
stream-meter "^1.0.4"
|
||||
tslib "2.3.1"
|
||||
|
||||
possible-typed-array-names@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
|
||||
integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==
|
||||
|
||||
prebuild-install@6.1.4:
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f"
|
||||
@ -2549,6 +2702,18 @@ set-blocking@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||
|
||||
set-function-length@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
|
||||
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
|
||||
dependencies:
|
||||
define-data-property "^1.1.4"
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
get-intrinsic "^1.2.4"
|
||||
gopd "^1.0.1"
|
||||
has-property-descriptors "^1.0.2"
|
||||
|
||||
setprototypeof@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
|
||||
@ -3002,6 +3167,17 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
util@^0.12.4:
|
||||
version "0.12.5"
|
||||
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
|
||||
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
is-arguments "^1.0.4"
|
||||
is-generator-function "^1.0.7"
|
||||
is-typed-array "^1.1.3"
|
||||
which-typed-array "^1.1.2"
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
@ -3025,6 +3201,17 @@ whatwg-url@^5.0.0:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which-typed-array@^1.1.14, which-typed-array@^1.1.2:
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
|
||||
integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==
|
||||
dependencies:
|
||||
available-typed-arrays "^1.0.7"
|
||||
call-bind "^1.0.7"
|
||||
for-each "^0.3.3"
|
||||
gopd "^1.0.1"
|
||||
has-tostringtag "^1.0.2"
|
||||
|
||||
which@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
|
Loading…
x
Reference in New Issue
Block a user