mirror of
https://github.com/whyour/qinglong.git
synced 2026-06-30 20:35:09 +08:00
添加openapi模块
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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/'],
|
||||
};
|
||||
|
||||
+1
-1
@@ -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 = [];
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
+34
-5
@@ -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) => {
|
||||
|
||||
@@ -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有误' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user