添加系统更新操作和设置删除日志频率

This commit is contained in:
hanhh 2021-10-12 00:27:42 +08:00
parent 9455ca64a2
commit b1077443a3
19 changed files with 531 additions and 18 deletions

1
.gitignore vendored
View File

@ -21,6 +21,7 @@
/.env.local /.env.local
.env .env
.history .history
.version.ts
/config /config
/log /log

View File

@ -33,14 +33,16 @@ export default defineConfig({
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
darkreader: 'window.DarkReader', darkreader: 'window.DarkReader',
codemirror: 'window.CodeMirror', codemirror: 'window.CodeMirror',
'sockjs-client': 'window.SockJS',
}, },
scripts: [ scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', '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://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/darkreader@4/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/lib/codemirror.min.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/shell/shell.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/python/python.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
'https://cdn.jsdelivr.net/npm/codemirror@5.62.0/mode/javascript/javascript.js', 'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
], ],
}); });

View File

@ -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);
}
},
);
}; };

View File

@ -11,7 +11,7 @@ async function startServer() {
await require('./loaders').default({ expressApp: app }); await require('./loaders').default({ expressApp: app });
app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.info(` Logger.info(`
################################################ ################################################
@ -23,6 +23,8 @@ async function startServer() {
Logger.error(err); Logger.error(err);
process.exit(1); process.exit(1);
}); });
await require('./loaders/sock').default({ server });
} }
startServer(); startServer();

View File

@ -4,6 +4,9 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 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 envFound = dotenv.config();
const rootPath = process.cwd(); const rootPath = process.cwd();
const envFile = path.join(rootPath, 'config/env.sh'); 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 envDbFile = path.join(rootPath, 'db/env.db');
const appDbFile = path.join(rootPath, 'db/app.db'); const appDbFile = path.join(rootPath, 'db/app.db');
const authDbFile = path.join(rootPath, 'db/auth.db'); const authDbFile = path.join(rootPath, 'db/auth.db');
const versionFile = path.join(rootPath, 'src/version.ts');
const configFound = dotenv.config({ path: confFile }); const configFound = dotenv.config({ path: confFile });
@ -84,4 +88,6 @@ export default {
'/api/init/user', '/api/init/user',
'/api/init/notification', '/api/init/notification',
], ],
versionFile,
lastVersionFile,
}; };

View File

@ -21,4 +21,5 @@ export enum AuthDataType {
'loginLog' = 'loginLog', 'loginLog' = 'loginLog',
'authToken' = 'authToken', 'authToken' = 'authToken',
'notification' = 'notification', 'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency',
} }

View File

@ -17,6 +17,7 @@ export default ({ app }: { app: Application }) => {
app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
jwt({ jwt({
secret: config.secret as string, secret: config.secret as string,

View File

@ -2,8 +2,9 @@ import expressLoader from './express';
import dependencyInjectorLoader from './dependencyInjector'; import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger'; import Logger from './logger';
import initData from './initData'; 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!'); Logger.info('✌️ DB loaded and connected!');
await dependencyInjectorLoader({ await dependencyInjectorLoader({

40
back/loaders/sock.ts Normal file
View File

@ -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);
};

61
back/services/schedule.ts Normal file
View File

@ -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<string, nodeSchedule.Job>();
constructor(@Inject('logger') private logger: winston.Logger) {}
async generateSchedule({ _id = '', command, name, schedule }: Crontab) {
this.logger.info(
'[创建定时任务]任务ID: %scron: %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();
}
}

33
back/services/sock.ts Normal file
View File

@ -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);
});
}
}

View File

@ -11,6 +11,10 @@ import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import { Request } from 'express'; import { Request } from 'express';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
@Service() @Service()
export default class UserService { export default class UserService {
@ -18,7 +22,11 @@ export default class UserService {
private notificationService!: NotificationService; private notificationService!: NotificationService;
private authDb = new DataStore({ filename: config.authDbFile }); 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) => { this.authDb.loadDatabase((err) => {
if (err) throw err; if (err) throw err;
}); });
@ -330,7 +338,7 @@ export default class UserService {
if (err) { if (err) {
resolve({} as NotificationInfo); resolve({} as NotificationInfo);
} else { } else {
resolve(doc.info); resolve({ ...doc.info, _id: doc._id });
} }
}, },
); );
@ -354,4 +362,62 @@ export default class UserService {
return { code: 400, data: '通知发送失败,请检查参数' }; 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 };
}
} }

