添加openapi模块

This commit is contained in:
hanhh 2021-08-26 19:01:39 +08:00
parent 7739cef7b8
commit 1e58254f4c
12 changed files with 730 additions and 47 deletions

View File

@ -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
View 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);
}
},
);
};

View File

@ -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/'],
};

View File

@ -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
View 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',
}

View File

@ -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
View 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有误' });
}
});
});
}
}

View File

@ -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",

View File

@ -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==}

View 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;

View File

@ -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;

View File

@ -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: '任务日志',
},
};