update db

This commit is contained in:
chaoszhu 2024-07-10 13:21:47 +08:00
parent 873e20cbcf
commit b8e08666a6
41 changed files with 990 additions and 426 deletions

6
.gitignore vendored
View File

@ -4,4 +4,8 @@ dist
easynode-server.zip easynode-server.zip
server/app/static/upload/* server/app/static/upload/*
server/app/socket/temp/* server/app/socket/temp/*
server/app/logs/* app/socket/.sftp-cache/*
server/app/logs/*
server/app/db/*
!server/app/db/README.md
plan.md

View File

@ -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) ## [1.2.1](https://github.com/chaos-zhu/easynode/releases) (2022-12-12)
### Features ### Features

18
Q&A.md
View File

@ -1,5 +1,23 @@
# Q&A # 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启动服务失败 ## CentOS7/8启动服务失败
> 先关闭SELinux > 先关闭SELinux

View File

@ -19,6 +19,7 @@
- [客户端](#客户端) - [客户端](#客户端)
- [版本日志](#版本日志) - [版本日志](#版本日志)
- [安全与说明](#安全与说明) - [安全与说明](#安全与说明)
- [开发](#开发)
- [Q&A](#qa) - [Q&A](#qa)
- [感谢Star](#感谢star) - [感谢Star](#感谢star)
- [License](#license) - [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使用` > 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&A](./Q%26A.md) - [Q&A](./Q%26A.md)

View File

@ -1,7 +1,7 @@
{ {
"name": "easynode", "name": "easynode",
"version": "1.0.0", "version": "1.0.0",
"description": "easy to manage the server", "description": "web ssh",
"private": true, "private": true,
"workspaces": ["server", "client"], "workspaces": ["server", "client"],
"repository": { "repository": {

7
server/.gitignore vendored
View File

@ -1,7 +0,0 @@
node_modules
app/static/upload/*
app/socket/temp/*
app/socket/.sftp-cache/*
app/logs/*
!.gitkeep
dist

View File

@ -2,19 +2,19 @@ const path = require('path')
module.exports = { module.exports = {
httpPort: 8082, httpPort: 8082,
clientPort: 22022, // 更改 clientPort: 22022, // 暂不支持更改
uploadDir: path.join(process.cwd(),'app/static/upload'), uploadDir: path.join(process.cwd(),'app/static/upload'),
staticDir: path.join(process.cwd(),'app/static'), staticDir: path.join(process.cwd(),'app/static'),
sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'), sftpCacheDir: path.join(process.cwd(),'app/socket/sftp-cache'),
sshRecordPath: path.join(process.cwd(),'app/storage/ssh-record.json'), sshRecordDBPath: path.join(process.cwd(),'app/db/ssh-record.db'),
keyPath: path.join(process.cwd(),'app/storage/key.json'), keyDBPath: path.join(process.cwd(),'app/db/key.db'),
hostListPath: path.join(process.cwd(),'app/storage/host-list.json'), hostListDBPath: path.join(process.cwd(),'app/db/host-list.db'),
emailPath: path.join(process.cwd(),'app/storage/email.json'), notifyConfDBPath: path.join(process.cwd(),'app/db/notify.db'),
notifyPath: path.join(process.cwd(),'app/storage/notify.json'), groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
groupPath: path.join(process.cwd(),'app/storage/group.json'), emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'),
apiPrefix: '/api/v1', apiPrefix: '/api/v1',
logConfig: { logConfig: {
outDir: path.join(process.cwd(),'./app/logs'), outDir: path.join(process.cwd(),'./app/logs'),
recordLog: false // 是否记录日志 recordLog: true // 是否记录日志
} }
} }

View File

@ -1,52 +1,51 @@
const { readGroupList, writeGroupList, readHostList, writeHostList,randomStr } = require('../utils') const { readGroupList, writeGroupList, readHostList, writeHostList, randomStr } = require('../utils')
function getGroupList({ res }) { async function getGroupList({ res }) {
const data = readGroupList() const data = await readGroupList()
data?.sort((a, b) => a.index - b.index)
res.success({ data }) res.success({ data })
} }
const addGroupList = async ({ res, request }) => { const addGroupList = async ({ res, request }) => {
let { body: { name, index } } = request let { body: { name, index } } = request
if(!name) return res.fail({ data: false, msg: '参数错误' }) if (!name) return res.fail({ data: false, msg: '参数错误' })
let groupList = readGroupList() let groupList = await readGroupList()
let group = { id: randomStr(), name, index } let group = { id: randomStr(), name, index }
groupList.push(group) groupList.push(group)
groupList.sort((a, b) => a.index - b.index) await writeGroupList(groupList)
writeGroupList(groupList)
res.success({ data: '新增成功' }) res.success({ data: '新增成功' })
} }
const updateGroupList = async ({ res, request }) => { const updateGroupList = async ({ res, request }) => {
let { params: { id } } = request let { params: { id } } = request
let { body: { name, index } } = request let { body: { name, index } } = request
if(!id || !name) return res.fail({ data: false, msg: '参数错误' }) if (!id || !name) return res.fail({ data: false, msg: '参数错误' })
let groupList = readGroupList() let groupList = await readGroupList()
let idx = groupList.findIndex(item => item.id === id) let idx = groupList.findIndex(item => item.id === id)
let group = { id, name, index } let group = { id, name, index: Number(index) || 0 }
if(idx === -1) return res.fail({ data: false, msg: `分组ID${ id }不存在` }) if (idx === -1) return res.fail({ data: false, msg: `分组ID${id}不存在` })
groupList.splice(idx, 1, group) groupList.splice(idx, 1, group)
groupList.sort((a, b) => a.index - b.index) await writeGroupList(groupList)
writeGroupList(groupList)
res.success({ data: '修改成功' }) res.success({ data: '修改成功' })
} }
const removeGroup = async ({ res, request }) => { const removeGroup = async ({ res, request }) => {
let { params: { id } } = request let { params: { id } } = request
if(id ==='default') return res.fail({ data: false, msg: '保留分组, 禁止删除' }) if (id === 'default') return res.fail({ data: false, msg: '保留分组, 禁止删除' })
let groupList = readGroupList() let groupList = await readGroupList()
let idx = groupList.findIndex(item => item.id === id) let idx = groupList.findIndex(item => item.id === id)
if(idx === -1) return res.fail({ msg: '分组不存在' }) if (idx === -1) return res.fail({ msg: '分组不存在' })
// 移除分组将所有该分组下host分配到default中去 // 移除分组将所有该分组下host分配到default中去
let hostList = readHostList() let hostList = await readHostList()
hostList = hostList.map((item) => { hostList = hostList?.map((item) => {
if(item.group === groupList[idx].id) item.group = 'default' if (item.group === groupList[idx].id) item.group = 'default'
return item return item
}) })
writeHostList(hostList) await writeHostList(hostList)
groupList.splice(idx, 1) groupList.splice(idx, 1)
writeGroupList(groupList) await writeGroupList(groupList)
res.success({ data: '移除成功' }) res.success({ data: '移除成功' })
} }

View File

@ -1,58 +1,61 @@
const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils') const { readHostList, writeHostList, readSSHRecord, writeSSHRecord } = require('../utils')
function getHostList({ res }) { async function getHostList({ res }) {
const data = readHostList() console.log('get - host - list')
const data = await readHostList()
res.success({ data }) res.success({ data })
} }
function saveHost({ res, request }) { async function saveHost({ res, request }) {
let { body: { host: newHost, name, expired, expiredNotify, group, consoleUrl, remark } } = request let { body: { host: newHost, name, expired, expiredNotify, group, consoleUrl, remark } } = request
if(!newHost || !name) return res.fail({ msg: 'missing params: name or host' }) console.log(request)
let hostList = readHostList() if (!newHost || !name) return res.fail({ msg: 'missing params: name or host' })
if(hostList.some(({ host }) => host === newHost)) return res.fail({ msg: `主机${ newHost }已存在` }) 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 }) hostList.push({ host: newHost, name, expired, expiredNotify, group, consoleUrl, remark })
writeHostList(hostList) await writeHostList(hostList)
res.success() res.success()
} }
function updateHost({ res, request }) { async function updateHost({ res, request }) {
let { body: { host: newHost, name: newName, oldHost, expired, expiredNotify, group, consoleUrl, remark } } = request let { body: { host: newHost, name: newName, oldHost, expired, expiredNotify, group, consoleUrl, remark } } = request
if(!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' }) if (!newHost || !newName || !oldHost) return res.fail({ msg: '参数错误' })
let hostList = readHostList() let hostList = await readHostList()
if(!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${ newHost }不存在` }) if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `主机${newHost}不存在` })
let targetIdx = hostList.findIndex(({ host }) => host === oldHost) let targetIdx = hostList.findIndex(({ host }) => host === oldHost)
hostList.splice(targetIdx, 1, { name: newName, host: newHost, expired, expiredNotify, group, consoleUrl, remark }) hostList.splice(targetIdx, 1, { name: newName, host: newHost, expired, expiredNotify, group, consoleUrl, remark })
writeHostList(hostList) writeHostList(hostList)
res.success() res.success()
} }
function removeHost({ res, request }) { async function removeHost({ res, request }) {
let { body: { host } } = request let { body: { host } } = request
let hostList = readHostList() let hostList = await readHostList()
let hostIdx = hostList.findIndex(item => item.host === host) 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) hostList.splice(hostIdx, 1)
writeHostList(hostList) writeHostList(hostList)
// 查询是否存在ssh记录 // 查询是否存在ssh记录
let sshRecord = readSSHRecord() let sshRecord = await readSSHRecord()
let sshIdx = sshRecord.findIndex(item => item.host === host) let sshIdx = sshRecord.findIndex(item => item.host === host)
let flag = sshIdx !== -1 let flag = sshIdx !== -1
if(flag) sshRecord.splice(sshIdx, 1) if (flag) sshRecord.splice(sshIdx, 1)
writeSSHRecord(sshRecord) 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 let { body: { list } } = request
if(!list) return res.fail({ msg: '参数错误' }) if (!list) return res.fail({ msg: '参数错误' })
let hostList = readHostList() let hostList = await readHostList()
if(hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' }) if (hostList.length !== list.length) return res.fail({ msg: '失败: host数量不匹配' })
let sortResult = [] let sortResult = []
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
const curHost = list[i] const curHost = list[i]
let temp = hostList.find(({ host }) => curHost.host === host) 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) sortResult.push(temp)
} }
writeHostList(sortResult) writeHostList(sortResult)

View File

@ -7,14 +7,14 @@ const {
writeNotifyList } = require('../utils') writeNotifyList } = require('../utils')
const commonTemp = require('../template/commonTemp') const commonTemp = require('../template/commonTemp')
function getSupportEmailList({ res }) { async function getSupportEmailList({ res }) {
const data = readSupportEmailList() const data = await readSupportEmailList()
res.success({ data }) res.success({ data })
} }
function getUserEmailList({ res }) { async function getUserEmailList({ res }) {
const userEmailList = readUserEmailList().map(({ target, auth: { user } }) => ({ target, user })) const userEmailList = (await readUserEmailList()).map(({ target, auth: { user } }) => ({ target, user }))
const supportEmailList = readSupportEmailList() const supportEmailList = await readSupportEmailList()
const data = userEmailList.map(({ target: userTarget, user: email }) => { const data = userEmailList.map(({ target: userTarget, user: email }) => {
let name = supportEmailList.find(({ target: supportTarget }) => supportTarget === userTarget).name let name = supportEmailList.find(({ target: supportTarget }) => supportTarget === userTarget).name
return { name, email } return { name, email }
@ -24,57 +24,57 @@ function getUserEmailList({ res }) {
async function pushEmail({ res, request }) { async function pushEmail({ res, request }) {
let { body: { toEmail, isTest } } = 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) consola.info('发送测试邮件:', toEmail)
let { code, msg } = await emailTransporter({ toEmail, title: '测试邮件', html: commonTemp('邮件通知测试邮件') }) let { code, msg } = await emailTransporter({ toEmail, title: '测试邮件', html: commonTemp('邮件通知测试邮件') })
msg = msg && msg.message || msg msg = msg && msg.message || msg
if(code === 0) return res.success({ msg }) if (code === 0) return res.success({ msg })
return res.fail({ msg }) return res.fail({ msg })
} }
function updateUserEmailList({ res, request }) { async function updateUserEmailList({ res, request }) {
let { body: { target, auth } } = request let { body: { target, auth } } = request
const supportList = readSupportEmailList() const supportList = await readSupportEmailList()
let flag = supportList.some((item) => item.target === target) let flag = supportList.some((item) => item.target === target)
if(!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` }) if (!flag) return res.fail({ msg: `不支持的邮箱类型:${ target }` })
if(!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' }) if (!auth.user || !auth.pass) return res.fail({ msg: 'missing params: auth.' })
let newUserEmail = { target, auth } let newUserEmail = { target, auth }
let userEmailList = readUserEmailList() let userEmailList = await readUserEmailList()
let idx = userEmailList.findIndex(({ auth: { user } }) => auth.user === user) 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) else userEmailList.unshift(newUserEmail)
const { code, msg } = writeUserEmailList(userEmailList) const { code, msg } = await writeUserEmailList(userEmailList)
if(code === 0) return res.success() if (code === 0) return res.success()
return res.fail({ msg }) return res.fail({ msg })
} }
function removeUserEmail({ res, request }) { async function removeUserEmail({ res, request }) {
let { params: { email } } = request let { params: { email } } = request
const userEmailList = readUserEmailList() const userEmailList = await readUserEmailList()
let idx = userEmailList.findIndex(({ auth: { user } }) => user === email) 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) userEmailList.splice(idx, 1)
const { code, msg } = writeUserEmailList(userEmailList) const { code, msg } = await writeUserEmailList(userEmailList)
if(code === 0) return res.success() if (code === 0) return res.success()
return res.fail({ msg }) return res.fail({ msg })
} }
function getNotifyList({ res }) { async function getNotifyList({ res }) {
const data = readNotifyList() const data = await readNotifyList()
res.success({ data }) res.success({ data })
} }
function updateNotifyList({ res, request }) { async function updateNotifyList({ res, request }) {
let { body: { type, sw } } = request let { body: { type, sw } } = request
if(!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw${ sw }, must be Boolean` }) if (!([true, false].includes(sw))) return res.fail({ msg: `Error type for sw${ sw }, must be Boolean` })
const notifyList = readNotifyList() const notifyList = await readNotifyList()
let target = notifyList.find((item) => item.type === type) 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 target.sw = sw
console.log(notifyList) // console.log(notifyList)
writeNotifyList(notifyList) await writeNotifyList(notifyList)
res.success() res.success()
} }

View File

@ -1,47 +1,47 @@
const { readSSHRecord, writeSSHRecord, AESEncrypt } = require('../utils') const { readSSHRecord, writeSSHRecord, AESEncryptSync } = require('../utils')
const updateSSH = async ({ res, request }) => { const updateSSH = async ({ res, request }) => {
let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request let { body: { host, port, username, type, password, privateKey, randomKey, command } } = request
let record = { host, port, username, type, password, privateKey, randomKey, command } let record = { host, port, username, type, password, privateKey, randomKey, command }
if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' }) if(!host || !port || !username || !type || !randomKey) return res.fail({ data: false, msg: '参数错误' })
// 再做一次对称加密(方便ssh连接时解密) // 再做一次对称加密(方便ssh连接时解密)
record.randomKey = AESEncrypt(randomKey) record.randomKey = await AESEncryptSync(randomKey)
let sshRecord = readSSHRecord() let sshRecord = await readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host) let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) if(idx === -1)
sshRecord.push(record) sshRecord.push(record)
else else
sshRecord.splice(idx, 1, record) sshRecord.splice(idx, 1, record)
writeSSHRecord(sshRecord) await writeSSHRecord(sshRecord)
consola.info('新增凭证:', host) consola.info('新增凭证:', host)
res.success({ data: '保存成功' }) res.success({ data: '保存成功' })
} }
const removeSSH = async ({ res, request }) => { const removeSSH = async ({ res, request }) => {
let { body: { host } } = request let { body: { host } } = request
let sshRecord = readSSHRecord() let sshRecord = await readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host) let idx = sshRecord.findIndex(item => item.host === host)
if(idx === -1) return res.fail({ msg: '凭证不存在' }) if(idx === -1) return res.fail({ msg: '凭证不存在' })
sshRecord.splice(idx, 1) sshRecord.splice(idx, 1)
consola.info('移除凭证:', host) consola.info('移除凭证:', host)
writeSSHRecord(sshRecord) await writeSSHRecord(sshRecord)
res.success({ data: '移除成功' }) res.success({ data: '移除成功' })
} }
const existSSH = async ({ res, request }) => { const existSSH = async ({ res, request }) => {
let { body: { host } } = request let { body: { host } } = request
let sshRecord = readSSHRecord() let sshRecord = await readSSHRecord()
let idx = sshRecord.findIndex(item => item.host === host) let isExist = sshRecord?.some(item => item.host === host)
consola.info('查询凭证:', 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 }) // 存在 res.success({ data: true }) // 存在
} }
const getCommand = async ({ res, request }) => { const getCommand = async ({ res, request }) => {
let { host } = request.query let { host } = request.query
if(!host) return res.fail({ data: false, msg: '参数错误' }) if(!host) return res.fail({ data: false, msg: '参数错误' })
let sshRecord = readSSHRecord() let sshRecord = await readSSHRecord()
let record = sshRecord.find(item => item.host === host) let record = sshRecord?.find(item => item.host === host)
consola.info('查询登录后执行的指令:', host) consola.info('查询登录后执行的指令:', host)
if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在 if(!record) return res.fail({ data: false, msg: 'host not found' }) // host不存在
const { command } = record const { command } = record

View File

@ -1,8 +1,8 @@
const jwt = require('jsonwebtoken') 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 }) => { const getpublicKey = async ({ res }) => {
let { publicKey: data } = readKey() 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 }) res.success({ data })
} }
@ -50,9 +50,9 @@ const login = async ({ res, request }) => {
// 登录流程 // 登录流程
try { try {
// console.log('ciphertext', ciphertext) // console.log('ciphertext', ciphertext)
let password = RSADecrypt(ciphertext) let password = await RSADecryptSync(ciphertext)
// console.log('Decrypt解密password:', password) // console.log('Decrypt解密password:', password)
let { pwd } = readKey() let { pwd } = await readKey()
if(password === 'admin' && pwd === 'admin') { if(password === 'admin' && pwd === 'admin') {
const token = await beforeLoginHandler(clientIp, jwtExpires) const token = await beforeLoginHandler(clientIp, jwtExpires)
return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' }) return res.success({ data: { token, jwtExpires }, msg: '登录成功,请及时修改默认密码' })
@ -72,13 +72,14 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => {
// consola.success('登录成功, 准备生成token', new Date()) // consola.success('登录成功, 准备生成token', new Date())
// 生产token // 生产token
let { commonKey } = readKey() let { commonKey } = await readKey()
let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token let token = jwt.sign({ date: Date.now() }, commonKey, { expiresIn: jwtExpires }) // 生成token
token = AESEncrypt(token) // 对称加密token后再传输给前端 token = await AESEncryptSync(token) // 对称加密token后再传输给前端
// 记录客户端登录IP(用于判断是否异地且只保留最近10条) // 记录客户端登录IP(用于判断是否异地且只保留最近10条)
const clientIPInfo = await getNetIPInfo(clientIp) const clientIPInfo = await getNetIPInfo(clientIp)
const { ip, country, city } = clientIPInfo || {} const { ip, country, city } = clientIPInfo || {}
consola.info('登录成功:', new Date(), { ip, country, city })
// 邮件登录通知 // 邮件登录通知
let sw = getNotifySwByType('login') let sw = getNotifySwByType('login')
@ -91,14 +92,14 @@ const beforeLoginHandler = async (clientIp, jwtExpires) => {
const updatePwd = async ({ res, request }) => { const updatePwd = async ({ res, request }) => {
let { body: { oldPwd, newPwd } } = request let { body: { oldPwd, newPwd } } = request
let rsaOldPwd = RSADecrypt(oldPwd) let rsaOldPwd = await RSADecryptSync(oldPwd)
oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd) oldPwd = rsaOldPwd === 'admin' ? 'admin' : SHA1Encrypt(rsaOldPwd)
let keyObj = readKey() let keyObj = await readKey()
if(oldPwd !== keyObj.pwd) return res.fail({ data: false, msg: '旧密码校验失败' }) 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 keyObj.pwd = newPwd
writeKey(keyObj) await writeKey(keyObj)
let sw = getNotifySwByType('updatePwd') let sw = getNotifySwByType('updatePwd')
if(sw) sendEmailToConfList('密码修改提醒', '面板登录密码已更改') if(sw) sendEmailToConfList('密码修改提醒', '面板登录密码已更改')

154
server/app/db.js Normal file
View 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()
}

View File

@ -1,38 +1,40 @@
# host-list.json db目录初始化后自动生成
> 存储服务器基本信息 **host-list.db**
# key.json > 存储服务器基本信息
> 用于加密的密钥相关 **key.db**
# ssh-record.json > 用于加密的密钥相关
> ssh密钥记录(加密存储) **ssh-record.db**
# email.json > ssh密钥记录(加密存储)
> 邮件配置 **email.db**
- port: 587 --> secure: false > 邮件配置
```json
// Gmail调试不通过, 暂缓 - port: 587 --> secure: false
{ ```db
"name": "Google邮箱", // Gmail调试不通过, 暂缓
"target": "google", {
"host": "smtp.gmail.com", "name": "Google邮箱",
"port": 465, "target": "google",
"secure": true, "host": "smtp.gmail.com",
"tls": { "port": 465,
"rejectUnauthorized": false "secure": true,
} "tls": {
} "rejectUnauthorized": false
``` }
}
# notify.json ```
> 通知配置 **notify.db**
# group.json > 通知配置
> 服务器分组配置 **group.db**
> 服务器分组配置

View File

@ -1,5 +1,5 @@
const NodeRSA = require('node-rsa') 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() const isDev = !isProd()
@ -7,7 +7,7 @@ const isDev = !isProd()
async function initLocalIp() { async function initLocalIp() {
if(isDev) return consola.info('非生产环境不初始化保存本地IP') if(isDev) return consola.info('非生产环境不初始化保存本地IP')
const localNetIPInfo = await getNetIPInfo() const localNetIPInfo = await getNetIPInfo()
let vpsList = readHostList() let vpsList = await readHostList()
let { ip: localNetIP } = localNetIPInfo let { ip: localNetIP } = localNetIPInfo
if(vpsList.some(({ host }) => host === localNetIP)) return consola.info('本机IP已储存: ', localNetIP) if(vpsList.some(({ host }) => host === localNetIP)) return consola.info('本机IP已储存: ', localNetIP)
vpsList.unshift({ name: 'server-side-host', host: localNetIP, group: 'default' }) vpsList.unshift({ name: 'server-side-host', host: localNetIP, group: 'default' })
@ -17,33 +17,32 @@ async function initLocalIp() {
// 初始化公私钥, 供登录、保存ssh密钥/密码等加解密 // 初始化公私钥, 供登录、保存ssh密钥/密码等加解密
async function initRsa() { async function initRsa() {
let keyObj = readKey() let keyObj = await readKey()
if(keyObj.privateKey && keyObj.publicKey) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]') if(keyObj.privateKey && keyObj.publicKey) return consola.info('公私钥已存在[重新生成会导致已保存的ssh密钥信息失效]')
let key = new NodeRSA({ b: 1024 }) let key = new NodeRSA({ b: 1024 })
key.setOptions({ encryptionScheme: 'pkcs1' }) key.setOptions({ encryptionScheme: 'pkcs1' })
let privateKey = key.exportKey('pkcs1-private-pem') let privateKey = key.exportKey('pkcs1-private-pem')
let publicKey = key.exportKey('pkcs8-public-pem') let publicKey = key.exportKey('pkcs8-public-pem')
keyObj.privateKey = AESEncrypt(privateKey) // 加密私钥 keyObj.privateKey = await AESEncryptSync(privateKey) // 加密私钥
keyObj.publicKey = publicKey // 公开公钥 keyObj.publicKey = publicKey // 公开公钥
writeKey(keyObj) await writeKey(keyObj)
consola.info('Task: 已生成新的非对称加密公私钥') consola.info('Task: 已生成新的非对称加密公私钥')
} }
// 随机的commonKey secret // 随机的commonKey secret
function randomJWTSecret() { async function randomJWTSecret() {
let keyObj = readKey() let keyObj = await readKey()
if(keyObj.commonKey) return consola.info('commonKey密钥已存在') if(keyObj?.commonKey) return consola.info('commonKey密钥已存在')
keyObj.commonKey = randomStr(16) keyObj.commonKey = randomStr(16)
writeKey(keyObj) await writeKey(keyObj)
consola.info('Task: 已生成新的随机commonKey密钥') consola.info('Task: 已生成新的随机commonKey密钥')
} }
module.exports = () => { module.exports = async () => {
randomJWTSecret() // 先生成全局唯一密钥 await randomJWTSecret() // 全局密钥
initLocalIp() await initRsa() // 全局公钥密钥
initRsa() // initLocalIp() // :TODO: 默认添加服务端vps
// 用于记录客户端登录IP的列表 // 用于记录客户端登录IP的列表
global.loginRecord = [] global.loginRecord = []
} }

View File

@ -1,13 +1,16 @@
const consola = require('consola') const consola = require('consola')
global.consola = consola global.consola = consola
const { httpServer, clientHttpServer } = require('./server') const { httpServer, clientHttpServer } = require('./server')
const initLocal = require('./init') const initDB = require('./db')
const scheduleJob = require('./schedule') const initEncryptConf = require('./init')
// const scheduleJob = require('./schedule')
scheduleJob() async function main() {
await initDB()
await initEncryptConf()
httpServer()
clientHttpServer()
// scheduleJob()
}
initLocal() main()
httpServer()
clientHttpServer()

View File

@ -1,4 +1,4 @@
const { verifyAuth } = require('../utils') const { verifyAuthSync } = require('../utils')
const { apiPrefix } = require('../config') const { apiPrefix } = require('../config')
let whitePath = [ let whitePath = [
@ -13,7 +13,7 @@ const useAuth = async ({ request, res }, next) => {
if(whitePath.includes(path)) return next() if(whitePath.includes(path)) return next()
if(!token) return res.fail({ msg: '未登录', status: 403 }) if(!token) return res.fail({ msg: '未登录', status: 403 })
// 验证token // 验证token
const { code, msg } = verifyAuth(token, request.ip) const { code, msg } = await verifyAuthSync(token, request.ip)
switch(code) { switch(code) {
case 1: case 1:
return await next() return await next()

View File

@ -1,3 +1,4 @@
// 响应压缩模块,自适应头部压缩方式
const compress = require('koa-compress') const compress = require('koa-compress')
const options = { threshold: 2048 } const options = { threshold: 2048 }

View File

@ -88,13 +88,13 @@ const notify = [
}, },
{ {
method: 'post', method: 'post',
path: '/push-email', path: '/user-email',
controller: pushEmail controller: updateUserEmailList
}, },
{ {
method: 'post', method: 'post',
path: '/user-email', path: '/push-email',
controller: updateUserEmailList controller: pushEmail
}, },
{ {
method: 'delete', method: 'delete',

View File

@ -3,7 +3,7 @@ const { readHostList, sendEmailToConfList, formatTimestamp } = require('../utils
const expiredNotifyJob = () => { const expiredNotifyJob = () => {
consola.info('=====开始检测服务器到期时间=====', new Date()) consola.info('=====开始检测服务器到期时间=====', new Date())
const hostList = readHostList() const hostList = await readHostList()
for (const item of hostList) { for (const item of hostList) {
if(!item.expiredNotify) continue if(!item.expiredNotify) continue
const { host, name, expired, consoleUrl } = item const { host, name, expired, consoleUrl } = item

View File

@ -4,11 +4,12 @@ const { readHostList, sendEmailToConfList, getNotifySwByType, formatTimestamp, i
const testConnectAsync = require('../utils/test-connect') const testConnectAsync = require('../utils/test-connect')
let sendNotifyRecord = new Map() let sendNotifyRecord = new Map()
const offlineJob = () => { const offlineJob = async () => {
let sw = getNotifySwByType('host_offline') let sw = getNotifySwByType('host_offline')
if(!sw) return if(!sw) return
consola.info('=====开始检测服务器状态=====', new Date()) consola.info('=====开始检测服务器状态=====', new Date())
for (const item of readHostList()) { const hostList = await readHostList()
for (const item of hostList) {
const { host, name } = item const { host, name } = item
// consola.info('start inpect:', host, name ) // consola.info('start inpect:', host, name )
testConnectAsync({ testConnectAsync({

View File

@ -2,15 +2,15 @@ const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client') const { io: ClientIO } = require('socket.io-client')
const { readHostList } = require('../utils') const { readHostList } = require('../utils')
const { clientPort } = require('../config') const { clientPort } = require('../config')
const { verifyAuth } = require('../utils') const { verifyAuthSync } = require('../utils')
let clientSockets = {}, clientsData = {} let clientSockets = {}, clientsData = {}
function getClientsInfo(socketId) { async function getClientsInfo(socketId) {
let hostList = readHostList() let hostList = await readHostList()
hostList hostList
.map(({ host, name }) => { ?.map(({ host, name }) => {
let clientSocket = ClientIO(`http://${ host }:${ clientPort }`, { let clientSocket = ClientIO(`http://${host}:${clientPort}`, {
path: '/client/os-info', path: '/client/os-info',
forceNew: true, forceNew: true,
timeout: 5000, timeout: 5000,
@ -58,10 +58,10 @@ module.exports = (httpServer) => {
serverIo.on('connection', (socket) => { serverIo.on('connection', (socket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务 // 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address 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) const { code, msg } = await verifyAuthSync(token, clientIp)
if(code !== 1) { if (code !== 1) {
socket.emit('token_verify_fail', msg || '鉴权失败') socket.emit('token_verify_fail', msg || '鉴权失败')
socket.disconnect() socket.disconnect()
return return
@ -86,7 +86,7 @@ module.exports = (httpServer) => {
// 关闭连接 // 关闭连接
socket.on('disconnect', () => { socket.on('disconnect', () => {
// 防止内存泄漏 // 防止内存泄漏
if(timer) clearInterval(timer) if (timer) clearInterval(timer)
// 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接 // 当web端与服务端断开连接时, 服务端与每个客户端的socket也应该断开连接
clientSockets[socket.id].forEach(socket => socket.close && socket.close()) clientSockets[socket.id].forEach(socket => socket.close && socket.close())
delete clientSockets[socket.id] delete clientSockets[socket.id]

View File

@ -1,7 +1,7 @@
const { Server: ServerIO } = require('socket.io') const { Server: ServerIO } = require('socket.io')
const { io: ClientIO } = require('socket.io-client') const { io: ClientIO } = require('socket.io-client')
const { clientPort } = require('../config') const { clientPort } = require('../config')
const { verifyAuth } = require('../utils') const { verifyAuthSync } = require('../utils')
let hostSockets = {} let hostSockets = {}
@ -47,9 +47,9 @@ module.exports = (httpServer) => {
serverIo.on('connection', (serverSocket) => { serverIo.on('connection', (serverSocket) => {
// 前者兼容nginx反代, 后者兼容nodejs自身服务 // 前者兼容nginx反代, 后者兼容nodejs自身服务
let clientIp = serverSocket.handshake.headers['x-forwarded-for'] || serverSocket.handshake.address 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) { if(code !== 1) {
serverSocket.emit('token_verify_fail', msg || '鉴权失败') serverSocket.emit('token_verify_fail', msg || '鉴权失败')
serverSocket.disconnect() serverSocket.disconnect()

View File

@ -60,7 +60,7 @@ module.exports = (httpServer) => {
}, 1000) }, 1000)
socket.on('disconnect', () => { socket.on('disconnect', () => {
// 断开时清对应的websocket连接 // 断开时清对应的websocket连接
if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id]) if(serverSockets[socket.id]) clearInterval(serverSockets[socket.id])
delete serverSockets[socket.id] delete serverSockets[socket.id]
socket.close && socket.close() socket.close && socket.close()

View File

@ -2,7 +2,7 @@ const { Server } = require('socket.io')
const SFTPClient = require('ssh2-sftp-client') const SFTPClient = require('ssh2-sftp-client')
const rawPath = require('path') const rawPath = require('path')
const fs = require('fs') const fs = require('fs')
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils') const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
const { sftpCacheDir } = require('../config') const { sftpCacheDir } = require('../config')
const CryptoJS = require('crypto-js') const CryptoJS = require('crypto-js')
@ -205,21 +205,21 @@ module.exports = (httpServer) => {
let sftpClient = new SFTPClient() let sftpClient = new SFTPClient()
consola.success('terminal websocket 已连接') consola.success('terminal websocket 已连接')
socket.on('create', ({ host: ip, token }) => { socket.on('create', async ({ host: ip, token }) => {
const { code } = verifyAuth(token, clientIp) const { code } = await verifyAuthSync(token, clientIp)
if(code !== 1) { if(code !== 1) {
socket.emit('token_verify_fail') socket.emit('token_verify_fail')
socket.disconnect() socket.disconnect()
return return
} }
const sshRecord = readSSHRecord() const sshRecord = await readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip) 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 let { type, host, port, username, randomKey } = loginInfo
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】 // 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
randomKey = AESDecrypt(randomKey) // 先对称解密key randomKey = await AESDecryptSync(randomKey) // 先对称解密key
randomKey = RSADecrypt(randomKey) // 再非对称解密key randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥 loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
consola.info('准备连接Sftp', host) consola.info('准备连接Sftp', host)
const authInfo = { host, port, username, [type]: loginInfo[type] } const authInfo = { host, port, username, [type]: loginInfo[type] }
sftpClient.connect(authInfo) sftpClient.connect(authInfo)

View File

@ -1,6 +1,6 @@
const { Server } = require('socket.io') const { Server } = require('socket.io')
const { Client: SSHClient } = require('ssh2') const { Client: SSHClient } = require('ssh2')
const { readSSHRecord, verifyAuth, RSADecrypt, AESDecrypt } = require('../utils') const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
function createTerminal(socket, sshClient) { function createTerminal(socket, sshClient) {
sshClient.shell({ term: 'xterm-color' }, (err, stream) => { sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
@ -16,7 +16,7 @@ function createTerminal(socket, sshClient) {
}) })
// web端输入 // web端输入
socket.on('input', key => { socket.on('input', key => {
if(sshClient._sock.writable === false) return consola.info('终端连接已关闭') if (sshClient._sock.writable === false) return consola.info('终端连接已关闭')
stream.write(key) stream.write(key)
}) })
socket.emit('connect_terminal') // 已连接终端web端可以执行指令了 socket.emit('connect_terminal') // 已连接终端web端可以执行指令了
@ -42,35 +42,40 @@ module.exports = (httpServer) => {
let sshClient = new SSHClient() let sshClient = new SSHClient()
consola.success('terminal websocket 已连接') consola.success('terminal websocket 已连接')
socket.on('create', ({ host: ip, token }) => { socket.on('create', async ({ host: ip, token }) => {
const { code } = verifyAuth(token, clientIp) const { code } = await verifyAuthSync(token, clientIp)
if(code !== 1) { if (code !== 1) {
socket.emit('token_verify_fail') socket.emit('token_verify_fail')
socket.disconnect() socket.disconnect()
return return
} }
const sshRecord = readSSHRecord() const sshRecord = await readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip) 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 let { type, host, port, username, randomKey } = loginInfo
try { try {
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】 // 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
randomKey = AESDecrypt(randomKey) // 先对称解密key randomKey = await AESDecryptSync(randomKey) // 先对称解密key
randomKey = RSADecrypt(randomKey) // 再非对称解密key randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
loginInfo[type] = AESDecrypt(loginInfo[type], randomKey) // 对称解密ssh密钥 loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
consola.info('准备连接终端:', host) 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 sshClient
.on('ready', () => { .on('ready', () => {
consola.success('已连接到终端:', host) consola.success('已连接到终端:', host)
socket.emit('connect_success', `已连接到终端:${ host }`) socket.emit('connect_success', `已连接到终端:${host}`)
createTerminal(socket, sshClient) createTerminal(socket, sshClient)
}) })
.on('error', (err) => { .on('error', (err) => {
console.log(err)
consola.error('连接终端失败:', err.level) consola.error('连接终端失败:', err.level)
socket.emit('connect_fail', err.message) socket.emit('connect_fail', err.message)
}) })
.connect(authInfo) .connect({
...authInfo,
// debug: (info) => console.log(info)
})
} catch (err) { } catch (err) {
consola.error('创建终端失败:', err.message) consola.error('创建终端失败:', err.message)
socket.emit('create_fail', err.message) socket.emit('create_fail', err.message)

View File

@ -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": [
]
}

View File

@ -1,7 +0,0 @@
[
{
"id": "default",
"name": "默认分组",
"index": 0
}
]

View File

@ -1 +0,0 @@
[]

View File

@ -1,6 +0,0 @@
{
"pwd": "admin",
"commonKey": "",
"publicKey": "",
"privateKey": ""
}

View File

@ -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
}
]

View File

@ -1 +0,0 @@
[]

View 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
}
}

View File

@ -11,12 +11,12 @@ const emailTransporter = async (params = {}) => {
let { toEmail, title, html } = params let { toEmail, title, html } = params
try { try {
if(!toEmail) throw Error('missing params: toEmail') 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 } 不存在已保存的配置文件中, 请移除后重新添加`) if(!userEmail) throw Error(`${ toEmail } 不存在已保存的配置文件中, 请移除后重新添加`)
let { target } = userEmail 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 }`) if(!emailServerConf) throw Error(`邮箱类型不支持:${ target }`)
const timeout = 1000*6 const timeout = 1000*5
let options = Object.assign({}, userEmail, emailServerConf, { greetingTimeout: timeout, connectionTimeout: timeout }) let options = Object.assign({}, userEmail, emailServerConf, { greetingTimeout: timeout, connectionTimeout: timeout })
let transporter = nodemailer.createTransport(options) let transporter = nodemailer.createTransport(options)
let info = await transporter.sendMail({ let info = await transporter.sendMail({
@ -36,7 +36,7 @@ const emailTransporter = async (params = {}) => {
const sendEmailToConfList = (title, content) => { const sendEmailToConfList = (title, content) => {
// eslint-disable-next-line // eslint-disable-next-line
return new Promise(async (res, rej) => { return new Promise(async (res, rej) => {
let emailList = readUserEmailList() let emailList = await readUserEmailList()
if(Array.isArray(emailList) && emailList.length >= 1) { if(Array.isArray(emailList) && emailList.length >= 1) {
for (const item of emailList) { for (const item of emailList) {
const toEmail = item.auth.user const toEmail = item.auth.user

View File

@ -4,10 +4,10 @@ const NodeRSA = require('node-rsa')
const { readKey } = require('./storage.js') const { readKey } = require('./storage.js')
// rsa非对称 私钥解密 // rsa非对称 私钥解密
const RSADecrypt = (ciphertext) => { const RSADecryptSync = async (ciphertext) => {
if(!ciphertext) return if (!ciphertext) return
let { privateKey } = readKey() let { privateKey } = await readKey()
privateKey = AESDecrypt(privateKey) // 先解密私钥 privateKey = await AESDecryptSync(privateKey) // 先解密私钥
const rsakey = new NodeRSA(privateKey) const rsakey = new NodeRSA(privateKey)
rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt rsakey.setOptions({ encryptionScheme: 'pkcs1' }) // Must Set It When Frontend Use jsencrypt
const plaintext = rsakey.decrypt(ciphertext, 'utf8') const plaintext = rsakey.decrypt(ciphertext, 'utf8')
@ -15,17 +15,17 @@ const RSADecrypt = (ciphertext) => {
} }
// aes对称 加密(default commonKey) // aes对称 加密(default commonKey)
const AESEncrypt = (text, key) => { const AESEncryptSync = async (text, key) => {
if(!text) return if(!text) return
let { commonKey } = readKey() let { commonKey } = await readKey()
let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString() let ciphertext = CryptoJS.AES.encrypt(text, key || commonKey).toString()
return ciphertext return ciphertext
} }
// aes对称 解密(default commonKey) // aes对称 解密(default commonKey)
const AESDecrypt = (ciphertext, key) => { const AESDecryptSync = async (ciphertext, key) => {
if(!ciphertext) return if(!ciphertext) return
let { commonKey } = readKey() let { commonKey } = await readKey()
let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey) let bytes = CryptoJS.AES.decrypt(ciphertext, key || commonKey)
let originalText = bytes.toString(CryptoJS.enc.Utf8) let originalText = bytes.toString(CryptoJS.enc.Utf8)
return originalText return originalText
@ -37,8 +37,8 @@ const SHA1Encrypt = (clearText) => {
} }
module.exports = { module.exports = {
RSADecrypt, RSADecryptSync,
AESEncrypt, AESEncryptSync,
AESDecrypt, AESDecryptSync,
SHA1Encrypt SHA1Encrypt
} }

View File

@ -12,9 +12,10 @@ const {
getNotifySwByType, getNotifySwByType,
writeNotifyList, writeNotifyList,
readGroupList, readGroupList,
writeGroupList } = require('./storage') writeGroupList
const { RSADecrypt, AESEncrypt, AESDecrypt, SHA1Encrypt } = require('./encrypt') } = require('./storage')
const { verifyAuth, isProd } = require('./verify-auth') const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt')
const { verifyAuthSync, isProd } = require('./verify-auth')
const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools') const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp } = require('./tools')
const { emailTransporter, sendEmailToConfList } = require('./email') const { emailTransporter, sendEmailToConfList } = require('./email')
@ -25,11 +26,11 @@ module.exports = {
randomStr, randomStr,
getUTCDate, getUTCDate,
formatTimestamp, formatTimestamp,
verifyAuth, verifyAuthSync,
isProd, isProd,
RSADecrypt, RSADecryptSync,
AESEncrypt, AESEncryptSync,
AESDecrypt, AESDecryptSync,
SHA1Encrypt, SHA1Encrypt,
readSSHRecord, readSSHRecord,
writeSSHRecord, writeSSHRecord,

View File

@ -1,124 +1,239 @@
const fs = require('fs') 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 = () => { const readKey = async () => {
let list 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 { try {
list = JSON.parse(fs.readFileSync(sshRecordPath, 'utf8')) support = (await readEmailNotifyConf()).support
} 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
} catch (error) { } catch (error) {
consola.error('读取email support错误: ', error) consola.error('读取email support错误: ', error)
} }
return supportEmailList return support
} }
const readUserEmailList = () => { const readUserEmailList = async () => {
let configEmailList = [] let user = []
try { try {
configEmailList = readEmailJson().user user = (await readEmailNotifyConf()).user
} catch (error) { } catch (error) {
consola.error('读取email config错误: ', 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 = () => { const getNotifySwByType = async (type) => {
let notifyList = [] if (!type) throw Error('missing params: type')
try { try {
notifyList = JSON.parse(fs.readFileSync(notifyPath, 'utf8')) let notifyList = await readNotifyList()
} catch (error) { let { sw } = notifyList.find((item) => item.type === type)
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)
return sw return sw
} catch (error) { } catch (error) {
consola.error(`通知类型[${ type }]不存在`) consola.error(`通知类型[${type}]不存在`)
return false return false
} }
} }
const writeNotifyList = (notifyList) => { const readNotifyList = async () => {
fs.writeFileSync(notifyPath, JSON.stringify(notifyList, null, 2)) 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 = () => { const writeNotifyList = async (notifyList) => {
let list return new Promise((resolve, reject) => {
try { const notifyDB = new NotifyDB().getInstance()
list = JSON.parse(fs.readFileSync(groupPath, 'utf8')) notifyDB.remove({}, { multi: true }, (err, numRemoved) => {
} catch (error) { if (err) {
consola.error('读取group-list错误, 即将重置group列表: ', error) consola.error('清空notify list出错:', err);
writeSSHRecord([]) reject(err);
} } else {
return list || [] notifyDB.insert(notifyList, (err, newDocs) => {
if (err) {
consola.error('写入新的notify list出错:', err);
reject(err)
} else {
notifyDB.compactDatafile()
resolve(newDocs);
}
})
}
})
})
} }
const writeGroupList = (list = []) => { const readGroupList = async () => {
fs.writeFileSync(groupPath, JSON.stringify(list, null, 2)) 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 = { module.exports = {
@ -128,12 +243,12 @@ module.exports = {
writeHostList, writeHostList,
readKey, readKey,
writeKey, writeKey,
readSupportEmailList,
readUserEmailList,
writeUserEmailList,
readNotifyList, readNotifyList,
getNotifySwByType, getNotifySwByType,
writeNotifyList, writeNotifyList,
readGroupList, readGroupList,
writeGroupList writeGroupList,
readSupportEmailList,
readUserEmailList,
writeUserEmailList
} }

View File

@ -1,10 +1,19 @@
const fs = require('fs')
const net = require('net')
const axios = require('axios') const axios = require('axios')
const request = axios.create({ timeout: 3000 }) const request = axios.create({ timeout: 3000 })
// 为空时请求本地IP // 为空时请求本地IP
const getNetIPInfo = async (searchIp = '') => { const getNetIPInfo = async (searchIp = '') => {
searchIp = searchIp.replace(/::ffff:/g, '') || '' // fix: nginx反代 console.log('searchIp:', searchIp)
if(['::ffff:', '::1'].includes(searchIp)) searchIp = '127.0.0.1' if (isLocalIP(searchIp)) {
return {
ip: searchIp,
country: '本地',
city: '局域网',
error: null
}
}
try { try {
let date = Date.now() let date = Date.now()
let ipUrls = [ let ipUrls = [
@ -63,7 +72,7 @@ const getNetIPInfo = async (searchIp = '') => {
let { origip: ip, location: country, city = '', regionName = '' } = res || {} let { origip: ip, location: country, city = '', regionName = '' } = res || {}
searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date }) searchResult.push({ ip, country, city: `${ regionName } ${ city }`, date })
} }
// console.log(searchResult) console.log(searchResult)
let validInfo = searchResult.find(item => Boolean(item.country)) let validInfo = searchResult.find(item => Boolean(item.country))
consola.info('查询IP信息', validInfo) consola.info('查询IP信息', validInfo)
return validInfo || { ip: '获取IP信息API出错,请排查或更新API', country: '未知', city: '未知', date } 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 throwError = ({ status = 500, msg = 'defalut error' } = {}) => {
const err = new Error(msg) const err = new Error(msg)
err.status = status // 主动抛错 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 = { module.exports = {
getNetIPInfo, getNetIPInfo,
throwError, throwError,
isIP, isIP,
randomStr, randomStr,
getUTCDate, getUTCDate,
formatTimestamp formatTimestamp,
resolvePath
} }

View File

@ -1,5 +1,5 @@
const { AESDecrypt } = require('./encrypt') const { AESDecryptSync } = require('./encrypt')
const { readKey } = require('./storage') const { readKey } = require('./storage')
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
@ -10,21 +10,13 @@ const enumLoginCode = {
} }
// 校验token与登录IP // 校验token与登录IP
const verifyAuth = (token, clientIp) =>{ const verifyAuthSync = async (token, clientIp) => {
if(['::ffff:', '::1'].includes(clientIp)) clientIp = '127.0.0.1' consola.info('verifyAuthSync IP', clientIp)
token = AESDecrypt(token) // 先aes解密 token = await AESDecryptSync(token) // 先aes解密
const { commonKey } = readKey() const { commonKey } = await readKey()
try { try {
const { exp } = jwt.verify(token, commonKey) const { exp } = jwt.verify(token, commonKey)
if(Date.now() > (exp * 1000)) return { code: -1, msg: 'token expires' } // 过期 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与上次登录访问的不一致
}
return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功 return { code: enumLoginCode.SUCCESS, msg: 'success' } // 验证成功
} catch (error) { } catch (error) {
return { code: enumLoginCode.ERROR_TOKEN, msg: error } // token错误, 验证失败 return { code: enumLoginCode.ERROR_TOKEN, msg: error } // token错误, 验证失败
@ -37,6 +29,6 @@ const isProd = () => {
} }
module.exports = { module.exports = {
verifyAuth, verifyAuthSync,
isProd isProd
} }

View File

@ -26,6 +26,7 @@
}, },
"dependencies": { "dependencies": {
"@koa/cors": "^5.0.0", "@koa/cors": "^5.0.0",
"@seald-io/nedb": "^4.0.4",
"axios": "^0.21.4", "axios": "^0.21.4",
"consola": "^2.15.3", "consola": "^2.15.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

189
yarn.lock
View File

@ -93,6 +93,20 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" 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": "@sindresorhus/is@^0.14.0":
version "0.14.0" version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" 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" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 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: axios@^0.21.4:
version "0.21.4" version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" 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" function-bind "^1.1.1"
get-intrinsic "^1.0.2" 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: callsites@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 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" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== 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: define-properties@^1.1.3:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" 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" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== 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: escalade@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 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" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4"
integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== 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: formidable@^1.1.1:
version "1.2.6" version "1.2.6"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" 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" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 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: functional-red-black-tree@^1.0.1:
version "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" 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 "^1.0.3"
has-symbols "^1.0.1" 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: get-stream@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" 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" merge2 "^1.4.1"
slash "^3.0.0" 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: got@^9.6.0:
version "9.6.0" version "9.6.0"
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@ -1273,7 +1356,19 @@ has-property-descriptors@^1.0.0:
dependencies: dependencies:
get-intrinsic "^1.1.1" 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" version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
@ -1285,6 +1380,13 @@ has-tostringtag@^1.0.0:
dependencies: dependencies:
has-symbols "^1.0.2" 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: has-unicode@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@ -1302,6 +1404,13 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" 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: http-assert@^1.3.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" 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" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== 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: import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 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" from2 "^2.3.0"
p-is-promise "^3.0.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: is-binary-path@~2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 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: dependencies:
binary-extensions "^2.0.0" 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: is-ci@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" 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" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== 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: is-typedarray@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 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" prelude-ls "~1.1.2"
type-check "~0.3.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: lodash.includes@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 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" stream-meter "^1.0.4"
tslib "2.3.1" 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: prebuild-install@6.1.4:
version "6.1.4" version "6.1.4"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" 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" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 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: setprototypeof@1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 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" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 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: v8-compile-cache@^2.0.3:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" 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" tr46 "~0.0.3"
webidl-conversions "^3.0.0" 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: which@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"