diff --git a/back/api/index.ts b/back/api/index.ts index 8f8e7f34..fec78580 100644 --- a/back/api/index.ts +++ b/back/api/index.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import auth from './auth'; +import user from './user'; import env from './env'; import config from './config'; import log from './log'; @@ -9,7 +9,7 @@ import open from './open'; export default () => { const app = Router(); - auth(app); + user(app); env(app); config(app); log(app); diff --git a/back/api/auth.ts b/back/api/user.ts similarity index 56% rename from back/api/auth.ts rename to back/api/user.ts index ef5bb9c7..df385d25 100644 --- a/back/api/auth.ts +++ b/back/api/user.ts @@ -3,7 +3,7 @@ import { Container } from 'typedi'; import { Logger } from 'winston'; import * as fs from 'fs'; import config from '../config'; -import AuthService from '../services/auth'; +import UserService from '../services/user'; import { celebrate, Joi } from 'celebrate'; const route = Router(); @@ -20,8 +20,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const data = await authService.login({ ...req.body }, req); + const userService = Container.get(UserService); + const data = await userService.login({ ...req.body }, req); return res.send(data); } catch (e) { logger.error('🔥 error: %o', e); @@ -35,18 +35,9 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - fs.readFile(config.authConfigFile, 'utf8', function (err, data) { - if (err) console.log(err); - const authInfo = JSON.parse(data); - fs.writeFileSync( - config.authConfigFile, - JSON.stringify({ - ...authInfo, - token: '', - }), - ); - res.send({ code: 200 }); - }); + const userService = Container.get(UserService); + await userService.logout(req.platform); + res.send({ code: 200 }); } catch (e) { logger.error('🔥 error: %o', e); return next(e); @@ -54,20 +45,20 @@ export default (app: Router) => { }, ); - route.post( + route.put( '/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 content = fs.readFileSync(config.authConfigFile, 'utf8'); - fs.writeFile( - config.authConfigFile, - JSON.stringify({ ...JSON.parse(content || '{}'), ...req.body }), - (err) => { - if (err) console.log(err); - res.send({ code: 200, message: '更新成功' }); - }, - ); + 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); @@ -80,8 +71,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const authInfo = await authService.getUserInfo(); + const userService = Container.get(UserService); + const authInfo = await userService.getUserInfo(); res.send({ code: 200, data: { @@ -101,8 +92,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const data = await authService.initTwoFactor(); + const userService = Container.get(UserService); + const data = await userService.initTwoFactor(); res.send({ code: 200, data }); } catch (e) { logger.error('🔥 error: %o', e); @@ -121,8 +112,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const data = await authService.activeTwoFactor(req.body.code); + const userService = Container.get(UserService); + const data = await userService.activeTwoFactor(req.body.code); res.send({ code: 200, data }); } catch (e) { logger.error('🔥 error: %o', e); @@ -136,8 +127,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const data = await authService.deactiveTwoFactor(); + const userService = Container.get(UserService); + const data = await userService.deactiveTwoFactor(); res.send({ code: 200, data }); } catch (e) { logger.error('🔥 error: %o', e); @@ -158,8 +149,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const data = await authService.twoFactorLogin(req.body, req); + const userService = Container.get(UserService); + const data = await userService.twoFactorLogin(req.body, req); res.send(data); } catch (e) { logger.error('🔥 error: %o', e); @@ -173,8 +164,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const data = await authService.getLoginLog(); + const userService = Container.get(UserService); + const data = await userService.getLoginLog(); res.send({ code: 200, data }); } catch (e) { logger.error('🔥 error: %o', e); @@ -188,8 +179,8 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const data = await authService.getNotificationMode(); + const userService = Container.get(UserService); + const data = await userService.getNotificationMode(); res.send({ code: 200, data }); } catch (e) { logger.error('🔥 error: %o', e); @@ -203,8 +194,74 @@ export default (app: Router) => { async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - const authService = Container.get(AuthService); - const result = await authService.updateNotificationMode(req.body); + const userService = Container.get(UserService); + 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); } catch (e) { logger.error('🔥 error: %o', e); diff --git a/back/config/index.ts b/back/config/index.ts index f3ba748a..4fddf754 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -76,4 +76,12 @@ export default { ], writePathList: [configPath, scriptPath], bakPath, + apiWhiteList: [ + '/api/login', + '/open/auth/token', + '/api/user/two-factor/login', + '/api/system', + '/api/init/user', + '/api/init/notification', + ], }; diff --git a/back/loaders/express.ts b/back/loaders/express.ts index b31ce871..8bab163e 100644 --- a/back/loaders/express.ts +++ b/back/loaders/express.ts @@ -9,6 +9,7 @@ import { getPlatform, getToken } from '../config/util'; import Container from 'typedi'; import OpenService from '../services/open'; import rewrite from 'express-urlrewrite'; +import UserService from '../services/user'; export default ({ app }: { app: Application }) => { app.enable('trust proxy'); @@ -21,12 +22,7 @@ export default ({ app }: { app: Application }) => { secret: config.secret as string, algorithms: ['HS384'], }).unless({ - path: [ - '/api/login', - '/api/crons/status', - /^\/open\//, - '/api/user/two-factor/login', - ], + path: [...config.apiWhiteList, /^\/open\//], }), ); @@ -62,9 +58,8 @@ export default ({ app }: { app: Application }) => { if ( !headerToken && req.path && - (req.path === '/api/login' || - req.path === '/open/auth/token' || - req.path === '/api/user/two-factor/login') + config.apiWhiteList.includes(req.path) && + req.path !== '/api/crons/status' ) { return next(); } @@ -89,6 +84,29 @@ export default ({ app }: { app: Application }) => { 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(config.api.prefix, routes()); diff --git a/back/services/notify.ts b/back/services/notify.ts index 78e2dde1..92ad8416 100644 --- a/back/services/notify.ts +++ b/back/services/notify.ts @@ -1,7 +1,7 @@ import { NotificationInfo } from '../data/notify'; import { Service, Inject } from 'typedi'; import winston from 'winston'; -import AuthService from './auth'; +import UserService from './user'; import got from 'got'; import nodemailer from 'nodemailer'; import crypto from 'crypto'; @@ -9,8 +9,8 @@ import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; @Service() export default class NotificationService { - @Inject((type) => AuthService) - private authService!: AuthService; + @Inject((type) => UserService) + private userService!: UserService; private modeMap = new Map([ ['goCqHttpBot', this.goCqHttpBot], @@ -32,8 +32,11 @@ export default class NotificationService { constructor(@Inject('logger') private logger: winston.Logger) {} - public async notify(title: string, content: string) { - const { type, ...rest } = await this.authService.getNotificationMode(); + public async notify( + title: string, + content: string, + ): Promise { + const { type, ...rest } = await this.userService.getNotificationMode(); if (type) { this.title = title; this.content = content; @@ -42,9 +45,10 @@ export default class NotificationService { try { return await notificationModeAction?.call(this); } catch (error: any) { - return error.message; + return false; } } + return false; } public async testNotify( diff --git a/back/services/auth.ts b/back/services/user.ts similarity index 94% rename from back/services/auth.ts rename to back/services/user.ts index fb606051..087d8284 100644 --- a/back/services/auth.ts +++ b/back/services/user.ts @@ -13,7 +13,7 @@ import NotificationService from './notify'; import { Request } from 'express'; @Service() -export default class AuthService { +export default class UserService { @Inject((type) => NotificationService) private notificationService!: NotificationService; private authDb = new DataStore({ filename: config.authDbFile }); @@ -160,6 +160,14 @@ export default class AuthService { } } + public async logout(platform: string): Promise { + const authInfo = this.getAuthInfo(); + this.updateAuthInfo(authInfo, { + token: '', + tokens: { ...authInfo.tokens, [platform]: '' }, + }); + } + public async getLoginLog(): Promise { return new Promise((resolve) => { 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 { return new Promise((resolve) => { fs.readFile(config.authConfigFile, 'utf8', (err, data) => { diff --git a/package.json b/package.json index 72cee3af..0571422e 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@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", "@types/cors": "^2.8.10", "@types/express": "^4.17.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7845647..624e2006 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,7 +2,7 @@ lockfileVersion: 5.3 specifiers: '@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 '@otplib/preset-default': ^12.0.1 '@types/cors': ^2.8.10 @@ -90,7 +90,7 @@ dependencies: devDependencies: '@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 '@types/cors': 2.8.10 '@types/express': 4.17.11 @@ -140,6 +140,10 @@ packages: resolution: {integrity: sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==} 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: resolution: {integrity: sha512-QsBG2BxBYU/rxr2eb8b2cZ4rPKAPBpzAR+0v6rrZLp/lnyvflLH3tw1vregK+M7aJauGWjIGNdFmUfpAOtw25A==} engines: {node: '>=8'} @@ -156,59 +160,78 @@ packages: - react-dom dev: true - /@ant-design/pro-layout/6.18.0_react-dom@17.0.2+react@17.0.2: - resolution: {integrity: sha512-w0Va3XpI01pNi6I+wxBn7xtGCVAPeUkGVmeKXLUOykqSBGZWFZPKtYwGg7xOX7cETgJN8QBWTjwtmHfO5Hx/UA==} + /@ant-design/icons/4.7.0_react-dom@17.0.2+react@17.0.2: + resolution: {integrity: sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g==} + engines: {node: '>=8'} 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' dependencies: - '@ant-design/icons': 4.6.2_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-utils': 1.16.5_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.19_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/use-params': 1.0.3_react@17.0.2 + '@umijs/use-params': 1.0.6_react@17.0.2 classnames: 2.3.1 + lodash.merge: 4.6.2 omit.js: 2.0.2 path-to-regexp: 2.4.0 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 - swr: 0.5.6_react@17.0.2 + swr: 1.1.0-beta.3_react@17.0.2 unstated-next: 1.1.0 - use-json-comparison: 1.0.5_react@17.0.2 - use-media-antd-query: 1.0.7_react@17.0.2 + use-json-comparison: 1.0.6_react@17.0.2 + use-media-antd-query: 1.1.0_react@17.0.2 warning: 4.0.3 transitivePeerDependencies: - react-dom dev: true - /@ant-design/pro-provider/1.4.12_react-dom@17.0.2+react@17.0.2: - resolution: {integrity: sha512-KgdM8H1zQHKFrcWWLVCOU85PhbuyLvedhj6RX3iEtyHQsYrcP8vDNuxzzVW5o5Isqxrz4VxHG9kL0DdIKDp2Ng==} + /@ant-design/pro-provider/1.4.19_react-dom@17.0.2+react@17.0.2: + resolution: {integrity: sha512-0cIRwHGxZ5fXuwEyzZbAKXE8eJ732Whh2pEy/1MfPAvFujtC5OwZQbAzRmVoKYeLYyBtfOBhgk96Bvii9taWoA==} peerDependencies: antd: 4.x react: '>=16.9.0' 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 transitivePeerDependencies: - react-dom dev: true - /@ant-design/pro-utils/1.16.5_react-dom@17.0.2+react@17.0.2: - resolution: {integrity: sha512-u8l8sfmTxR2bX9L/+WRFJa65D9MVr4PzOY8HtLxwiGurWWp3nPV+hsg9n48jRsztOxs5EcruInO8j4jLinYjKA==} + /@ant-design/pro-utils/1.24.6_react-dom@17.0.2+react@17.0.2: + resolution: {integrity: sha512-/MBuTFyrEyKEnVBGnoUpB6yqLu2slW5MsyVm0eGX28TS3u/19/+JjV2Fxo/pxFOeEg6uasz8nOzru9f/CuJRUw==} peerDependencies: antd: 4.x react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@ant-design/icons': 4.6.2_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/icons': 4.7.0_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 - fast-deep-equal: 3.1.3 + lodash.merge: 4.6.2 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-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 /@ant-design/react-slick/0.28.3: @@ -480,6 +503,13 @@ packages: regenerator-runtime: 0.13.7 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: resolution: {integrity: sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==} dependencies: @@ -1553,8 +1583,8 @@ packages: webpack-chain: 6.5.1 dev: true - /@umijs/use-params/1.0.3_react@17.0.2: - resolution: {integrity: sha512-gob94yiBsyFNeRKG+zK2nAKxqVsEkYh4vWM3sSyqwidHoUR+C7MmZUHXBBtB1/9C0dpRAi9arkUYvzuNla6q7Q==} + /@umijs/use-params/1.0.6_react@17.0.2: + resolution: {integrity: sha512-w4C73Szn2RdUSeAoUxoJW/Jc0RHJ+MNZ6ywLfkNm2+9CkJ0GmyT5DTJiTtO5J5YZcurNPUN1evUBpGKYOsVSEA==} peerDependencies: react: '*' dependencies: @@ -3271,11 +3301,6 @@ packages: engines: {node: '>= 0.6'} dev: false - /dequal/2.0.2: - resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==} - engines: {node: '>=6'} - dev: true - /des.js/1.0.1: resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==} dependencies: @@ -5740,6 +5765,10 @@ packages: resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=} dev: false + /lodash.merge/4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + /lodash.once/4.1.1: resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=} dev: false @@ -7523,9 +7552,9 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.14.0 + '@babel/runtime': 7.15.4 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-dom: 17.0.2_react@17.0.2 resize-observer-polyfill: 1.5.1 @@ -7789,6 +7818,19 @@ packages: shallowequal: 1.1.0 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: resolution: {integrity: sha512-8FiQLDzm3c/tMX0d62SQtKDhLH7zFlSI6pWBAPt+TUntEqd3Lz9zFAmpvTu8gkvUom/HCsDSZs4wfV4wDPWC0Q==} engines: {node: '>=8.x'} @@ -7964,6 +8006,19 @@ packages: tiny-warning: 1.0.3 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: resolution: {integrity: sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==} peerDependencies: @@ -8098,6 +8153,10 @@ packages: resolution: {integrity: sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==} dev: true + /regenerator-runtime/0.13.9: + resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} + dev: true + /regex-not/1.0.2: resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} engines: {node: '>=0.10.0'} @@ -9035,12 +9094,11 @@ packages: supports-color: 7.2.0 dev: true - /swr/0.5.6_react@17.0.2: - resolution: {integrity: sha512-Bmx3L4geMZjYT5S2Z6EE6/5Cx6v1Ka0LhqZKq8d6WL2eu9y6gHWz3dUzfIK/ymZVHVfwT/EweFXiYGgfifei3w==} + /swr/1.1.0-beta.3_react@17.0.2: + resolution: {integrity: sha512-1fR0VBmSewUMHtICnWg2Q6tzyV9tbL2b5MLEodBCO4AT50k51WYNLsDEzqu801vD6Sdhhqh+fmkubuRkOayYvQ==} peerDependencies: - react: ^16.11.0 || ^17.0.0 + react: ^16.11.0 || ^17.0.0 || ^18.0.0 dependencies: - dequal: 2.0.2 react: 17.0.2 dev: true @@ -9498,16 +9556,16 @@ packages: querystring: 0.2.0 dev: true - /use-json-comparison/1.0.5_react@17.0.2: - resolution: {integrity: sha512-P/AgEKXphcN0L/8G5wLfbEz88mZQ9ayHS1OVESZaS2nxkN/msDWTGE8E1e9HXOWCZ9yoAfDrbVKAmTYpsYupFA==} + /use-json-comparison/1.0.6_react@17.0.2: + resolution: {integrity: sha512-xPadt5yMRbEmVfOSGFSMqjjICrq7nLbfSH3rYIXsrtcuFX7PmbYDN/ku8ObBn3v8o/yZelO1OxUS5+5TI3+fUw==} peerDependencies: - react: 16.x + react: '>=16.9.0' dependencies: react: 17.0.2 dev: true - /use-media-antd-query/1.0.7_react@17.0.2: - resolution: {integrity: sha512-vxO+ThMiuHevxrdbNfcs8NCQ2GXy6b4n8YS273fKxLMEu4P3fmEm5cPdSdN2qh07uMclk5R6AHHhjga31I0pDg==} + /use-media-antd-query/1.1.0_react@17.0.2: + resolution: {integrity: sha512-B6kKZwNV4R+l4Rl11sWO7HqOay9alzs1Vp1b4YJqjz33YxbltBCZtt/yxXxkXN9rc1S7OeEL/GbwC30Wmqhw6Q==} peerDependencies: react: '>=16.9.0' dependencies: diff --git a/src/components/pageLoading.tsx b/src/components/pageLoading.tsx index 06ee9b93..665b114b 100644 --- a/src/components/pageLoading.tsx +++ b/src/components/pageLoading.tsx @@ -1,11 +1,7 @@ import { PageLoading } from '@ant-design/pro-layout'; const NewPageLoading = () => { - return ( -
- -
- ); + return ; }; export default NewPageLoading; diff --git a/src/layouts/defaultProps.tsx b/src/layouts/defaultProps.tsx index 2ed49178..20a103fe 100644 --- a/src/layouts/defaultProps.tsx +++ b/src/layouts/defaultProps.tsx @@ -18,6 +18,12 @@ export default { hideInMenu: true, component: '@/pages/login/index', }, + { + name: '初始化', + path: '/initialization', + hideInMenu: true, + component: '@/pages/initialization/index', + }, { path: '/crontab', name: '定时任务', diff --git a/src/layouts/index.less b/src/layouts/index.less index a7827364..3f8dd711 100644 --- a/src/layouts/index.less +++ b/src/layouts/index.less @@ -247,4 +247,8 @@ ::placeholder { opacity: 0.5 !important; } + + .ant-select-selection-placeholder { + opacity: 0.5 !important; + } } diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index f099de70..e06f8d03 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -22,6 +22,7 @@ export default function (props: any) { const theme = useTheme(); const [user, setUser] = useState(); const [loading, setLoading] = useState(true); + const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); const logout = () => { 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) => { needLoading && setLoading(true); request.get(`${config.apiPrefix}user`).then(({ code, data }) => { @@ -63,19 +80,21 @@ export default function (props: any) { }; useEffect(() => { - const isAuth = localStorage.getItem(config.authKey); - if (!isAuth) { - history.push('/login'); - } vhCheck(); }, []); useEffect(() => { - if (!user) { + if (systemInfo && systemInfo.isInitialized && !user) { getUser(); } }, [props.location.pathname]); + useEffect(() => { + if (!systemInfo) { + getSystemInfo(); + } + }, [systemInfo]); + useEffect(() => { setTheme(); }, [theme.theme]); @@ -92,8 +111,13 @@ export default function (props: any) { } }, []); - if (props.location.pathname === '/login') { - return props.children; + if (['/login', '/initialization'].includes(props.location.pathname)) { + document.title = `${ + (config.documentTitleMap as any)[props.location.pathname] + } - 控制面板`; + if (systemInfo) { + return props.children; + } } const isFirefox = navigator.userAgent.includes('Firefox'); @@ -148,7 +172,7 @@ export default function (props: any) { ]; }} pageTitleRender={(props, pageName, info) => { - if (info) { + if (info && typeof info.pageName === 'string') { return `${info.pageName} - 控制面板`; } return '控制面板'; diff --git a/src/pages/initialization/index.less b/src/pages/initialization/index.less new file mode 100644 index 00000000..4505d773 --- /dev/null +++ b/src/pages/initialization/index.less @@ -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; +} diff --git a/src/pages/initialization/index.tsx b/src/pages/initialization/index.tsx new file mode 100644 index 00000000..34d12793 --- /dev/null +++ b/src/pages/initialization/index.tsx @@ -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([]); + + 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: ( +
+
+ 欢迎使用青龙控制面板 +
+
+ +
+
+ ), + }, + { + title: '账户设置', + content: ( +
+ + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('您输入的两个密码不匹配!')); + }, + }), + ]} + > + + + +
+ ), + }, + { + title: '通知设置', + content: ( +
+ + + + {fields.map((x) => ( + + + + ))} + + +
+ ), + }, + { + title: '完成安装', + content: ( +
+
+ 恭喜安装完成! +
+
+ +
+
+ ), + }, + ]; + + return ( +
+
+
+ logo + 初始化配置 +
+
+
+ + {steps.map((item) => ( + + ))} + +
+ {steps[current].content} +
+
+
+ ); +}; + +export default Login; diff --git a/src/pages/login/index.less b/src/pages/login/index.less index 7f44cac0..cbe653a9 100644 --- a/src/pages/login/index.less +++ b/src/pages/login/index.less @@ -3,29 +3,12 @@ .container { display: flex; flex-direction: column; + align-items: center; height: 100vh; height: calc(100vh - var(--vh-offset, 0px)); overflow: auto; background: @layout-body-background; -} - -.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; + padding-top: 70px; } @media (min-width: @screen-md-min) { @@ -70,6 +53,7 @@ border-radius: 6px; background-color: #f6f8fa; border: 1px solid #ebedef; + width: 340px; @media screen and (max-width: @screen-sm) { width: 95%; diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index bcf9e8e8..35b06d4f 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -122,97 +122,89 @@ const Login = () => { return (
-
-
-
- logo - - {twoFactor ? '两步验证' : config.siteName} - +
+
+ logo + + {twoFactor ? '两步验证' : config.siteName} + +
+
+
+ {twoFactor ? ( +
+ + + + +
+ ) : ( +
+ + + + + + + + {waitTime ? ( + + ) : ( + + )} + +
+ )} +
+
+ {twoFactor ? ( +
+ + 在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。
-
-
- {twoFactor ? ( -
- - - - -
- ) : ( -
- - - - - - - - {waitTime ? ( - - ) : ( - - )} - -
- )} -
-
- {twoFactor ? ( -
- - 在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。 -
- ) : ( - '' - )} -
+ ) : ( + '' + )}
); diff --git a/src/pages/setting/security.tsx b/src/pages/setting/security.tsx index 06455ea0..84445142 100644 --- a/src/pages/setting/security.tsx +++ b/src/pages/setting/security.tsx @@ -17,7 +17,7 @@ const SecuritySettings = ({ user, userChange }: any) => { const handleOk = (values: any) => { request - .post(`${config.apiPrefix}user`, { + .put(`${config.apiPrefix}user`, { data: { username: values.username, password: values.password, diff --git a/src/utils/config.ts b/src/utils/config.ts index f7886b63..054cc68c 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -174,4 +174,15 @@ export default { { label: 'emailPass', tip: '邮箱SMTP授权码', required: true }, ], }, + documentTitleMap: { + '/login': '登录', + '/initialization': '初始化', + '/cron': '定时任务', + '/env': '环境变量', + '/config': '配置文件', + '/script': '脚本管理', + '/diff': '对比工具', + '/log': '任务日志', + '/setting': '系统设置', + }, };