增加初始化过程

This commit is contained in:
hanhh 2021-10-03 20:58:55 +08:00
parent bd1ca7e975
commit 14b20873c7
18 changed files with 714 additions and 223 deletions

View File

@ -1,5 +1,5 @@
import { Router } from 'express'; import { Router } from 'express';
import auth from './auth'; import user from './user';
import env from './env'; import env from './env';
import config from './config'; import config from './config';
import log from './log'; import log from './log';
@ -9,7 +9,7 @@ import open from './open';
export default () => { export default () => {
const app = Router(); const app = Router();
auth(app); user(app);
env(app); env(app);
config(app); config(app);
log(app); log(app);

View File

@ -3,7 +3,7 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import AuthService from '../services/auth'; import UserService from '../services/user';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
@ -20,8 +20,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const data = await authService.login({ ...req.body }, req); const data = await userService.login({ ...req.body }, req);
return res.send(data); return res.send(data);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -35,18 +35,9 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
fs.readFile(config.authConfigFile, 'utf8', function (err, data) { const userService = Container.get(UserService);
if (err) console.log(err); await userService.logout(req.platform);
const authInfo = JSON.parse(data);
fs.writeFileSync(
config.authConfigFile,
JSON.stringify({
...authInfo,
token: '',
}),
);
res.send({ code: 200 }); res.send({ code: 200 });
});
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
@ -54,20 +45,20 @@ export default (app: Router) => {
}, },
); );
route.post( route.put(
'/user', '/user',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const content = fs.readFileSync(config.authConfigFile, 'utf8'); const userService = Container.get(UserService);
fs.writeFile( await userService.updateUsernameAndPassword(req.body);
config.authConfigFile,
JSON.stringify({ ...JSON.parse(content || '{}'), ...req.body }),
(err) => {
if (err) console.log(err);
res.send({ code: 200, message: '更新成功' }); res.send({ code: 200, message: '更新成功' });
},
);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
@ -80,8 +71,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const authInfo = await authService.getUserInfo(); const authInfo = await userService.getUserInfo();
res.send({ res.send({
code: 200, code: 200,
data: { data: {
@ -101,8 +92,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const data = await authService.initTwoFactor(); const data = await userService.initTwoFactor();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -121,8 +112,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const data = await authService.activeTwoFactor(req.body.code); const data = await userService.activeTwoFactor(req.body.code);
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -136,8 +127,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const data = await authService.deactiveTwoFactor(); const data = await userService.deactiveTwoFactor();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -158,8 +149,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const data = await authService.twoFactorLogin(req.body, req); const data = await userService.twoFactorLogin(req.body, req);
res.send(data); res.send(data);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -173,8 +164,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const data = await authService.getLoginLog(); const data = await userService.getLoginLog();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -188,8 +179,8 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const data = await authService.getNotificationMode(); const data = await userService.getNotificationMode();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@ -203,8 +194,74 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const authService = Container.get(AuthService); const userService = Container.get(UserService);
const result = await authService.updateNotificationMode(req.body); const result = await userService.updateNotificationMode(req.body);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/system',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
let isInitialized = true;
if (
!authInfo ||
(Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin')
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
// 初始化api
route.put(
'/init/user',
celebrate({
body: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/init/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateNotificationMode(req.body);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);

View File

@ -76,4 +76,12 @@ export default {
], ],
writePathList: [configPath, scriptPath], writePathList: [configPath, scriptPath],
bakPath, bakPath,
apiWhiteList: [
'/api/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/init/user',
'/api/init/notification',
],
}; };

View File

@ -9,6 +9,7 @@ import { getPlatform, getToken } from '../config/util';
import Container from 'typedi'; import Container from 'typedi';
import OpenService from '../services/open'; import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import UserService from '../services/user';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
@ -21,12 +22,7 @@ export default ({ app }: { app: Application }) => {
secret: config.secret as string, secret: config.secret as string,
algorithms: ['HS384'], algorithms: ['HS384'],
}).unless({ }).unless({
path: [ path: [...config.apiWhiteList, /^\/open\//],
'/api/login',
'/api/crons/status',
/^\/open\//,
'/api/user/two-factor/login',
],
}), }),
); );
@ -62,9 +58,8 @@ export default ({ app }: { app: Application }) => {
if ( if (
!headerToken && !headerToken &&
req.path && req.path &&
(req.path === '/api/login' || config.apiWhiteList.includes(req.path) &&
req.path === '/open/auth/token' || req.path !== '/api/crons/status'
req.path === '/api/user/two-factor/login')
) { ) {
return next(); return next();
} }
@ -89,6 +84,29 @@ export default ({ app }: { app: Application }) => {
next(err); next(err);
}); });
app.use(async (req, res, next) => {
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) {
return next();
}
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
let isInitialized = true;
if (
!authInfo ||
(Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin')
) {
isInitialized = false;
}
if (isInitialized) {
return res.send({ code: 450, message: '未知错误' });
} else {
return next();
}
});
app.use(rewrite('/open/*', '/api/$1')); app.use(rewrite('/open/*', '/api/$1'));
app.use(config.api.prefix, routes()); app.use(config.api.prefix, routes());

View File

@ -1,7 +1,7 @@
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import AuthService from './auth'; import UserService from './user';
import got from 'got'; import got from 'got';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import crypto from 'crypto'; import crypto from 'crypto';
@ -9,8 +9,8 @@ import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@Inject((type) => AuthService) @Inject((type) => UserService)
private authService!: AuthService; private userService!: UserService;
private modeMap = new Map([ private modeMap = new Map([
['goCqHttpBot', this.goCqHttpBot], ['goCqHttpBot', this.goCqHttpBot],
@ -32,8 +32,11 @@ export default class NotificationService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async notify(title: string, content: string) { public async notify(
const { type, ...rest } = await this.authService.getNotificationMode(); title: string,
content: string,
): Promise<boolean | undefined> {
const { type, ...rest } = await this.userService.getNotificationMode();
if (type) { if (type) {
this.title = title; this.title = title;
this.content = content; this.content = content;
@ -42,9 +45,10 @@ export default class NotificationService {
try { try {
return await notificationModeAction?.call(this); return await notificationModeAction?.call(this);
} catch (error: any) { } catch (error: any) {
return error.message; return false;
} }
} }
return false;
} }
public async testNotify( public async testNotify(

View File

@ -13,7 +13,7 @@ import NotificationService from './notify';
import { Request } from 'express'; import { Request } from 'express';
@Service() @Service()
export default class AuthService { export default class UserService {
@Inject((type) => NotificationService) @Inject((type) => NotificationService)
private notificationService!: NotificationService; private notificationService!: NotificationService;
private authDb = new DataStore({ filename: config.authDbFile }); private authDb = new DataStore({ filename: config.authDbFile });
@ -160,6 +160,14 @@ export default class AuthService {
} }
} }
public async logout(platform: string): Promise<any> {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, {
token: '',
tokens: { ...authInfo.tokens, [platform]: '' },
});
}
public async getLoginLog(): Promise<AuthInfo[]> { public async getLoginLog(): Promise<AuthInfo[]> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => { this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => {
@ -205,6 +213,18 @@ export default class AuthService {
}; };
} }
public async updateUsernameAndPassword({
username,
password,
}: {
username: string;
password: string;
}) {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' };
}
public getUserInfo(): Promise<any> { public getUserInfo(): Promise<any> {
return new Promise((resolve) => { return new Promise((resolve) => {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => { fs.readFile(config.authConfigFile, 'utf8', (err, data) => {

View File

@ -50,7 +50,7 @@
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.6.2",
"@ant-design/pro-layout": "^6.5.0", "@ant-design/pro-layout": "^6.26.0",
"@monaco-editor/react": "^4.2.1", "@monaco-editor/react": "^4.2.1",
"@types/cors": "^2.8.10", "@types/cors": "^2.8.10",
"@types/express": "^4.17.8", "@types/express": "^4.17.8",

View File

@ -2,7 +2,7 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@ant-design/icons': ^4.6.2 '@ant-design/icons': ^4.6.2
'@ant-design/pro-layout': ^6.5.0 '@ant-design/pro-layout': ^6.26.0
'@monaco-editor/react': ^4.2.1 '@monaco-editor/react': ^4.2.1
'@otplib/preset-default': ^12.0.1 '@otplib/preset-default': ^12.0.1
'@types/cors': ^2.8.10 '@types/cors': ^2.8.10
@ -90,7 +90,7 @@ dependencies:
devDependencies: devDependencies:
'@ant-design/icons': 4.6.2_react-dom@17.0.2+react@17.0.2 '@ant-design/icons': 4.6.2_react-dom@17.0.2+react@17.0.2
'@ant-design/pro-layout': 6.18.0_react-dom@17.0.2+react@17.0.2 '@ant-design/pro-layout': 6.26.0_react-dom@17.0.2+react@17.0.2
'@monaco-editor/react': 4.2.1_react-dom@17.0.2+react@17.0.2 '@monaco-editor/react': 4.2.1_react-dom@17.0.2+react@17.0.2
'@types/cors': 2.8.10 '@types/cors': 2.8.10
'@types/express': 4.17.11 '@types/express': 4.17.11
@ -140,6 +140,10 @@ packages:
resolution: {integrity: sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==} resolution: {integrity: sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==}
dev: true dev: true
/@ant-design/icons-svg/4.2.1:
resolution: {integrity: sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==}
dev: true
/@ant-design/icons/4.6.2_react-dom@17.0.2+react@17.0.2: /@ant-design/icons/4.6.2_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-QsBG2BxBYU/rxr2eb8b2cZ4rPKAPBpzAR+0v6rrZLp/lnyvflLH3tw1vregK+M7aJauGWjIGNdFmUfpAOtw25A==} resolution: {integrity: sha512-QsBG2BxBYU/rxr2eb8b2cZ4rPKAPBpzAR+0v6rrZLp/lnyvflLH3tw1vregK+M7aJauGWjIGNdFmUfpAOtw25A==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -156,59 +160,78 @@ packages:
- react-dom - react-dom
dev: true dev: true
/@ant-design/pro-layout/6.18.0_react-dom@17.0.2+react@17.0.2: /@ant-design/icons/4.7.0_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-w0Va3XpI01pNi6I+wxBn7xtGCVAPeUkGVmeKXLUOykqSBGZWFZPKtYwGg7xOX7cETgJN8QBWTjwtmHfO5Hx/UA==} resolution: {integrity: sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g==}
engines: {node: '>=8'}
peerDependencies: peerDependencies:
antd: ^4.x react: '>=16.0.0'
react-dom: '>=16.0.0'
dependencies:
'@ant-design/colors': 6.0.0
'@ant-design/icons-svg': 4.2.1
'@babel/runtime': 7.15.4
classnames: 2.3.1
rc-util: 5.14.0_react-dom@17.0.2+react@17.0.2
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
dev: true
/@ant-design/pro-layout/6.26.0_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-kr/+OkxGq8R6hcYse35uCrfd6+Ypo/nEtMr+vQtEa3OR4JzDrBW/wgbsUxshCwnzIOzckh85B1uAdaBQpU/kTw==}
peerDependencies:
antd: 4.x
react: '>=16.9.0' react: '>=16.9.0'
dependencies: dependencies:
'@ant-design/icons': 4.6.2_react-dom@17.0.2+react@17.0.2 '@ant-design/icons': 4.7.0_react-dom@17.0.2+react@17.0.2
'@ant-design/pro-provider': 1.4.12_react-dom@17.0.2+react@17.0.2 '@ant-design/pro-provider': 1.4.19_react-dom@17.0.2+react@17.0.2
'@ant-design/pro-utils': 1.16.5_react-dom@17.0.2+react@17.0.2 '@ant-design/pro-utils': 1.24.6_react-dom@17.0.2+react@17.0.2
'@umijs/route-utils': 1.0.37 '@umijs/route-utils': 1.0.37
'@umijs/use-params': 1.0.3_react@17.0.2 '@umijs/use-params': 1.0.6_react@17.0.2
classnames: 2.3.1 classnames: 2.3.1
lodash.merge: 4.6.2
omit.js: 2.0.2 omit.js: 2.0.2
path-to-regexp: 2.4.0 path-to-regexp: 2.4.0
rc-resize-observer: 0.2.6_react-dom@17.0.2+react@17.0.2 rc-resize-observer: 0.2.6_react-dom@17.0.2+react@17.0.2
rc-util: 5.12.2_react-dom@17.0.2+react@17.0.2 rc-util: 5.14.0_react-dom@17.0.2+react@17.0.2
react: 17.0.2 react: 17.0.2
swr: 0.5.6_react@17.0.2 swr: 1.1.0-beta.3_react@17.0.2
unstated-next: 1.1.0 unstated-next: 1.1.0
use-json-comparison: 1.0.5_react@17.0.2 use-json-comparison: 1.0.6_react@17.0.2
use-media-antd-query: 1.0.7_react@17.0.2 use-media-antd-query: 1.1.0_react@17.0.2
warning: 4.0.3 warning: 4.0.3
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
dev: true dev: true
/@ant-design/pro-provider/1.4.12_react-dom@17.0.2+react@17.0.2: /@ant-design/pro-provider/1.4.19_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-KgdM8H1zQHKFrcWWLVCOU85PhbuyLvedhj6RX3iEtyHQsYrcP8vDNuxzzVW5o5Isqxrz4VxHG9kL0DdIKDp2Ng==} resolution: {integrity: sha512-0cIRwHGxZ5fXuwEyzZbAKXE8eJ732Whh2pEy/1MfPAvFujtC5OwZQbAzRmVoKYeLYyBtfOBhgk96Bvii9taWoA==}
peerDependencies: peerDependencies:
antd: 4.x antd: 4.x
react: '>=16.9.0' react: '>=16.9.0'
dependencies: dependencies:
rc-util: 5.12.2_react-dom@17.0.2+react@17.0.2 rc-util: 5.14.0_react-dom@17.0.2+react@17.0.2
react: 17.0.2 react: 17.0.2
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
dev: true dev: true
/@ant-design/pro-utils/1.16.5_react-dom@17.0.2+react@17.0.2: /@ant-design/pro-utils/1.24.6_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-u8l8sfmTxR2bX9L/+WRFJa65D9MVr4PzOY8HtLxwiGurWWp3nPV+hsg9n48jRsztOxs5EcruInO8j4jLinYjKA==} resolution: {integrity: sha512-/MBuTFyrEyKEnVBGnoUpB6yqLu2slW5MsyVm0eGX28TS3u/19/+JjV2Fxo/pxFOeEg6uasz8nOzru9f/CuJRUw==}
peerDependencies: peerDependencies:
antd: 4.x antd: 4.x
react: '>=16.9.0' react: '>=16.9.0'
react-dom: '>=16.9.0' react-dom: '>=16.9.0'
dependencies: dependencies:
'@ant-design/icons': 4.6.2_react-dom@17.0.2+react@17.0.2 '@ant-design/icons': 4.7.0_react-dom@17.0.2+react@17.0.2
'@ant-design/pro-provider': 1.4.12_react-dom@17.0.2+react@17.0.2 '@ant-design/pro-provider': 1.4.19_react-dom@17.0.2+react@17.0.2
classnames: 2.3.1 classnames: 2.3.1
fast-deep-equal: 3.1.3 lodash.merge: 4.6.2
moment: 2.29.1 moment: 2.29.1
rc-util: 5.12.2_react-dom@17.0.2+react@17.0.2 rc-util: 5.14.0_react-dom@17.0.2+react@17.0.2
react: 17.0.2 react: 17.0.2
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
react-sortable-hoc: 2.0.0_react-dom@17.0.2+react@17.0.2
swr: 1.1.0-beta.3_react@17.0.2
dev: true dev: true
/@ant-design/react-slick/0.28.3: /@ant-design/react-slick/0.28.3:
@ -480,6 +503,13 @@ packages:
regenerator-runtime: 0.13.7 regenerator-runtime: 0.13.7
dev: true dev: true
/@babel/runtime/7.15.4:
resolution: {integrity: sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.9
dev: true
/@babel/template/7.12.13: /@babel/template/7.12.13:
resolution: {integrity: sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==} resolution: {integrity: sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==}
dependencies: dependencies:
@ -1553,8 +1583,8 @@ packages:
webpack-chain: 6.5.1 webpack-chain: 6.5.1
dev: true dev: true
/@umijs/use-params/1.0.3_react@17.0.2: /@umijs/use-params/1.0.6_react@17.0.2:
resolution: {integrity: sha512-gob94yiBsyFNeRKG+zK2nAKxqVsEkYh4vWM3sSyqwidHoUR+C7MmZUHXBBtB1/9C0dpRAi9arkUYvzuNla6q7Q==} resolution: {integrity: sha512-w4C73Szn2RdUSeAoUxoJW/Jc0RHJ+MNZ6ywLfkNm2+9CkJ0GmyT5DTJiTtO5J5YZcurNPUN1evUBpGKYOsVSEA==}
peerDependencies: peerDependencies:
react: '*' react: '*'
dependencies: dependencies:
@ -3271,11 +3301,6 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: false dev: false
/dequal/2.0.2:
resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==}
engines: {node: '>=6'}
dev: true
/des.js/1.0.1: /des.js/1.0.1:
resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==} resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==}
dependencies: dependencies:
@ -5740,6 +5765,10 @@ packages:
resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=} resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=}
dev: false dev: false
/lodash.merge/4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
/lodash.once/4.1.1: /lodash.once/4.1.1:
resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=} resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=}
dev: false dev: false
@ -7523,9 +7552,9 @@ packages:
react: '>=16.9.0' react: '>=16.9.0'
react-dom: '>=16.9.0' react-dom: '>=16.9.0'
dependencies: dependencies:
'@babel/runtime': 7.14.0 '@babel/runtime': 7.15.4
classnames: 2.3.1 classnames: 2.3.1
rc-util: 5.12.2_react-dom@17.0.2+react@17.0.2 rc-util: 5.14.0_react-dom@17.0.2+react@17.0.2
react: 17.0.2 react: 17.0.2
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
resize-observer-polyfill: 1.5.1 resize-observer-polyfill: 1.5.1
@ -7789,6 +7818,19 @@ packages:
shallowequal: 1.1.0 shallowequal: 1.1.0
dev: true dev: true
/rc-util/5.14.0_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-2vy6/Z1BJUcwLjm/UEJb/htjUTQPigITUIemCcFEo1fQevAumc9sA32x2z5qyWoa9uhrXbiAjSDpPIUqyg65sA==}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.15.4
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
react-is: 16.13.1
shallowequal: 1.1.0
dev: true
/rc-virtual-list/3.2.6_react-dom@17.0.2+react@17.0.2: /rc-virtual-list/3.2.6_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-8FiQLDzm3c/tMX0d62SQtKDhLH7zFlSI6pWBAPt+TUntEqd3Lz9zFAmpvTu8gkvUom/HCsDSZs4wfV4wDPWC0Q==} resolution: {integrity: sha512-8FiQLDzm3c/tMX0d62SQtKDhLH7zFlSI6pWBAPt+TUntEqd3Lz9zFAmpvTu8gkvUom/HCsDSZs4wfV4wDPWC0Q==}
engines: {node: '>=8.x'} engines: {node: '>=8.x'}
@ -7964,6 +8006,19 @@ packages:
tiny-warning: 1.0.3 tiny-warning: 1.0.3
dev: true dev: true
/react-sortable-hoc/2.0.0_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==}
peerDependencies:
react: ^16.3.0 || ^17.0.0
react-dom: ^16.3.0 || ^17.0.0
dependencies:
'@babel/runtime': 7.15.4
invariant: 2.2.4
prop-types: 15.7.2
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
dev: true
/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:
resolution: {integrity: sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==} resolution: {integrity: sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==}
peerDependencies: peerDependencies:
@ -8098,6 +8153,10 @@ packages:
resolution: {integrity: sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==} resolution: {integrity: sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==}
dev: true dev: true
/regenerator-runtime/0.13.9:
resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
dev: true
/regex-not/1.0.2: /regex-not/1.0.2:
resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -9035,12 +9094,11 @@ packages:
supports-color: 7.2.0 supports-color: 7.2.0
dev: true dev: true
/swr/0.5.6_react@17.0.2: /swr/1.1.0-beta.3_react@17.0.2:
resolution: {integrity: sha512-Bmx3L4geMZjYT5S2Z6EE6/5Cx6v1Ka0LhqZKq8d6WL2eu9y6gHWz3dUzfIK/ymZVHVfwT/EweFXiYGgfifei3w==} resolution: {integrity: sha512-1fR0VBmSewUMHtICnWg2Q6tzyV9tbL2b5MLEodBCO4AT50k51WYNLsDEzqu801vD6Sdhhqh+fmkubuRkOayYvQ==}
peerDependencies: peerDependencies:
react: ^16.11.0 || ^17.0.0 react: ^16.11.0 || ^17.0.0 || ^18.0.0
dependencies: dependencies:
dequal: 2.0.2
react: 17.0.2 react: 17.0.2
dev: true dev: true
@ -9498,16 +9556,16 @@ packages:
querystring: 0.2.0 querystring: 0.2.0
dev: true dev: true
/use-json-comparison/1.0.5_react@17.0.2: /use-json-comparison/1.0.6_react@17.0.2:
resolution: {integrity: sha512-P/AgEKXphcN0L/8G5wLfbEz88mZQ9ayHS1OVESZaS2nxkN/msDWTGE8E1e9HXOWCZ9yoAfDrbVKAmTYpsYupFA==} resolution: {integrity: sha512-xPadt5yMRbEmVfOSGFSMqjjICrq7nLbfSH3rYIXsrtcuFX7PmbYDN/ku8ObBn3v8o/yZelO1OxUS5+5TI3+fUw==}
peerDependencies: peerDependencies:
react: 16.x react: '>=16.9.0'
dependencies: dependencies:
react: 17.0.2 react: 17.0.2
dev: true dev: true
/use-media-antd-query/1.0.7_react@17.0.2: /use-media-antd-query/1.1.0_react@17.0.2:
resolution: {integrity: sha512-vxO+ThMiuHevxrdbNfcs8NCQ2GXy6b4n8YS273fKxLMEu4P3fmEm5cPdSdN2qh07uMclk5R6AHHhjga31I0pDg==} resolution: {integrity: sha512-B6kKZwNV4R+l4Rl11sWO7HqOay9alzs1Vp1b4YJqjz33YxbltBCZtt/yxXxkXN9rc1S7OeEL/GbwC30Wmqhw6Q==}
peerDependencies: peerDependencies:
react: '>=16.9.0' react: '>=16.9.0'
dependencies: dependencies:

View File

@ -1,11 +1,7 @@
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
const NewPageLoading = () => { const NewPageLoading = () => {
return ( return <PageLoading delay={1}></PageLoading>;
<div style={{ margin: '-24px -24px 0' }}>
<PageLoading delay={1}></PageLoading>
</div>
);
}; };
export default NewPageLoading; export default NewPageLoading;

View File

@ -18,6 +18,12 @@ export default {
hideInMenu: true, hideInMenu: true,
component: '@/pages/login/index', component: '@/pages/login/index',
}, },
{
name: '初始化',
path: '/initialization',
hideInMenu: true,
component: '@/pages/initialization/index',
},
{ {
path: '/crontab', path: '/crontab',
name: '定时任务', name: '定时任务',

View File

@ -247,4 +247,8 @@
::placeholder { ::placeholder {
opacity: 0.5 !important; opacity: 0.5 !important;
} }
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
} }

View File

@ -22,6 +22,7 @@ export default function (props: any) {
const theme = useTheme(); const theme = useTheme();
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 logout = () => { const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => { request.post(`${config.apiPrefix}logout`).then(() => {
@ -30,6 +31,22 @@ export default function (props: any) {
}); });
}; };
const getSystemInfo = () => {
request.get(`${config.apiPrefix}system`).then(({ code, data }) => {
if (code === 200) {
setSystemInfo(data);
if (!data.isInitialized) {
history.push('/initialization');
setLoading(false);
} else {
getUser();
}
} else {
message.error(data);
}
});
};
const getUser = (needLoading = true) => { const getUser = (needLoading = true) => {
needLoading && setLoading(true); needLoading && setLoading(true);
request.get(`${config.apiPrefix}user`).then(({ code, data }) => { request.get(`${config.apiPrefix}user`).then(({ code, data }) => {
@ -63,19 +80,21 @@ export default function (props: any) {
}; };
useEffect(() => { useEffect(() => {
const isAuth = localStorage.getItem(config.authKey);
if (!isAuth) {
history.push('/login');
}
vhCheck(); vhCheck();
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!user) { if (systemInfo && systemInfo.isInitialized && !user) {
getUser(); getUser();
} }
}, [props.location.pathname]); }, [props.location.pathname]);
useEffect(() => {
if (!systemInfo) {
getSystemInfo();
}
}, [systemInfo]);
useEffect(() => { useEffect(() => {
setTheme(); setTheme();
}, [theme.theme]); }, [theme.theme]);
@ -92,9 +111,14 @@ export default function (props: any) {
} }
}, []); }, []);
if (props.location.pathname === '/login') { if (['/login', '/initialization'].includes(props.location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[props.location.pathname]
} - `;
if (systemInfo) {
return props.children; return props.children;
} }
}
const isFirefox = navigator.userAgent.includes('Firefox'); const isFirefox = navigator.userAgent.includes('Firefox');
const isSafari = const isSafari =
@ -148,7 +172,7 @@ export default function (props: any) {
]; ];
}} }}
pageTitleRender={(props, pageName, info) => { pageTitleRender={(props, pageName, info) => {
if (info) { if (info && typeof info.pageName === 'string') {
return `${info.pageName} - 控制面板`; return `${info.pageName} - 控制面板`;
} }
return '控制面板'; return '控制面板';

View File

@ -0,0 +1,72 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
height: calc(100vh - var(--vh-offset, 0px));
overflow: auto;
background: @layout-body-background;
padding-top: 70px;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
}
.top {
text-align: center;
}
.header {
display: flex;
align-items: center;
flex-direction: column;
}
.logo {
width: 48px;
display: block;
margin-bottom: 24px;
margin-top: 20px;
}
.title {
font-size: 20px;
margin-bottom: 16px;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.main {
padding: 20px;
border-radius: 6px;
background-color: #f6f8fa;
border: 1px solid #ebedef;
display: flex;
width: 500px;
height: 500px;
.ant-steps {
width: 150px;
}
.steps-container {
flex: 1;
overflow-y: auto;
padding-right: 20px;
}
}
.extra {
margin-top: 20px;
}

View File

@ -0,0 +1,237 @@
import React, { Fragment, useEffect, useState } from 'react';
import {
Button,
Row,
Input,
Form,
message,
notification,
Steps,
Select,
} from 'antd';
import config from '@/utils/config';
import { history, Link } from 'umi';
import styles from './index.less';
import { request } from '@/utils/http';
const FormItem = Form.Item;
const { Step } = Steps;
const { Option } = Select;
const Login = () => {
const [loading, setLoading] = useState(false);
const [current, setCurrent] = React.useState(0);
const [fields, setFields] = useState<any[]>([]);
const next = () => {
setCurrent(current + 1);
};
const prev = () => {
setCurrent(current - 1);
};
const submitAccountSetting = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}init/user`, {
data: {
username: values.username,
password: values.password,
},
})
.then((data) => {
if (data.code === 200) {
next();
} else {
message.error(data.message);
}
})
.finally(() => setLoading(false));
};
const submitNotification = (values: any) => {
request
.put(`${config.apiPrefix}init/notification`, {
data: {
...values,
},
})
.then((_data: any) => {
if (_data && _data.code === 200) {
next();
} else {
message.error(_data.data);
}
})
.finally(() => setLoading(false));
};
const notificationModeChange = (value: string) => {
const _fields = (config.notificationModeMap as any)[value];
setFields(_fields || []);
};
const steps = [
{
title: '欢迎使用',
content: (
<div className={styles.top} style={{ marginTop: 120 }}>
<div className={styles.header}>
<span className={styles.title}>使</span>
</div>
<div className={styles.action}>
<Button
type="primary"
onClick={() => {
next();
}}
>
</Button>
</div>
</div>
),
},
{
title: '账户设置',
content: (
<Form onFinish={submitAccountSetting} layout="vertical">
<Form.Item
label="用户名"
name="username"
rules={[{ required: true }]}
style={{ maxWidth: 350 }}
>
<Input placeholder="用户名" />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true }]}
hasFeedback
style={{ maxWidth: 350 }}
>
<Input type="password" placeholder="密码" />
</Form.Item>
<Form.Item
name="confirm"
label="确认密码"
dependencies={['password']}
hasFeedback
style={{ maxWidth: 350 }}
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('您输入的两个密码不匹配!'));
},
}),
]}
>
<Input.Password placeholder="确认密码" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Form>
),
},
{
title: '通知设置',
content: (
<Form onFinish={submitNotification} layout="vertical">
<Form.Item
label="通知方式"
name="type"
rules={[{ required: true, message: '请选择通知方式' }]}
style={{ maxWidth: 350 }}
>
<Select
onChange={notificationModeChange}
placeholder="请选择通知方式"
>
{config.notificationModes
.filter((x) => x.value !== 'closed')
.map((x) => (
<Option value={x.value}>{x.label}</Option>
))}
</Select>
</Form.Item>
{fields.map((x) => (
<Form.Item
label={x.label}
name={x.label}
extra={x.tip}
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
<Input.TextArea
autoSize={true}
placeholder={`请输入${x.label}`}
/>
</Form.Item>
))}
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
<Button type="link" htmlType="button" onClick={() => next()}>
</Button>
</Form>
),
},
{
title: '完成安装',
content: (
<div className={styles.top} style={{ marginTop: 120 }}>
<div className={styles.header}>
<span className={styles.title}></span>
</div>
<div className={styles.action}>
<Button
type="primary"
onClick={() => {
history.push('/login');
}}
>
</Button>
</div>
</div>
),
},
];
return (
<div className={styles.container}>
<div className={styles.top}>
<div className={styles.header}>
<img alt="logo" className={styles.logo} src="/images/qinglong.png" />
<span className={styles.title}></span>
</div>
</div>
<div className={styles.main}>
<Steps
current={current}
direction="vertical"
className={styles['ant-steps']}
>
{steps.map((item) => (
<Step key={item.title} title={item.title} />
))}
</Steps>
<div className={styles['steps-container']}>
{steps[current].content}
</div>
</div>
</div>
);
};
export default Login;