View File

@ -15,6 +15,7 @@ http {
server_tokens off; server_tokens off;
client_max_body_size 20m; client_max_body_size 20m;
client_body_buffer_size: 20m;
keepalive_timeout 65; keepalive_timeout 65;

View File

@ -44,6 +44,7 @@
"nodemailer": "^6.6.3", "nodemailer": "^6.6.3",
"p-queue": "6.6.2", "p-queue": "6.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sockjs": "^0.3.21",
"typedi": "^0.8.0", "typedi": "^0.8.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.3.3"
@ -60,10 +61,12 @@
"@types/nedb": "^1.8.11", "@types/nedb": "^1.8.11",
"@types/node": "^14.11.2", "@types/node": "^14.11.2",
"@types/node-fetch": "^2.5.8", "@types/node-fetch": "^2.5.8",
"@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.1", "@types/qrcode.react": "^1.0.1",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@types/sockjs": "^0.3.33",
"@umijs/plugin-antd": "^0.9.1", "@umijs/plugin-antd": "^0.9.1",
"@umijs/test": "^3.3.9", "@umijs/test": "^3.3.9",
"codemirror": "^5.62.2", "codemirror": "^5.62.2",

View File

@ -13,10 +13,12 @@ specifiers:
'@types/nedb': ^1.8.11 '@types/nedb': ^1.8.11
'@types/node': ^14.11.2 '@types/node': ^14.11.2
'@types/node-fetch': ^2.5.8 '@types/node-fetch': ^2.5.8
'@types/node-schedule': ^1.3.2
'@types/nodemailer': ^6.4.4 '@types/nodemailer': ^6.4.4
'@types/qrcode.react': ^1.0.1 '@types/qrcode.react': ^1.0.1
'@types/react': ^17.0.0 '@types/react': ^17.0.0
'@types/react-dom': ^17.0.0 '@types/react-dom': ^17.0.0
'@types/sockjs': ^0.3.33
'@umijs/plugin-antd': ^0.9.1 '@umijs/plugin-antd': ^0.9.1
'@umijs/test': ^3.3.9 '@umijs/test': ^3.3.9
body-parser: ^1.19.0 body-parser: ^1.19.0
@ -51,7 +53,9 @@ specifiers:
react-dnd-html5-backend: ^14.0.0 react-dnd-html5-backend: ^14.0.0
react-dom: 17.x react-dom: 17.x
react-split-pane: ^0.1.92 react-split-pane: ^0.1.92
react-use-websocket: ^2.8.0
reflect-metadata: ^0.1.13 reflect-metadata: ^0.1.13
sockjs: ^0.3.21
ts-node: ^9.0.0 ts-node: ^9.0.0
typedi: ^0.8.0 typedi: ^0.8.0
typescript: ^4.1.2 typescript: ^4.1.2
@ -84,6 +88,7 @@ dependencies:
nodemailer: 6.6.3 nodemailer: 6.6.3
p-queue: 6.6.2 p-queue: 6.6.2
reflect-metadata: 0.1.13 reflect-metadata: 0.1.13
sockjs: 0.3.21
typedi: 0.8.0 typedi: 0.8.0
uuid: 8.3.2 uuid: 8.3.2
winston: 3.3.3 winston: 3.3.3
@ -100,10 +105,12 @@ devDependencies:
'@types/nedb': 1.8.11 '@types/nedb': 1.8.11
'@types/node': 14.14.45 '@types/node': 14.14.45
'@types/node-fetch': 2.5.10 '@types/node-fetch': 2.5.10
'@types/node-schedule': 1.3.2
'@types/nodemailer': 6.4.4 '@types/nodemailer': 6.4.4
'@types/qrcode.react': 1.0.1 '@types/qrcode.react': 1.0.1
'@types/react': 17.0.5 '@types/react': 17.0.5
'@types/react-dom': 17.0.5 '@types/react-dom': 17.0.5
'@types/sockjs': 0.3.33
'@umijs/plugin-antd': 0.9.1_5ccfec03b6e15849b3687a64fe975f75 '@umijs/plugin-antd': 0.9.1_5ccfec03b6e15849b3687a64fe975f75
'@umijs/test': 3.4.20_ts-node@9.1.1 '@umijs/test': 3.4.20_ts-node@9.1.1
codemirror: 5.62.2 codemirror: 5.62.2
@ -120,6 +127,7 @@ devDependencies:
react-dnd-html5-backend: 14.0.0 react-dnd-html5-backend: 14.0.0
react-dom: 17.0.2_react@17.0.2 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-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 ts-node: 9.1.1_typescript@4.2.4
typescript: 4.2.4 typescript: 4.2.4
umi: 3.4.20 umi: 3.4.20
@ -1214,6 +1222,12 @@ packages:
form-data: 3.0.1 form-data: 3.0.1
dev: true 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: /@types/node/14.14.45:
resolution: {integrity: sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==} resolution: {integrity: sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==}
dev: true dev: true
@ -1226,6 +1240,10 @@ packages:
resolution: {integrity: sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==} resolution: {integrity: sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==}
dev: true dev: true
/@types/node/14.17.21:
resolution: {integrity: sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==}
dev: true
/@types/nodemailer/6.4.4: /@types/nodemailer/6.4.4:
resolution: {integrity: sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==} resolution: {integrity: sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==}
dependencies: dependencies:
@ -1337,6 +1355,12 @@ packages:
'@types/node': 14.14.45 '@types/node': 14.14.45
dev: true 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: /@types/stack-utils/1.0.1:
resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==} resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==}
dev: true dev: true
@ -3818,6 +3842,13 @@ packages:
resolution: {integrity: sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==} resolution: {integrity: sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==}
dev: false 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: /fb-watchman/2.0.1:
resolution: {integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==} resolution: {integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==}
dependencies: dependencies:
@ -4319,6 +4350,10 @@ packages:
toidentifier: 1.0.0 toidentifier: 1.0.0
dev: false dev: false
/http-parser-js/0.5.3:
resolution: {integrity: sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==}
dev: false
/http-signature/1.2.0: /http-signature/1.2.0:
resolution: {integrity: sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=} resolution: {integrity: sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=}
engines: {node: '>=0.8', npm: '>=1.3.7'} engines: {node: '>=0.8', npm: '>=1.3.7'}
@ -8045,6 +8080,16 @@ packages:
tween-functions: 1.2.0 tween-functions: 1.2.0
dev: true 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: /react/16.14.0:
resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -8774,6 +8819,14 @@ packages:
use: 3.1.1 use: 3.1.1
dev: true 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: /sort-keys/1.1.2:
resolution: {integrity: sha1-RBttTTRnmPG05J6JIK37oOVD+a0=} resolution: {integrity: sha1-RBttTTRnmPG05J6JIK37oOVD+a0=}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -9618,8 +9671,8 @@ packages:
/uuid/3.4.0: /uuid/3.4.0:
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} 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 hasBin: true
dev: true
/uuid/8.3.2: /uuid/8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
@ -9790,6 +9843,20 @@ packages:
webpack-sources: 2.2.0 webpack-sources: 2.2.0
dev: true 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: /whatwg-encoding/1.0.5:
resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
dependencies: dependencies:

