mirror of
				https://github.com/whyour/qinglong.git
				synced 2025-10-30 16:36:07 +08:00 
			
		
		
		
	添加openapi模块
This commit is contained in:
		
							parent
							
								
									7739cef7b8
								
							
						
					
					
						commit
						1e58254f4c
					
				|  | @ -5,6 +5,7 @@ import config from './config'; | |||
| import log from './log'; | ||||
| import cron from './cron'; | ||||
| import script from './script'; | ||||
| import open from './open'; | ||||
| 
 | ||||
| export default () => { | ||||
|   const app = Router(); | ||||
|  | @ -14,6 +15,7 @@ export default () => { | |||
|   log(app); | ||||
|   cron(app); | ||||
|   script(app); | ||||
|   open(app); | ||||
| 
 | ||||
|   return app; | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										126
									
								
								back/api/open.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								back/api/open.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,126 @@ | |||
| import { Router, Request, Response, NextFunction } from 'express'; | ||||
| import { Container } from 'typedi'; | ||||
| import OpenService from '../services/open'; | ||||
| import { Logger } from 'winston'; | ||||
| import { celebrate, Joi } from 'celebrate'; | ||||
| const route = Router(); | ||||
| 
 | ||||
| export default (app: Router) => { | ||||
|   app.use('/', route); | ||||
|   route.get( | ||||
|     '/apps', | ||||
|     async (req: Request, res: Response, next: NextFunction) => { | ||||
|       const logger: Logger = Container.get('logger'); | ||||
|       try { | ||||
|         const openService = Container.get(OpenService); | ||||
|         const data = await openService.list(); | ||||
|         return res.send({ code: 200, data }); | ||||
|       } catch (e) { | ||||
|         logger.error('🔥 error: %o', e); | ||||
|         return next(e); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   route.post( | ||||
|     '/apps', | ||||
|     celebrate({ | ||||
|       body: Joi.object({ | ||||
|         name: Joi.string().required(), | ||||
|         scopes: Joi.array().items(Joi.string().required()), | ||||
|       }), | ||||
|     }), | ||||
|     async (req: Request, res: Response, next: NextFunction) => { | ||||
|       const logger: Logger = Container.get('logger'); | ||||
|       try { | ||||
|         const openService = Container.get(OpenService); | ||||
|         const data = await openService.create(req.body); | ||||
|         return res.send({ code: 200, data }); | ||||
|       } catch (e) { | ||||
|         logger.error('🔥 error: %o', e); | ||||
|         return next(e); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   route.put( | ||||
|     '/apps', | ||||
|     celebrate({ | ||||
|       body: Joi.object({ | ||||
|         name: Joi.string().required(), | ||||
|         scopes: Joi.array().items(Joi.string()), | ||||
|         _id: Joi.string().required(), | ||||
|       }), | ||||
|     }), | ||||
|     async (req: Request, res: Response, next: NextFunction) => { | ||||
|       const logger: Logger = Container.get('logger'); | ||||
|       try { | ||||
|         const openService = Container.get(OpenService); | ||||
|         const data = await openService.update(req.body); | ||||
|         return res.send({ code: 200, data }); | ||||
|       } catch (e) { | ||||
|         logger.error('🔥 error: %o', e); | ||||
|         return next(e); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   route.delete( | ||||
|     '/apps', | ||||
|     celebrate({ | ||||
|       body: Joi.array().items(Joi.string().required()), | ||||
|     }), | ||||
|     async (req: Request, res: Response, next: NextFunction) => { | ||||
|       const logger: Logger = Container.get('logger'); | ||||
|       try { | ||||
|         const openService = Container.get(OpenService); | ||||
|         const data = await openService.remove(req.body); | ||||
|         return res.send({ code: 200, data }); | ||||
|       } catch (e) { | ||||
|         logger.error('🔥 error: %o', e); | ||||
|         return next(e); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   route.put( | ||||
|     '/apps/:id/reset-secret', | ||||
|     celebrate({ | ||||
|       params: Joi.object({ | ||||
|         id: Joi.string().required(), | ||||
|       }), | ||||
|     }), | ||||
|     async (req: Request, res: Response, next: NextFunction) => { | ||||
|       const logger: Logger = Container.get('logger'); | ||||
|       try { | ||||
|         const openService = Container.get(OpenService); | ||||
|         const data = await openService.resetSecret(req.params.id); | ||||
|         return res.send({ code: 200, data }); | ||||
|       } catch (e) { | ||||
|         logger.error('🔥 error: %o', e); | ||||
|         return next(e); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   route.get( | ||||
|     '/auth/token', | ||||
|     celebrate({ | ||||
|       query: { | ||||
|         client_id: Joi.string().required(), | ||||
|         client_secret: Joi.string().required(), | ||||
|       }, | ||||
|     }), | ||||
|     async (req: Request, res: Response, next: NextFunction) => { | ||||
|       const logger: Logger = Container.get('logger'); | ||||
|       try { | ||||
|         const openService = Container.get(OpenService); | ||||
|         const result = await openService.authToken(req.query as any); | ||||
|         return res.send(result); | ||||
|       } catch (e) { | ||||
|         logger.error('🔥 error: %o', e); | ||||
|         return next(e); | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
| }; | ||||
|  | @ -23,6 +23,8 @@ const configString = 'config sample crontab shareCode diy'; | |||
| const dbPath = path.join(rootPath, 'db/'); | ||||
| const cronDbFile = path.join(rootPath, 'db/crontab.db'); | ||||
| const envDbFile = path.join(rootPath, 'db/env.db'); | ||||
| const appDbFile = path.join(rootPath, 'db/app.db'); | ||||
| 
 | ||||
| const configFound = dotenv.config({ path: confFile }); | ||||
| 
 | ||||
| if (envFound.error) { | ||||
|  | @ -57,6 +59,7 @@ export default { | |||
|   dbPath, | ||||
|   cronDbFile, | ||||
|   envDbFile, | ||||
|   appDbFile, | ||||
|   configPath, | ||||
|   scriptPath, | ||||
|   samplePath, | ||||
|  | @ -67,10 +70,5 @@ export default { | |||
|     'crontab.list', | ||||
|     'env.sh', | ||||
|   ], | ||||
|   writePathList: [ | ||||
|     '/ql/scripts/', | ||||
|     '/ql/config/', | ||||
|     '/ql/jbot/', | ||||
|     '/ql/bak/', | ||||
|   ], | ||||
|   writePathList: ['/ql/scripts/', '/ql/config/', '/ql/jbot/', '/ql/bak/'], | ||||
| }; | ||||
|  |  | |||
|  | @ -86,7 +86,7 @@ export function createRandomString(min: number, max: number): string { | |||
|     'Y', | ||||
|     'Z', | ||||
|   ]; | ||||
|   const special = ['-', '_', '#']; | ||||
|   const special = ['-', '_']; | ||||
|   const config = num.concat(english).concat(ENGLISH).concat(special); | ||||
| 
 | ||||
|   const arr = []; | ||||
|  |  | |||
							
								
								
									
										31
									
								
								back/data/open.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								back/data/open.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| export class App { | ||||
|   name: string; | ||||
|   scopes: AppScope[]; | ||||
|   client_id: string; | ||||
|   client_secret: string; | ||||
|   tokens?: AppToken[]; | ||||
|   _id?: string; | ||||
| 
 | ||||
|   constructor(options: App) { | ||||
|     this.name = options.name; | ||||
|     this.scopes = options.scopes; | ||||
|     this.client_id = options.client_id; | ||||
|     this.client_secret = options.client_secret; | ||||
|     this._id = options._id; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export interface AppToken { | ||||
|   value: string; | ||||
|   type: 'Bearer'; | ||||
|   expiration: number; | ||||
| } | ||||
| 
 | ||||
| export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs'; | ||||
| 
 | ||||
| export enum CrontabStatus { | ||||
|   'running', | ||||
|   'idle', | ||||
|   'disabled', | ||||
|   'queued', | ||||
| } | ||||
|  | @ -6,6 +6,9 @@ import config from '../config'; | |||
| import jwt from 'express-jwt'; | ||||
| import fs from 'fs'; | ||||
| import { getToken } from '../config/util'; | ||||
| import Container from 'typedi'; | ||||
| import OpenService from '../services/open'; | ||||
| import rewrite from 'express-urlrewrite'; | ||||
| 
 | ||||
| export default ({ app }: { app: Application }) => { | ||||
|   app.enable('trust proxy'); | ||||
|  | @ -15,19 +18,42 @@ export default ({ app }: { app: Application }) => { | |||
|   app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); | ||||
|   app.use( | ||||
|     jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({ | ||||
|       path: ['/api/login', '/api/crons/status'], | ||||
|       path: ['/api/login', '/api/crons/status', /^\/open\//], | ||||
|     }), | ||||
|   ); | ||||
|   app.use((req, res, next) => { | ||||
|     const data = fs.readFileSync(config.authConfigFile, 'utf8'); | ||||
| 
 | ||||
|   app.use(async (req, res, next) => { | ||||
|     const headerToken = getToken(req); | ||||
|     if (req.path.startsWith('/open/')) { | ||||
|       const openService = Container.get(OpenService); | ||||
|       const doc = await openService.findTokenByValue(headerToken); | ||||
|       if (doc && doc.tokens.length > 0) { | ||||
|         const currentToken = doc.tokens.find((x) => x.value === headerToken); | ||||
|         const key = | ||||
|           req.path.match(/\/open\/([a-z]+)\/*/) && | ||||
|           req.path.match(/\/open\/([a-z]+)\/*/)[1]; | ||||
|         if ( | ||||
|           doc.scopes.includes(key as any) && | ||||
|           currentToken && | ||||
|           currentToken.expiration >= Math.round(Date.now() / 1000) | ||||
|         ) { | ||||
|           return next(); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const data = fs.readFileSync(config.authConfigFile, 'utf8'); | ||||
|     if (data) { | ||||
|       const { token } = JSON.parse(data); | ||||
|       if (token && headerToken === token) { | ||||
|         return next(); | ||||
|       } | ||||
|     } | ||||
|     if (!headerToken && req.path && req.path === '/api/login') { | ||||
|     if ( | ||||
|       !headerToken && | ||||
|       req.path && | ||||
|       (req.path === '/api/login' || req.path === '/open/auth/token') | ||||
|     ) { | ||||
|       return next(); | ||||
|     } | ||||
|     const remoteAddress = req.socket.remoteAddress; | ||||
|  | @ -37,10 +63,13 @@ export default ({ app }: { app: Application }) => { | |||
|     ) { | ||||
|       return next(); | ||||
|     } | ||||
| 
 | ||||
|     const err: any = new Error('UnauthorizedError'); | ||||
|     err['status'] = 401; | ||||
|     err.status = 401; | ||||
|     next(err); | ||||
|   }); | ||||
| 
 | ||||
|   app.use(rewrite('/open/*', '/api/$1')); | ||||
|   app.use(config.api.prefix, routes()); | ||||
| 
 | ||||
|   app.use((req, res, next) => { | ||||
|  |  | |||
							
								
								
									
										171
									
								
								back/services/open.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								back/services/open.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,171 @@ | |||
| import { Service, Inject } from 'typedi'; | ||||
| import winston from 'winston'; | ||||
| import { createRandomString } from '../config/util'; | ||||
| import config from '../config'; | ||||
| import DataStore from 'nedb'; | ||||
| import { App } from '../data/open'; | ||||
| import { v4 as uuidV4 } from 'uuid'; | ||||
| 
 | ||||
| @Service() | ||||
| export default class OpenService { | ||||
|   private appDb = new DataStore({ filename: config.appDbFile }); | ||||
|   constructor(@Inject('logger') private logger: winston.Logger) { | ||||
|     this.appDb.loadDatabase((err) => { | ||||
|       if (err) throw err; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public getDb(): DataStore { | ||||
|     return this.appDb; | ||||
|   } | ||||
| 
 | ||||
|   public async findTokenByValue(token: string): Promise<App> { | ||||
|     return new Promise((resolve) => { | ||||
|       this.appDb.find( | ||||
|         { tokens: { $elemMatch: { value: token } } }, | ||||
|         (err, docs) => { | ||||
|           if (err) { | ||||
|             this.logger.error(err); | ||||
|           } else { | ||||
|             resolve(docs[0]); | ||||
|           } | ||||
|         }, | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public async create(payload: App): Promise<App> { | ||||
|     const tab = new App({ ...payload }); | ||||
|     tab.client_id = createRandomString(12, 12); | ||||
|     tab.client_secret = createRandomString(24, 24); | ||||
|     const docs = await this.insert([tab]); | ||||
|     return { ...docs[0], tokens: [] }; | ||||
|   } | ||||
| 
 | ||||
|   public async insert(payloads: App[]): Promise<App[]> { | ||||
|     return new Promise((resolve) => { | ||||
|       this.appDb.insert(payloads, (err, docs) => { | ||||
|         if (err) { | ||||
|           this.logger.error(err); | ||||
|         } else { | ||||
|           resolve(docs); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public async update(payload: App): Promise<App> { | ||||
|     const { _id, client_id, client_secret, tokens, ...other } = payload; | ||||
|     const doc = await this.get(_id); | ||||
|     const tab = new App({ ...doc, ...other }); | ||||
|     const newDoc = await this.updateDb(tab); | ||||
|     return { ...newDoc, tokens: [] }; | ||||
|   } | ||||
| 
 | ||||
|   private async updateDb(payload: App): Promise<App> { | ||||
|     return new Promise((resolve) => { | ||||
|       this.appDb.update( | ||||
|         { _id: payload._id }, | ||||
|         payload, | ||||
|         { returnUpdatedDocs: true }, | ||||
|         (err, num, doc, up) => { | ||||
|           if (err) { | ||||
|             this.logger.error(err); | ||||
|           } else { | ||||
|             resolve(doc); | ||||
|           } | ||||
|         }, | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public async remove(ids: string[]) { | ||||
|     return new Promise((resolve: any) => { | ||||
|       this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => { | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public async resetSecret(_id: string): Promise<App> { | ||||
|     const doc = await this.get(_id); | ||||
|     const tab = new App({ ...doc }); | ||||
|     tab.client_secret = createRandomString(24, 24); | ||||
|     tab.tokens = []; | ||||
|     const newDoc = await this.updateDb(tab); | ||||
|     return newDoc; | ||||
|   } | ||||
| 
 | ||||
|   public async list( | ||||
|     searchText: string = '', | ||||
|     sort: any = {}, | ||||
|     query: any = {}, | ||||
|   ): Promise<App[]> { | ||||
|     let condition = { ...query }; | ||||
|     if (searchText) { | ||||
|       const reg = new RegExp(searchText); | ||||
|       condition = { | ||||
|         $or: [ | ||||
|           { | ||||
|             value: reg, | ||||
|           }, | ||||
|           { | ||||
|             name: reg, | ||||
|           }, | ||||
|           { | ||||
|             remarks: reg, | ||||
|           }, | ||||
|         ], | ||||
|       }; | ||||
|     } | ||||
|     const newDocs = await this.find(condition, sort); | ||||
|     return newDocs.map((x) => ({ ...x, tokens: [] })); | ||||
|   } | ||||
| 
 | ||||
|   private async find(query: any, sort: any): Promise<App[]> { | ||||
|     return new Promise((resolve) => { | ||||
|       this.appDb | ||||
|         .find(query) | ||||
|         .sort({ ...sort }) | ||||
|         .exec((err, docs) => { | ||||
|           resolve(docs); | ||||
|         }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public async get(_id: string): Promise<App> { | ||||
|     return new Promise((resolve) => { | ||||
|       this.appDb.find({ _id }).exec((err, docs) => { | ||||
|         resolve(docs[0]); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   public async authToken({ client_id, client_secret }): Promise<any> { | ||||
|     const token = uuidV4(); | ||||
|     const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
 | ||||
|     return new Promise((resolve) => { | ||||
|       this.appDb.find({ client_id, client_secret }).exec((err, docs) => { | ||||
|         if (docs && docs[0]) { | ||||
|           this.appDb.update( | ||||
|             { client_id, client_secret }, | ||||
|             { $push: { tokens: { value: token, expiration } } }, | ||||
|             {}, | ||||
|             (err, num, doc) => { | ||||
|               resolve({ | ||||
|                 code: 200, | ||||
|                 data: { | ||||
|                   token, | ||||
|                   token_type: 'Bearer', | ||||
|                   expiration, | ||||
|                 }, | ||||
|               }); | ||||
|             }, | ||||
|           ); | ||||
|         } else { | ||||
|           resolve({ code: 400, message: 'client_id或client_seret有误' }); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | @ -31,6 +31,7 @@ | |||
|     "dotenv": "^8.2.0", | ||||
|     "express": "^4.17.1", | ||||
|     "express-jwt": "^6.0.0", | ||||
|     "express-urlrewrite": "^1.4.0", | ||||
|     "got": "^11.8.2", | ||||
|     "jsonwebtoken": "^8.5.1", | ||||
|     "lodash": "^4.17.21", | ||||
|  | @ -40,6 +41,7 @@ | |||
|     "p-queue": "6.6.2", | ||||
|     "reflect-metadata": "^0.1.13", | ||||
|     "typedi": "^0.8.0", | ||||
|     "uuid": "^8.3.2", | ||||
|     "winston": "^3.3.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|  | @ -69,10 +71,10 @@ | |||
|     "react": "17.x", | ||||
|     "react-codemirror2": "^7.2.1", | ||||
|     "react-diff-viewer": "^3.1.1", | ||||
|     "react-split-pane": "^0.1.92", | ||||
|     "react-dnd": "^14.0.2", | ||||
|     "react-dnd-html5-backend": "^14.0.0", | ||||
|     "react-dom": "17.x", | ||||
|     "react-split-pane": "^0.1.92", | ||||
|     "ts-node": "^9.0.0", | ||||
|     "typescript": "^4.1.2", | ||||
|     "umi": "^3.3.9", | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ specifiers: | |||
|   '@types/react-dom': ^17.0.0 | ||||
|   '@umijs/plugin-antd': ^0.9.1 | ||||
|   '@umijs/test': ^3.3.9 | ||||
|   axios: ^0.21.1 | ||||
|   body-parser: ^1.19.0 | ||||
|   celebrate: ^13.0.3 | ||||
|   codemirror: ^5.62.2 | ||||
|  | @ -28,6 +27,7 @@ specifiers: | |||
|   dotenv: ^8.2.0 | ||||
|   express: ^4.17.1 | ||||
|   express-jwt: ^6.0.0 | ||||
|   express-urlrewrite: ^1.4.0 | ||||
|   got: ^11.8.2 | ||||
|   jsonwebtoken: ^8.5.1 | ||||
|   lint-staged: ^10.0.7 | ||||
|  | @ -52,13 +52,13 @@ specifiers: | |||
|   typescript: ^4.1.2 | ||||
|   umi: ^3.3.9 | ||||
|   umi-request: ^1.3.5 | ||||
|   uuid: ^8.3.2 | ||||
|   vh-check: ^2.0.5 | ||||
|   webpack: ^5.28.0 | ||||
|   winston: ^3.3.3 | ||||
|   yorkie: ^2.0.0 | ||||
| 
 | ||||
| dependencies: | ||||
|   axios: 0.21.1 | ||||
|   body-parser: 1.19.0 | ||||
|   celebrate: 13.0.4 | ||||
|   cors: 2.8.5 | ||||
|  | @ -66,6 +66,7 @@ dependencies: | |||
|   dotenv: 8.6.0 | ||||
|   express: 4.17.1 | ||||
|   express-jwt: 6.0.0 | ||||
|   express-urlrewrite: 1.4.0 | ||||
|   got: 11.8.2 | ||||
|   jsonwebtoken: 8.5.1 | ||||
|   lodash: 4.17.21 | ||||
|  | @ -75,6 +76,7 @@ dependencies: | |||
|   p-queue: 6.6.2 | ||||
|   reflect-metadata: 0.1.13 | ||||
|   typedi: 0.8.0 | ||||
|   uuid: 8.3.2 | ||||
|   winston: 3.3.3 | ||||
| 
 | ||||
| devDependencies: | ||||
|  | @ -2008,14 +2010,6 @@ packages: | |||
|     resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==} | ||||
|     dev: true | ||||
| 
 | ||||
|   /axios/0.21.1: | ||||
|     resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==} | ||||
|     dependencies: | ||||
|       follow-redirects: 1.14.2 | ||||
|     transitivePeerDependencies: | ||||
|       - debug | ||||
|     dev: false | ||||
| 
 | ||||
|   /babel-core/7.0.0-bridge.0_@babel+core@7.12.10: | ||||
|     resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} | ||||
|     peerDependencies: | ||||
|  | @ -3122,6 +3116,18 @@ packages: | |||
|       ms: 2.1.2 | ||||
|     dev: true | ||||
| 
 | ||||
|   /debug/4.3.2: | ||||
|     resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} | ||||
|     engines: {node: '>=6.0'} | ||||
|     peerDependencies: | ||||
|       supports-color: '*' | ||||
|     peerDependenciesMeta: | ||||
|       supports-color: | ||||
|         optional: true | ||||
|     dependencies: | ||||
|       ms: 2.1.2 | ||||
|     dev: false | ||||
| 
 | ||||
|   /decamelize/1.2.0: | ||||
|     resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} | ||||
|     engines: {node: '>=0.10.0'} | ||||
|  | @ -3643,6 +3649,15 @@ packages: | |||
|     resolution: {integrity: sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=} | ||||
|     dev: false | ||||
| 
 | ||||
|   /express-urlrewrite/1.4.0: | ||||
|     resolution: {integrity: sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA==} | ||||
|     dependencies: | ||||
|       debug: 4.3.2 | ||||
|       path-to-regexp: 1.8.0 | ||||
|     transitivePeerDependencies: | ||||
|       - supports-color | ||||
|     dev: false | ||||
| 
 | ||||
|   /express/4.17.1: | ||||
|     resolution: {integrity: sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==} | ||||
|     engines: {node: '>= 0.10.0'} | ||||
|  | @ -3838,16 +3853,6 @@ packages: | |||
|     resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} | ||||
|     dev: false | ||||
| 
 | ||||
|   /follow-redirects/1.14.2: | ||||
|     resolution: {integrity: sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==} | ||||
|     engines: {node: '>=4.0'} | ||||
|     peerDependencies: | ||||
|       debug: '*' | ||||
|     peerDependenciesMeta: | ||||
|       debug: | ||||
|         optional: true | ||||
|     dev: false | ||||
| 
 | ||||
|   /for-each/0.3.3: | ||||
|     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} | ||||
|     dependencies: | ||||
|  | @ -4687,7 +4692,6 @@ packages: | |||
| 
 | ||||
|   /isarray/0.0.1: | ||||
|     resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=} | ||||
|     dev: true | ||||
| 
 | ||||
|   /isarray/1.0.0: | ||||
|     resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} | ||||
|  | @ -6003,7 +6007,6 @@ packages: | |||
| 
 | ||||
|   /ms/2.1.2: | ||||
|     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} | ||||
|     dev: true | ||||
| 
 | ||||
|   /ms/2.1.3: | ||||
|     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} | ||||
|  | @ -6518,7 +6521,6 @@ packages: | |||
|     resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} | ||||
|     dependencies: | ||||
|       isarray: 0.0.1 | ||||
|     dev: true | ||||
| 
 | ||||
|   /path-to-regexp/2.4.0: | ||||
|     resolution: {integrity: sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==} | ||||
|  | @ -9506,8 +9508,6 @@ packages: | |||
|   /uuid/8.3.2: | ||||
|     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} | ||||
|     hasBin: true | ||||
|     dev: true | ||||
|     optional: true | ||||
| 
 | ||||
|   /v8-compile-cache/2.3.0: | ||||
|     resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} | ||||
|  |  | |||
							
								
								
									
										80
									
								
								src/pages/setting/appModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/pages/setting/appModal.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | |||
| import React, { useEffect, useState } from 'react'; | ||||
| import { Modal, message, Input, Form, Select } from 'antd'; | ||||
| import { request } from '@/utils/http'; | ||||
| import config from '@/utils/config'; | ||||
| 
 | ||||
| const AppModal = ({ | ||||
|   app, | ||||
|   handleCancel, | ||||
|   visible, | ||||
| }: { | ||||
|   app?: any; | ||||
|   visible: boolean; | ||||
|   handleCancel: (needUpdate?: boolean) => void; | ||||
| }) => { | ||||
|   const [form] = Form.useForm(); | ||||
|   const [loading, setLoading] = useState(false); | ||||
| 
 | ||||
|   const handleOk = async (values: any) => { | ||||
|     setLoading(true); | ||||
|     const method = app ? 'put' : 'post'; | ||||
|     const payload = { ...values }; | ||||
|     if (app) { | ||||
|       payload._id = app._id; | ||||
|     } | ||||
|     const { code, data } = await request[method](`${config.apiPrefix}apps`, { | ||||
|       data: payload, | ||||
|     }); | ||||
|     if (code === 200) { | ||||
|       message.success(app ? '更新应用成功' : '添加应用成功'); | ||||
|     } else { | ||||
|       message.error(data); | ||||
|     } | ||||
|     setLoading(false); | ||||
|     handleCancel(data); | ||||
|   }; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     form.resetFields(); | ||||
|   }, [app, visible]); | ||||
| 
 | ||||
|   return ( | ||||
|     <Modal | ||||
|       title={app ? '编辑应用' : '新建应用'} | ||||
|       visible={visible} | ||||
|       forceRender | ||||
|       onOk={() => { | ||||
|         form | ||||
|           .validateFields() | ||||
|           .then((values) => { | ||||
|             handleOk(values); | ||||
|           }) | ||||
|           .catch((info) => { | ||||
|             console.log('Validate Failed:', info); | ||||
|           }); | ||||
|       }} | ||||
|       onCancel={() => handleCancel()} | ||||
|       confirmLoading={loading} | ||||
|     > | ||||
|       <Form | ||||
|         form={form} | ||||
|         layout="vertical" | ||||
|         name="form_app_modal" | ||||
|         initialValues={app} | ||||
|       > | ||||
|         <Form.Item name="name" label="名称"> | ||||
|           <Input placeholder="请输入应用名称" /> | ||||
|         </Form.Item> | ||||
|         <Form.Item name="scopes" label="权限" rules={[{ required: true }]}> | ||||
|           <Select mode="multiple" allowClear style={{ width: '100%' }}> | ||||
|             {config.scopes.map((x) => { | ||||
|               return <Select.Option value={x.value}>{x.name}</Select.Option>; | ||||
|             })} | ||||
|           </Select> | ||||
|         </Form.Item> | ||||
|       </Form> | ||||
|     </Modal> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default AppModal; | ||||
|  | @ -1,5 +1,18 @@ | |||
| import React, { useState, useEffect } from 'react'; | ||||
| import { Button, Input, Form, Radio, Tabs } from 'antd'; | ||||
| import { | ||||
|   Button, | ||||
|   Input, | ||||
|   Form, | ||||
|   Radio, | ||||
|   Tabs, | ||||
|   Table, | ||||
|   Tooltip, | ||||
|   Space, | ||||
|   Tag, | ||||
|   Modal, | ||||
|   message, | ||||
|   Typography, | ||||
| } from 'antd'; | ||||
| import config from '@/utils/config'; | ||||
| import { PageContainer } from '@ant-design/pro-layout'; | ||||
| import { request } from '@/utils/http'; | ||||
|  | @ -10,19 +23,87 @@ import { | |||
|   setFetchMethod, | ||||
| } from 'darkreader'; | ||||
| import { history } from 'umi'; | ||||
| import { useCtx } from '@/utils/hooks'; | ||||
| import AppModal from './appModal'; | ||||
| import { | ||||
|   EditOutlined, | ||||
|   DeleteOutlined, | ||||
|   ReloadOutlined, | ||||
| } from '@ant-design/icons'; | ||||
| 
 | ||||
| const { Text } = Typography; | ||||
| const optionsWithDisabled = [ | ||||
|   { label: '亮色', value: 'light' }, | ||||
|   { label: '暗色', value: 'dark' }, | ||||
|   { label: '跟随系统', value: 'auto' }, | ||||
| ]; | ||||
| 
 | ||||
| const Password = ({ headerStyle, isPhone }: any) => { | ||||
|   const [value, setValue] = useState(''); | ||||
| const Setting = ({ headerStyle, isPhone }: any) => { | ||||
|   const columns = [ | ||||
|     { | ||||
|       title: '名称', | ||||
|       dataIndex: 'name', | ||||
|       key: 'name', | ||||
|       align: 'center' as const, | ||||
|     }, | ||||
|     { | ||||
|       title: 'Client ID', | ||||
|       dataIndex: 'client_id', | ||||
|       key: 'client_id', | ||||
|       align: 'center' as const, | ||||
|     }, | ||||
|     { | ||||
|       title: 'Client Secret', | ||||
|       dataIndex: 'client_secret', | ||||
|       key: 'client_secret', | ||||
|       align: 'center' as const, | ||||
|     }, | ||||
|     { | ||||
|       title: '权限', | ||||
|       dataIndex: 'scopes', | ||||
|       key: 'scopes', | ||||
|       align: 'center' as const, | ||||
|       render: (text: string, record: any) => { | ||||
|         return record.scopes.map((scope: any) => { | ||||
|           return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>; | ||||
|         }); | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       key: 'action', | ||||
|       align: 'center' as const, | ||||
|       render: (text: string, record: any, index: number) => { | ||||
|         const isPc = !isPhone; | ||||
|         return ( | ||||
|           <Space size="middle" style={{ paddingLeft: 8 }}> | ||||
|             <Tooltip title={isPc ? '编辑' : ''}> | ||||
|               <a onClick={() => editApp(record, index)}> | ||||
|                 <EditOutlined /> | ||||
|               </a> | ||||
|             </Tooltip> | ||||
|             <Tooltip title={isPc ? '重置secret' : ''}> | ||||
|               <a onClick={() => resetSecret(record, index)}> | ||||
|                 <ReloadOutlined /> | ||||
|               </a> | ||||
|             </Tooltip> | ||||
|             <Tooltip title={isPc ? '删除' : ''}> | ||||
|               <a onClick={() => deleteApp(record, index)}> | ||||
|                 <DeleteOutlined /> | ||||
|               </a> | ||||
|             </Tooltip> | ||||
|           </Space> | ||||
|         ); | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| 
 | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const defaultDarken = localStorage.getItem('qinglong_dark_theme') || 'auto'; | ||||
|   const [theme, setTheme] = useState(defaultDarken); | ||||
|   const [dataSource, setDataSource] = useState<any[]>([]); | ||||
|   const [isModalVisible, setIsModalVisible] = useState(false); | ||||
|   const [editedApp, setEditedApp] = useState(); | ||||
|   const [tabActiveKey, setTabActiveKey] = useState('person'); | ||||
| 
 | ||||
|   const handleOk = (values: any) => { | ||||
|     request | ||||
|  | @ -46,12 +127,116 @@ const Password = ({ headerStyle, isPhone }: any) => { | |||
|     localStorage.setItem('qinglong_dark_theme', e.target.value); | ||||
|   }; | ||||
| 
 | ||||
|   const importJob = () => { | ||||
|     request.get(`${config.apiPrefix}crons/import`).then((data: any) => { | ||||
|       console.log(data); | ||||
|   const getApps = () => { | ||||
|     setLoading(true); | ||||
|     request | ||||
|       .get(`${config.apiPrefix}apps`) | ||||
|       .then((data: any) => { | ||||
|         setDataSource(data.data); | ||||
|       }) | ||||
|       .finally(() => setLoading(false)); | ||||
|   }; | ||||
| 
 | ||||
|   const addApp = () => { | ||||
|     setIsModalVisible(true); | ||||
|   }; | ||||
| 
 | ||||
|   const editApp = (record: any, index: number) => { | ||||
|     setEditedApp(record); | ||||
|     setIsModalVisible(true); | ||||
|   }; | ||||
| 
 | ||||
|   const deleteApp = (record: any, index: number) => { | ||||
|     Modal.confirm({ | ||||
|       title: '确认删除', | ||||
|       content: ( | ||||
|         <> | ||||
|           确认删除应用{' '} | ||||
|           <Text style={{ wordBreak: 'break-all' }} type="warning"> | ||||
|             {record.name} | ||||
|           </Text>{' '} | ||||
|           吗 | ||||
|         </> | ||||
|       ), | ||||
|       onOk() { | ||||
|         request | ||||
|           .delete(`${config.apiPrefix}apps`, { data: [record._id] }) | ||||
|           .then((data: any) => { | ||||
|             if (data.code === 200) { | ||||
|               message.success('删除成功'); | ||||
|               const result = [...dataSource]; | ||||
|               result.splice(index, 1); | ||||
|               setDataSource(result); | ||||
|             } else { | ||||
|               message.error(data); | ||||
|             } | ||||
|           }); | ||||
|       }, | ||||
|       onCancel() { | ||||
|         console.log('Cancel'); | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const resetSecret = (record: any, index: number) => { | ||||
|     Modal.confirm({ | ||||
|       title: '确认重置', | ||||
|       content: ( | ||||
|         <> | ||||
|           确认重置应用{' '} | ||||
|           <Text style={{ wordBreak: 'break-all' }} type="warning"> | ||||
|             {record.name} | ||||
|           </Text>{' '} | ||||
|           的Secret吗 | ||||
|           <br /> | ||||
|           <Text type="secondary">重置Secret会让当前应用所有token失效</Text> | ||||
|         </> | ||||
|       ), | ||||
|       onOk() { | ||||
|         request | ||||
|           .put(`${config.apiPrefix}apps/${record._id}/reset-secret`) | ||||
|           .then((data: any) => { | ||||
|             if (data.code === 200) { | ||||
|               message.success('重置成功'); | ||||
|               handleApp(data.data); | ||||
|             } else { | ||||
|               message.error(data); | ||||
|             } | ||||
|           }); | ||||
|       }, | ||||
|       onCancel() { | ||||
|         console.log('Cancel'); | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const handleCancel = (app?: any) => { | ||||
|     setIsModalVisible(false); | ||||
|     if (app) { | ||||
|       handleApp(app); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleApp = (app: any) => { | ||||
|     const index = dataSource.findIndex((x) => x._id === app._id); | ||||
|     const result = [...dataSource]; | ||||
|     if (index === -1) { | ||||
|       result.push(app); | ||||
|     } else { | ||||
|       result.splice(index, 1, { | ||||
|         ...app, | ||||
|       }); | ||||
|     } | ||||
|     setDataSource(result); | ||||
|   }; | ||||
| 
 | ||||
|   const tabChange = (activeKey: string) => { | ||||
|     setTabActiveKey(activeKey); | ||||
|     if (activeKey === 'app') { | ||||
|       getApps(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setFetchMethod(window.fetch); | ||||
|     if (theme === 'dark') { | ||||
|  | @ -70,8 +255,22 @@ const Password = ({ headerStyle, isPhone }: any) => { | |||
|       header={{ | ||||
|         style: headerStyle, | ||||
|       }} | ||||
|       extra={ | ||||
|         tabActiveKey === 'app' | ||||
|           ? [ | ||||
|               <Button key="2" type="primary" onClick={() => addApp()}> | ||||
|                 添加应用 | ||||
|               </Button>, | ||||
|             ] | ||||
|           : [] | ||||
|       } | ||||
|     > | ||||
|       <Tabs defaultActiveKey="person" size="small" tabPosition="top"> | ||||
|       <Tabs | ||||
|         defaultActiveKey="person" | ||||
|         size="small" | ||||
|         tabPosition="top" | ||||
|         onChange={tabChange} | ||||
|       > | ||||
|         <Tabs.TabPane tab="个人设置" key="person"> | ||||
|           <Form onFinish={handleOk} layout="vertical"> | ||||
|             <Form.Item | ||||
|  | @ -97,6 +296,17 @@ const Password = ({ headerStyle, isPhone }: any) => { | |||
|             </Button> | ||||
|           </Form> | ||||
|         </Tabs.TabPane> | ||||
|         <Tabs.TabPane tab="应用设置" key="app"> | ||||
|           <Table | ||||
|             columns={columns} | ||||
|             pagination={false} | ||||
|             dataSource={dataSource} | ||||
|             rowKey="_id" | ||||
|             size="middle" | ||||
|             scroll={{ x: 768 }} | ||||
|             loading={loading} | ||||
|           /> | ||||
|         </Tabs.TabPane> | ||||
|         <Tabs.TabPane tab="其他设置" key="theme"> | ||||
|           <Form layout="vertical"> | ||||
|             <Form.Item label="主题设置" name="theme" initialValue={theme}> | ||||
|  | @ -111,8 +321,13 @@ const Password = ({ headerStyle, isPhone }: any) => { | |||
|           </Form> | ||||
|         </Tabs.TabPane> | ||||
|       </Tabs> | ||||
|       <AppModal | ||||
|         visible={isModalVisible} | ||||
|         handleCancel={handleCancel} | ||||
|         app={editedApp} | ||||
|       /> | ||||
|     </PageContainer> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Password; | ||||
| export default Setting; | ||||
|  |  | |||
|  | @ -34,4 +34,33 @@ export default { | |||
|     ], | ||||
|     defaultLanguage: 'en', | ||||
|   }, | ||||
|   scopes: [ | ||||
|     { | ||||
|       name: '定时任务', | ||||
|       value: 'crons', | ||||
|     }, | ||||
|     { | ||||
|       name: '环境变量', | ||||
|       value: 'envs', | ||||
|     }, | ||||
|     { | ||||
|       name: '配置文件', | ||||
|       value: 'configs', | ||||
|     }, | ||||
|     { | ||||
|       name: '脚本管理', | ||||
|       value: 'scripts', | ||||
|     }, | ||||
|     { | ||||
|       name: '任务日志', | ||||
|       value: 'logs', | ||||
|     }, | ||||
|   ], | ||||
|   scopesMap: { | ||||
|     crons: '定时任务', | ||||
|     envs: '环境变量', | ||||
|     configs: '配置文件', | ||||
|     scripts: '脚本管理', | ||||
|     logs: '任务日志', | ||||
|   }, | ||||
| }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 hanhh
						hanhh