mirror of
https://github.com/whyour/qinglong.git
synced 2025-05-22 22:36:06 +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