View File

@ -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 ProLayout, { PageLoading } from '@ant-design/pro-layout';
import { import {
enable as enableDarkMode, enable as enableDarkMode,
@ -16,6 +16,8 @@ import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version'; import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal } from 'antd'; import { message, Badge, Modal } from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
export default function (props: any) { export default function (props: any) {
const ctx = useCtx(); const ctx = useCtx();
@ -23,6 +25,7 @@ export default function (props: any) {
const [user, setUser] = useState<any>(); const [user, setUser] = useState<any>();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
const ws = useRef<any>(null);
const logout = () => { const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => { 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)) { if (['/login', '/initialization'].includes(props.location.pathname)) {
document.title = `${ document.title = `${
(config.documentTitleMap as any)[props.location.pathname] (config.documentTitleMap as any)[props.location.pathname]
} - `; } - `;
if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
history.push('/crontab');
}
if (systemInfo) { 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, user,
reloadUser, reloadUser,
reloadTheme: setTheme, reloadTheme: setTheme,
ws: ws.current,
}); });
})} })}
</ProLayout> </ProLayout>

View File

@ -19,7 +19,7 @@ const { Step } = Steps;
const { Option } = Select; const { Option } = Select;
const { Link } = Typography; const { Link } = Typography;
const Login = () => { const Initialization = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [current, setCurrent] = React.useState(0); const [current, setCurrent] = React.useState(0);
const [fields, setFields] = useState<any[]>([]); const [fields, setFields] = useState<any[]>([]);
@ -241,4 +241,4 @@ const Login = () => {
); );
}; };
export default Login; export default Initialization;