View File

@ -3,29 +3,12 @@
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
height: 100vh; height: 100vh;
height: calc(100vh - var(--vh-offset, 0px)); height: calc(100vh - var(--vh-offset, 0px));
overflow: auto; overflow: auto;
background: @layout-body-background; background: @layout-body-background;
} padding-top: 70px;
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
position: absolute;
top: 86px;
left: 50%;
margin-left: -170px;
width: 340px;
padding: 0 16px;
} }
@media (min-width: @screen-md-min) { @media (min-width: @screen-md-min) {
@ -70,6 +53,7 @@
border-radius: 6px; border-radius: 6px;
background-color: #f6f8fa; background-color: #f6f8fa;
border: 1px solid #ebedef; border: 1px solid #ebedef;
width: 340px;
@media screen and (max-width: @screen-sm) { @media screen and (max-width: @screen-sm) {
width: 95%; width: 95%;

View File

@ -122,14 +122,9 @@ const Login = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.content}>
<div className={styles.top}> <div className={styles.top}>
<div className={styles.header}> <div className={styles.header}>
<img <img alt="logo" className={styles.logo} src="/images/qinglong.png" />
alt="logo"
className={styles.logo}
src="/images/qinglong.png"
/>
<span className={styles.title}> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName} {twoFactor ? '两步验证' : config.siteName}
</span> </span>
@ -204,9 +199,7 @@ const Login = () => {
<div className={styles.extra}> <div className={styles.extra}>
{twoFactor ? ( {twoFactor ? (
<div style={{ paddingLeft: 20, position: 'relative' }}> <div style={{ paddingLeft: 20, position: 'relative' }}>
<MobileOutlined <MobileOutlined style={{ position: 'absolute', left: 0, top: 4 }} />
style={{ position: 'absolute', left: 0, top: 4 }}
/>
</div> </div>
) : ( ) : (
@ -214,7 +207,6 @@ const Login = () => {
)} )}
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -17,7 +17,7 @@ const SecuritySettings = ({ user, userChange }: any) => {
const handleOk = (values: any) => { const handleOk = (values: any) => {
request request
.post(`${config.apiPrefix}user`, { .put(`${config.apiPrefix}user`, {
data: { data: {
username: values.username, username: values.username,
password: values.password, password: values.password,

View File

@ -174,4 +174,15 @@ export default {
{ label: 'emailPass', tip: '邮箱SMTP授权码', required: true }, { label: 'emailPass', tip: '邮箱SMTP授权码', required: true },
], ],
}, },
documentTitleMap: {
'/login': '登录',
'/initialization': '初始化',
'/cron': '定时任务',
'/env': '环境变量',
'/config': '配置文件',
'/script': '脚本管理',
'/diff': '对比工具',
'/log': '任务日志',
'/setting': '系统设置',
},
}; };