From b1077443a3d486e1adbf75ab3b4d3d78a2293456 Mon Sep 17 00:00:00 2001 From: hanhh <18330117883@163.com> Date: Tue, 12 Oct 2021 00:27:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=B3=BB=E7=BB=9F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=93=8D=E4=BD=9C=E5=92=8C=E8=AE=BE=E7=BD=AE=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=97=A5=E5=BF=97=E9=A2=91=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .umirc.ts | 12 +-- back/api/user.ts | 52 +++++++++++ back/app.ts | 4 +- back/config/index.ts | 6 ++ back/data/auth.ts | 1 + back/loaders/express.ts | 1 + back/loaders/index.ts | 3 +- back/loaders/sock.ts | 40 +++++++++ back/services/schedule.ts | 61 +++++++++++++ back/services/sock.ts | 33 +++++++ back/services/user.ts | 70 ++++++++++++++- docker/nginx.conf | 1 + package.json | 3 + pnpm-lock.yaml | 69 ++++++++++++++- src/layouts/index.tsx | 40 ++++++++- src/pages/initialization/index.tsx | 4 +- src/pages/setting/checkUpdate.tsx | 133 +++++++++++++++++++++++++++++ src/pages/setting/index.tsx | 15 +++- 19 files changed, 531 insertions(+), 18 deletions(-) create mode 100644 back/loaders/sock.ts create mode 100644 back/services/schedule.ts create mode 100644 back/services/sock.ts create mode 100644 src/pages/setting/checkUpdate.tsx diff --git a/.gitignore b/.gitignore index 5facd0ad..5a15ac00 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ /.env.local .env .history +.version.ts /config /log diff --git a/.umirc.ts b/.umirc.ts index fb9c1015..1cc38f3c 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -33,14 +33,16 @@ export default defineConfig({ 'react-dom': 'window.ReactDOM', darkreader: 'window.DarkReader', codemirror: 'window.CodeMirror', + 'sockjs-client': 'window.SockJS', }, scripts: [ 'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js', - 'https://cdn.jsdelivr.net/npm/darkreader@4.9.34/darkreader.min.js', - 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/lib/codemirror.min.js', - 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/shell/shell.js', - 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/python/python.js', - 'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/javascript/javascript.js', + 'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js', + 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js', + 'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js', ], }); diff --git a/back/api/user.ts b/back/api/user.ts index ef0a704a..4e3bfcfb 100644 --- a/back/api/user.ts +++ b/back/api/user.ts @@ -272,4 +272,56 @@ export default (app: Router) => { } }, ); + + route.put( + '/system/log/remove', + celebrate({ + body: Joi.object({ + frequency: Joi.number().required(), + }), + }), + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const userService = Container.get(UserService); + const result = await userService.updateLogRemoveFrequency( + req.body.frequency, + ); + res.send(result); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); + + route.put( + '/system/update-check', + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const userService = Container.get(UserService); + const result = await userService.checkUpdate(); + res.send(result); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); + + route.put( + '/system/update', + async (req: Request, res: Response, next: NextFunction) => { + const logger: Logger = Container.get('logger'); + try { + const userService = Container.get(UserService); + const result = await userService.updateSystem(); + res.send(result); + } catch (e) { + logger.error('🔥 error: %o', e); + return next(e); + } + }, + ); }; diff --git a/back/app.ts b/back/app.ts index 632bb413..d7a2855a 100644 --- a/back/app.ts +++ b/back/app.ts @@ -11,7 +11,7 @@ async function startServer() { await require('./loaders').default({ expressApp: app }); - app + const server = app .listen(config.port, () => { Logger.info(` ################################################ @@ -23,6 +23,8 @@ async function startServer() { Logger.error(err); process.exit(1); }); + + await require('./loaders/sock').default({ server }); } startServer(); diff --git a/back/config/index.ts b/back/config/index.ts index 4fddf754..c98ad210 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -4,6 +4,9 @@ import { createRandomString } from './util'; process.env.NODE_ENV = process.env.NODE_ENV || 'development'; +const lastVersionFile = + 'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts'; + const envFound = dotenv.config(); const rootPath = process.cwd(); const envFile = path.join(rootPath, 'config/env.sh'); @@ -26,6 +29,7 @@ const cronDbFile = path.join(rootPath, 'db/crontab.db'); const envDbFile = path.join(rootPath, 'db/env.db'); const appDbFile = path.join(rootPath, 'db/app.db'); const authDbFile = path.join(rootPath, 'db/auth.db'); +const versionFile = path.join(rootPath, 'src/version.ts'); const configFound = dotenv.config({ path: confFile }); @@ -84,4 +88,6 @@ export default { '/api/init/user', '/api/init/notification', ], + versionFile, + lastVersionFile, }; diff --git a/back/data/auth.ts b/back/data/auth.ts index 7a760a74..1b41cb48 100644 --- a/back/data/auth.ts +++ b/back/data/auth.ts @@ -21,4 +21,5 @@ export enum AuthDataType { 'loginLog' = 'loginLog', 'authToken' = 'authToken', 'notification' = 'notification', + 'removeLogFrequency' = 'removeLogFrequency', } diff --git a/back/loaders/express.ts b/back/loaders/express.ts index e4c86f33..c57c7fd5 100644 --- a/back/loaders/express.ts +++ b/back/loaders/express.ts @@ -17,6 +17,7 @@ export default ({ app }: { app: Application }) => { app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); + app.use( jwt({ secret: config.secret as string, diff --git a/back/loaders/index.ts b/back/loaders/index.ts index 72629368..3738390e 100644 --- a/back/loaders/index.ts +++ b/back/loaders/index.ts @@ -2,8 +2,9 @@ import expressLoader from './express'; import dependencyInjectorLoader from './dependencyInjector'; import Logger from './logger'; import initData from './initData'; +import { Application } from 'express'; -export default async ({ expressApp }: { expressApp: any }) => { +export default async ({ expressApp }: { expressApp: Application }) => { Logger.info('✌️ DB loaded and connected!'); await dependencyInjectorLoader({ diff --git a/back/loaders/sock.ts b/back/loaders/sock.ts new file mode 100644 index 00000000..9393d926 --- /dev/null +++ b/back/loaders/sock.ts @@ -0,0 +1,40 @@ +import sockjs from 'sockjs'; +import { Server } from 'http'; +import Logger from './logger'; +import { Container } from 'typedi'; +import SockService from '../services/sock'; +import config from '../config/index'; +import fs from 'fs'; +import { getPlatform } from '../config/util'; + +export default async ({ server }: { server: Server }) => { + const echo = sockjs.createServer({ prefix: '/ws' }); + const sockService = Container.get(SockService); + + echo.on('connection', (conn) => { + const data = fs.readFileSync(config.authConfigFile, 'utf8'); + const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop'; + const headerToken = conn.url.replace(`${conn.pathname}?token=`, ''); + if (data) { + const { token = '', tokens = {} } = JSON.parse(data); + if (headerToken === token || tokens[platform] === headerToken) { + Logger.info('✌️ Sockjs connection success'); + sockService.addClient(conn); + + conn.on('data', (message) => { + conn.write(message); + }); + + conn.on('close', function () { + sockService.removeClient(conn); + }); + + return; + } + } + + conn.close('404'); + }); + + echo.installHandlers(server); +}; diff --git a/back/services/schedule.ts b/back/services/schedule.ts new file mode 100644 index 00000000..76a6f492 --- /dev/null +++ b/back/services/schedule.ts @@ -0,0 +1,61 @@ +import { Service, Inject } from 'typedi'; +import winston from 'winston'; +import nodeSchedule from 'node-schedule'; +import { Crontab } from '../data/cron'; +import { exec } from 'child_process'; + +@Service() +export default class ScheduleService { + private scheduleStacks = new Map(); + + constructor(@Inject('logger') private logger: winston.Logger) {} + + async generateSchedule({ _id = '', command, name, schedule }: Crontab) { + this.logger.info( + '[创建定时任务],任务ID: %s,cron: %s,任务名: %s,任务方法: %s', + _id, + schedule, + name, + ); + + this.scheduleStacks.set( + _id, + nodeSchedule.scheduleJob(_id, schedule, async () => { + try { + exec(command, async (error, stdout, stderr) => { + if (error) { + await this.logger.info( + '执行任务`%s`失败,时间:%s, 错误信息:%j', + name, + new Date().toLocaleString(), + error, + ); + } + + if (stderr) { + await this.logger.info( + '执行任务`%s`失败,时间:%s, 错误信息:%j', + name, + new Date().toLocaleString(), + stderr, + ); + } + }); + } catch (error) { + await this.logger.info( + '执行任务`%s`失败,时间:%s, 错误信息:%j', + name, + new Date().toLocaleString(), + error, + ); + } finally { + } + }), + ); + } + + async cancelSchedule(id: string, jobName: string) { + this.logger.info('[取消定时任务],任务名:%s', jobName); + this.scheduleStacks.has(id) && this.scheduleStacks.get(id)?.cancel(); + } +} diff --git a/back/services/sock.ts b/back/services/sock.ts new file mode 100644 index 00000000..c39dd9f7 --- /dev/null +++ b/back/services/sock.ts @@ -0,0 +1,33 @@ +import { Service, Inject } from 'typedi'; +import winston from 'winston'; +import { Connection } from 'sockjs'; + +@Service() +export default class SockService { + private clients: Connection[] = []; + + constructor(@Inject('logger') private logger: winston.Logger) {} + + public getClients() { + return this.clients; + } + + public addClient(conn: Connection) { + if (this.clients.indexOf(conn) === -1) { + this.clients.push(conn); + } + } + + public removeClient(conn: Connection) { + const index = this.clients.indexOf(conn); + if (index !== -1) { + this.clients.splice(index, 1); + } + } + + public sendMessage(msg: string) { + this.clients.forEach((x) => { + x.write(msg); + }); + } +} diff --git a/back/services/user.ts b/back/services/user.ts index 087d8284..cfc9d4ac 100644 --- a/back/services/user.ts +++ b/back/services/user.ts @@ -11,6 +11,10 @@ import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth'; import { NotificationInfo } from '../data/notify'; import NotificationService from './notify'; import { Request } from 'express'; +import ScheduleService from './schedule'; +import { spawn } from 'child_process'; +import SockService from './sock'; +import got from 'got'; @Service() export default class UserService { @@ -18,7 +22,11 @@ export default class UserService { private notificationService!: NotificationService; private authDb = new DataStore({ filename: config.authDbFile }); - constructor(@Inject('logger') private logger: winston.Logger) { + constructor( + @Inject('logger') private logger: winston.Logger, + private scheduleService: ScheduleService, + private sockService: SockService, + ) { this.authDb.loadDatabase((err) => { if (err) throw err; }); @@ -330,7 +338,7 @@ export default class UserService { if (err) { resolve({} as NotificationInfo); } else { - resolve(doc.info); + resolve({ ...doc.info, _id: doc._id }); } }, ); @@ -354,4 +362,62 @@ export default class UserService { return { code: 400, data: '通知发送失败,请检查参数' }; } } + + public async updateLogRemoveFrequency(frequency: number) { + const result = await this.updateNotificationDb({ + type: AuthDataType.removeLogFrequency, + info: { frequency }, + }); + const cron = { + _id: result._id, + name: '删除日志', + command: `ql rmlog ${frequency}`, + schedule: `5 23 */${frequency} * *`, + }; + await this.scheduleService.generateSchedule(cron); + return { code: 200, data: { ...cron } }; + } + + public async checkUpdate() { + try { + const { version } = await import(config.versionFile); + const lastVersionFileContent = await got.get(config.lastVersionFile); + const filePath = `${config.rootPath}/.version.ts`; + fs.writeFileSync(filePath, lastVersionFileContent.body, { + encoding: 'utf-8', + }); + const result = await import(config.versionFile); + + return { + code: 200, + data: { + hasNewVersion: version !== result.version, + ...result, + }, + }; + } catch (error) { + return { + code: 400, + data: '获取版本文件失败', + }; + } + } + + public async updateSystem() { + const cp = spawn('ql update', { shell: '/bin/bash' }); + + cp.stdout.on('data', (data) => { + this.sockService.sendMessage(data.toString()); + }); + + cp.stderr.on('data', (data) => { + this.sockService.sendMessage(data.toString()); + }); + + cp.on('error', (err) => { + this.sockService.sendMessage(JSON.stringify(err)); + }); + + return { code: 200 }; + } } diff --git a/docker/nginx.conf b/docker/nginx.conf index a0b8f6f4..86f7c12b 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -15,6 +15,7 @@ http { server_tokens off; client_max_body_size 20m; + client_body_buffer_size: 20m; keepalive_timeout 65; diff --git a/package.json b/package.json index 0571422e..f62f758a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "nodemailer": "^6.6.3", "p-queue": "6.6.2", "reflect-metadata": "^0.1.13", + "sockjs": "^0.3.21", "typedi": "^0.8.0", "uuid": "^8.3.2", "winston": "^3.3.3" @@ -60,10 +61,12 @@ "@types/nedb": "^1.8.11", "@types/node": "^14.11.2", "@types/node-fetch": "^2.5.8", + "@types/node-schedule": "^1.3.2", "@types/nodemailer": "^6.4.4", "@types/qrcode.react": "^1.0.1", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", + "@types/sockjs": "^0.3.33", "@umijs/plugin-antd": "^0.9.1", "@umijs/test": "^3.3.9", "codemirror": "^5.62.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 624e2006..a5d6e0ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,12 @@ specifiers: '@types/nedb': ^1.8.11 '@types/node': ^14.11.2 '@types/node-fetch': ^2.5.8 + '@types/node-schedule': ^1.3.2 '@types/nodemailer': ^6.4.4 '@types/qrcode.react': ^1.0.1 '@types/react': ^17.0.0 '@types/react-dom': ^17.0.0 + '@types/sockjs': ^0.3.33 '@umijs/plugin-antd': ^0.9.1 '@umijs/test': ^3.3.9 body-parser: ^1.19.0 @@ -51,7 +53,9 @@ specifiers: react-dnd-html5-backend: ^14.0.0 react-dom: 17.x react-split-pane: ^0.1.92 + react-use-websocket: ^2.8.0 reflect-metadata: ^0.1.13 + sockjs: ^0.3.21 ts-node: ^9.0.0 typedi: ^0.8.0 typescript: ^4.1.2 @@ -84,6 +88,7 @@ dependencies: nodemailer: 6.6.3 p-queue: 6.6.2 reflect-metadata: 0.1.13 + sockjs: 0.3.21 typedi: 0.8.0 uuid: 8.3.2 winston: 3.3.3 @@ -100,10 +105,12 @@ devDependencies: '@types/nedb': 1.8.11 '@types/node': 14.14.45 '@types/node-fetch': 2.5.10 + '@types/node-schedule': 1.3.2 '@types/nodemailer': 6.4.4 '@types/qrcode.react': 1.0.1 '@types/react': 17.0.5 '@types/react-dom': 17.0.5 + '@types/sockjs': 0.3.33 '@umijs/plugin-antd': 0.9.1_5ccfec03b6e15849b3687a64fe975f75 '@umijs/test': 3.4.20_ts-node@9.1.1 codemirror: 5.62.2 @@ -120,6 +127,7 @@ devDependencies: react-dnd-html5-backend: 14.0.0 react-dom: 17.0.2_react@17.0.2 react-split-pane: 0.1.92_react-dom@17.0.2+react@17.0.2 + react-use-websocket: 2.8.0_react-dom@17.0.2+react@17.0.2 ts-node: 9.1.1_typescript@4.2.4 typescript: 4.2.4 umi: 3.4.20 @@ -1214,6 +1222,12 @@ packages: form-data: 3.0.1 dev: true + /@types/node-schedule/1.3.2: + resolution: {integrity: sha512-Y0CqdAr+lCpArT8CJJjJq4U2v8Bb5e7ru2nV/NhDdaptCMCRdOL3Y7tAhen39HluQMaIKWvPbDuiFBUQpg7Srw==} + dependencies: + '@types/node': 14.17.21 + dev: true + /@types/node/14.14.45: resolution: {integrity: sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==} dev: true @@ -1226,6 +1240,10 @@ packages: resolution: {integrity: sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==} dev: true + /@types/node/14.17.21: + resolution: {integrity: sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==} + dev: true + /@types/nodemailer/6.4.4: resolution: {integrity: sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==} dependencies: @@ -1337,6 +1355,12 @@ packages: '@types/node': 14.14.45 dev: true + /@types/sockjs/0.3.33: + resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} + dependencies: + '@types/node': 14.17.21 + dev: true + /@types/stack-utils/1.0.1: resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==} dev: true @@ -3818,6 +3842,13 @@ packages: resolution: {integrity: sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==} dev: false + /faye-websocket/0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + dependencies: + websocket-driver: 0.7.4 + dev: false + /fb-watchman/2.0.1: resolution: {integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==} dependencies: @@ -4319,6 +4350,10 @@ packages: toidentifier: 1.0.0 dev: false + /http-parser-js/0.5.3: + resolution: {integrity: sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==} + dev: false + /http-signature/1.2.0: resolution: {integrity: sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -8045,6 +8080,16 @@ packages: tween-functions: 1.2.0 dev: true + /react-use-websocket/2.8.0_react-dom@17.0.2+react@17.0.2: + resolution: {integrity: sha512-0J1gsX7NFTsZYBBfAQo9vKjIyGE/uxBfb0p8yq6Iza+rZF3mQocj3kkIJujFiXCYQIBt00pWJzNj+YI5srfxZg==} + peerDependencies: + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + dependencies: + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: true + /react/16.14.0: resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} engines: {node: '>=0.10.0'} @@ -8774,6 +8819,14 @@ packages: use: 3.1.1 dev: true + /sockjs/0.3.21: + resolution: {integrity: sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==} + dependencies: + faye-websocket: 0.11.4 + uuid: 3.4.0 + websocket-driver: 0.7.4 + dev: false + /sort-keys/1.1.2: resolution: {integrity: sha1-RBttTTRnmPG05J6JIK37oOVD+a0=} engines: {node: '>=0.10.0'} @@ -9618,8 +9671,8 @@ packages: /uuid/3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true - dev: true /uuid/8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} @@ -9790,6 +9843,20 @@ packages: webpack-sources: 2.2.0 dev: true + /websocket-driver/0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + dependencies: + http-parser-js: 0.5.3 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + dev: false + + /websocket-extensions/0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + dev: false + /whatwg-encoding/1.0.5: resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} dependencies: diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 69530854..35241448 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import { enable as enableDarkMode, @@ -16,6 +16,8 @@ import vhCheck from 'vh-check'; import { version, changeLogLink, changeLog } from '../version'; import { useCtx, useTheme } from '@/utils/hooks'; import { message, Badge, Modal } from 'antd'; +// @ts-ignore +import SockJS from 'sockjs-client'; export default function (props: any) { const ctx = useCtx(); @@ -23,6 +25,7 @@ export default function (props: any) { const [user, setUser] = useState(); const [loading, setLoading] = useState(true); const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); + const ws = useRef(null); const logout = () => { request.post(`${config.apiPrefix}logout`).then(() => { @@ -152,12 +155,44 @@ export default function (props: any) { } }, []); + useEffect(() => { + ws.current = new SockJS( + `http://127.0.0.1:5600/ws?token=${localStorage.getItem(config.authKey)}`, + ); + ws.current.onopen = () => { + console.log('ws opened'); + }; + + ws.current.onclose = () => console.log('ws closed'); + const wsCurrent = ws.current; + + return () => { + wsCurrent.close(); + }; + }, []); + if (['/login', '/initialization'].includes(props.location.pathname)) { document.title = `${ (config.documentTitleMap as any)[props.location.pathname] } - 控制面板`; + if ( + systemInfo?.isInitialized && + props.location.pathname === '/initialization' + ) { + history.push('/crontab'); + } + if (systemInfo) { - return props.children; + return React.Children.map(props.children, (child) => { + return React.cloneElement(child, { + ...ctx, + ...theme, + user, + reloadUser, + reloadTheme: setTheme, + ws: ws.current, + }); + }); } } @@ -247,6 +282,7 @@ export default function (props: any) { user, reloadUser, reloadTheme: setTheme, + ws: ws.current, }); })} diff --git a/src/pages/initialization/index.tsx b/src/pages/initialization/index.tsx index f6393e1a..0bf155fb 100644 --- a/src/pages/initialization/index.tsx +++ b/src/pages/initialization/index.tsx @@ -19,7 +19,7 @@ const { Step } = Steps; const { Option } = Select; const { Link } = Typography; -const Login = () => { +const Initialization = () => { const [loading, setLoading] = useState(false); const [current, setCurrent] = React.useState(0); const [fields, setFields] = useState([]); @@ -241,4 +241,4 @@ const Login = () => { ); }; -export default Login; +export default Initialization; diff --git a/src/pages/setting/checkUpdate.tsx b/src/pages/setting/checkUpdate.tsx new file mode 100644 index 00000000..811f860b --- /dev/null +++ b/src/pages/setting/checkUpdate.tsx @@ -0,0 +1,133 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { Typography, Modal, Tag, Button, Spin, message } from 'antd'; +import { request } from '@/utils/http'; +import config from '@/utils/config'; +import { version } from '../../version'; + +const { Text, Link } = Typography; + +const CheckUpdate = ({ ws }: any) => { + const [updateLoading, setUpdateLoading] = useState(false); + const [value, setValue] = useState(''); + const modalRef = useRef(); + + const checkUpgrade = () => { + if (updateLoading) return; + setUpdateLoading(true); + const hide = message.loading('检查更新中...', 0); + request + .put(`${config.apiPrefix}system/update-check`) + .then((_data: any) => { + const { code, data } = _data; + if (code === 200 && !data.hasNewVersion) { + showConfirmUpdateModal(data); + } else { + message.success('已经是最新版了!'); + } + }) + .catch((error: any) => { + console.log(error); + }) + .finally(() => { + setUpdateLoading(false); + hide(); + }); + }; + + const showConfirmUpdateModal = (data: any) => { + const { version: newVersion, changeLog } = data; + Modal.confirm({ + width: 500, + title: ( + <> +
更新可用
+
+ 新版本{newVersion}可用。你使用的版本为{version}。 +
+ + ), + content: ( +
+          {changeLog}
+        
+ ), + okText: '更新', + cancelText: '以后再说', + onOk() { + showUpdatingModal(); + request + .put(`${config.apiPrefix}system/update`) + .then((_data: any) => {}) + .catch((error: any) => { + console.log(error); + }); + }, + }); + }; + + const showUpdatingModal = () => { + modalRef.current = Modal.info({ + width: 600, + maskClosable: false, + closable: false, + okButtonProps: { disabled: true }, + title: , + centered: true, + content: ( +
+          {value}
+        
+ ), + }); + }; + + useEffect(() => { + ws.onmessage = (e) => { + modalRef.current.update({ + content: ( +
+            {e.data + value}
+          
+ ), + }); + setValue(e.data); + }; + }, []); + + return ( + <> + {value} + + + ); +}; + +export default CheckUpdate; diff --git a/src/pages/setting/index.tsx b/src/pages/setting/index.tsx index 69902714..9fcebe04 100644 --- a/src/pages/setting/index.tsx +++ b/src/pages/setting/index.tsx @@ -31,6 +31,7 @@ import { import SecuritySettings from './security'; import LoginLog from './loginLog'; import NotificationSetting from './notification'; +import CheckUpdate from './checkUpdate'; const { Text } = Typography; const optionsWithDisabled = [ @@ -45,6 +46,7 @@ const Setting = ({ user, reloadUser, reloadTheme, + ws, }: any) => { const columns = [ { @@ -328,11 +330,16 @@ const Setting = ({ buttonStyle="solid" /> - - + + - - + +