View File

@ -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<any>();
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: (
<>
<div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{newVersion}使{version}
</div>
</>
),
content: (
<pre
style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
paddingTop: 15,
fontSize: 12,
fontWeight: 400,
}}
>
{changeLog}
</pre>
),
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: <span></span>,
centered: true,
content: (
<pre
style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
paddingTop: 15,
fontSize: 12,
fontWeight: 400,
minHeight: '60vh',
}}
>
{value}
</pre>
),
});
};
useEffect(() => {
ws.onmessage = (e) => {
modalRef.current.update({
content: (
<pre
style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
paddingTop: 15,
fontSize: 12,
fontWeight: 400,
minHeight: '60vh',
}}
>
{e.data + value}
</pre>
),
});
setValue(e.data);
};
}, []);
return (
<>
{value}
<Button type="primary" onClick={checkUpgrade}>
</Button>
</>
);
};
export default CheckUpdate;

View File

@ -31,6 +31,7 @@ import {
import SecuritySettings from './security'; import SecuritySettings from './security';
import LoginLog from './loginLog'; import LoginLog from './loginLog';
import NotificationSetting from './notification'; import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate';
const { Text } = Typography; const { Text } = Typography;
const optionsWithDisabled = [ const optionsWithDisabled = [
@ -45,6 +46,7 @@ const Setting = ({
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
ws,
}: any) => { }: any) => {
const columns = [ const columns = [
{ {
@ -328,11 +330,16 @@ const Setting = ({
buttonStyle="solid" buttonStyle="solid"
/> />
</Form.Item> </Form.Item>
<Form.Item label="日志删除频率" name="frequency" initialValue={0}> <Form.Item
<Input addonBefore="每" addonAfter="天" style={{ width: 300 }} /> label="日志删除频率"
name="frequency"
initialValue={0}
tooltip="每x天自动删除x天以前的日志"
>
<Input addonBefore="每" addonAfter="天" style={{ width: 150 }} />
</Form.Item> </Form.Item>
<Form.Item label="检查更新" name="theme" initialValue={theme}> <Form.Item label="检查更新" name="update">
<Button type="primary"></Button> <CheckUpdate ws={ws} />
</Form.Item> </Form.Item>
</Form> </Form>
</Tabs.TabPane> </Tabs.TabPane>