From 1e58254f4c49010a6a3756d37ead2b785afb2aad Mon Sep 17 00:00:00 2001 From: hanhh <18330117883@163.com> Date: Thu, 26 Aug 2021 19:01:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0openapi=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/api/index.ts | 2 + back/api/open.ts | 126 ++++++++++++++++++ back/config/index.ts | 10 +- back/config/util.ts | 2 +- back/data/open.ts | 31 +++++ back/loaders/express.ts | 39 +++++- back/services/open.ts | 171 ++++++++++++++++++++++++ package.json | 4 +- pnpm-lock.yaml | 50 +++---- src/pages/setting/appModal.tsx | 80 +++++++++++ src/pages/setting/index.tsx | 233 +++++++++++++++++++++++++++++++-- src/utils/config.ts | 29 ++++ 12 files changed, 730 insertions(+), 47 deletions(-) create mode 100644 back/api/open.ts create mode 100644 back/data/open.ts create mode 100644 back/services/open.ts create mode 100644 src/pages/setting/appModal.tsx diff --git a/back/api/index.ts b/back/api/index.ts index b200f60f..8f8e7f34 100644 --- a/back/api/index.ts +++ b/back/api/index.ts @@ -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; }; diff --git a/back/api/open.ts b/back/api/open.ts new file mode 100644 index 00000000..b52bd6fb --- /dev/null +++ b/back/api/open.ts @@ -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); + } + }, + ); +}; diff --git a/back/config/index.ts b/back/config/index.ts index 4d37381a..54bcab01 100644 --- a/back/config/index.ts +++ b/back/config/index.ts @@ -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/'], }; diff --git a/back/config/util.ts b/back/config/util.ts index ebb41434..eaa94062 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -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 = []; diff --git a/back/data/open.ts b/back/data/open.ts new file mode 100644 index 00000000..858b4234 --- /dev/null +++ b/back/data/open.ts @@ -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', +} diff --git a/back/loaders/express.ts b/back/loaders/express.ts index 3ef5cf83..183e59f8 100644 --- a/back/loaders/express.ts +++ b/back/loaders/express.ts @@ -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) => { diff --git a/back/services/open.ts b/back/services/open.ts new file mode 100644 index 00000000..17f406da --- /dev/null +++ b/back/services/open.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return new Promise((resolve) => { + this.appDb + .find(query) + .sort({ ...sort }) + .exec((err, docs) => { + resolve(docs); + }); + }); + } + + public async get(_id: string): Promise { + return new Promise((resolve) => { + this.appDb.find({ _id }).exec((err, docs) => { + resolve(docs[0]); + }); + }); + } + + public async authToken({ client_id, client_secret }): Promise { + 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有误' }); + } + }); + }); + } +} diff --git a/package.json b/package.json index 3db3f7db..0fcbea85 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28a5b194..8d60b6d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/src/pages/setting/appModal.tsx b/src/pages/setting/appModal.tsx new file mode 100644 index 00000000..396d62ca --- /dev/null +++ b/src/pages/setting/appModal.tsx @@ -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 ( + { + form + .validateFields() + .then((values) => { + handleOk(values); + }) + .catch((info) => { + console.log('Validate Failed:', info); + }); + }} + onCancel={() => handleCancel()} + confirmLoading={loading} + > +
+ + + + + + +
+
+ ); +}; + +export default AppModal; diff --git a/src/pages/setting/index.tsx b/src/pages/setting/index.tsx index a8430d39..14608c6f 100644 --- a/src/pages/setting/index.tsx +++ b/src/pages/setting/index.tsx @@ -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 {(config.scopesMap as any)[scope]}; + }); + }, + }, + { + title: '操作', + key: 'action', + align: 'center' as const, + render: (text: string, record: any, index: number) => { + const isPc = !isPhone; + return ( + + + editApp(record, index)}> + + + + + resetSecret(record, index)}> + + + + + deleteApp(record, index)}> + + + + + ); + }, + }, + ]; + const [loading, setLoading] = useState(true); const defaultDarken = localStorage.getItem('qinglong_dark_theme') || 'auto'; const [theme, setTheme] = useState(defaultDarken); + const [dataSource, setDataSource] = useState([]); + 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: ( + <> + 确认删除应用{' '} + + {record.name} + {' '} + 吗 + + ), + 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: ( + <> + 确认重置应用{' '} + + {record.name} + {' '} + 的Secret吗 +
+ 重置Secret会让当前应用所有token失效 + + ), + 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' + ? [ + , + ] + : [] + } > - +
{
+ + +
@@ -111,8 +321,13 @@ const Password = ({ headerStyle, isPhone }: any) => {
+ ); }; -export default Password; +export default Setting; diff --git a/src/utils/config.ts b/src/utils/config.ts index 4cbe07a1..5bcfece4 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -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: '任务日志', + }, };