mirror of
https://github.com/whyour/qinglong.git
synced 2026-02-13 06:25:39 +08:00
Simplify to single global SSH key in system settings
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
06123c1fca
commit
c3322fb7ad
|
|
@ -11,7 +11,6 @@ import system from './system';
|
||||||
import subscription from './subscription';
|
import subscription from './subscription';
|
||||||
import update from './update';
|
import update from './update';
|
||||||
import health from './health';
|
import health from './health';
|
||||||
import sshKey from './sshKey';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
|
|
@ -27,7 +26,6 @@ export default () => {
|
||||||
subscription(app);
|
subscription(app);
|
||||||
update(app);
|
update(app);
|
||||||
health(app);
|
health(app);
|
||||||
sshKey(app);
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
import { Joi, celebrate } from 'celebrate';
|
|
||||||
import { NextFunction, Request, Response, Router } from 'express';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import GlobalSshKeyService from '../services/globalSshKey';
|
|
||||||
const route = Router();
|
|
||||||
|
|
||||||
export default (app: Router) => {
|
|
||||||
app.use('/sshKeys', route);
|
|
||||||
|
|
||||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
|
||||||
const data = await globalSshKeyService.list(
|
|
||||||
req.query.searchValue as string,
|
|
||||||
);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('🔥 error: %o', e);
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
route.post(
|
|
||||||
'/',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(
|
|
||||||
Joi.object({
|
|
||||||
alias: Joi.string().required(),
|
|
||||||
private_key: Joi.string().required(),
|
|
||||||
remarks: Joi.string().optional().allow(''),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
|
||||||
if (!req.body?.length) {
|
|
||||||
return res.send({ code: 400, message: '参数不正确' });
|
|
||||||
}
|
|
||||||
const data = await globalSshKeyService.create(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.object({
|
|
||||||
alias: Joi.string().required(),
|
|
||||||
private_key: Joi.string().required(),
|
|
||||||
remarks: Joi.string().optional().allow('').allow(null),
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
|
||||||
const data = await globalSshKeyService.update(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.delete(
|
|
||||||
'/',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
|
||||||
const data = await globalSshKeyService.remove(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/disable',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
|
||||||
const data = await globalSshKeyService.disabled(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.put(
|
|
||||||
'/enable',
|
|
||||||
celebrate({
|
|
||||||
body: Joi.array().items(Joi.number().required()),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
|
||||||
const data = await globalSshKeyService.enabled(req.body);
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
|
||||||
'/:id',
|
|
||||||
celebrate({
|
|
||||||
params: Joi.object({
|
|
||||||
id: Joi.number().required(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
|
||||||
const data = await globalSshKeyService.getDb({ id: req.params.id });
|
|
||||||
return res.send({ code: 200, data });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -426,6 +426,24 @@ export default (app: Router) => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/config/global-ssh-key',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
globalSshKey: Joi.string().allow('').allow(null),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const systemService = Container.get(SystemService);
|
||||||
|
const result = await systemService.updateGlobalSshKey(req.body);
|
||||||
|
res.send(result);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.put(
|
route.put(
|
||||||
'/config/dependence-clean',
|
'/config/dependence-clean',
|
||||||
celebrate({
|
celebrate({
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { DataTypes, Model } from 'sequelize';
|
|
||||||
import { sequelize } from '.';
|
|
||||||
|
|
||||||
export class SshKey {
|
|
||||||
id?: number;
|
|
||||||
alias: string;
|
|
||||||
private_key: string;
|
|
||||||
remarks?: string;
|
|
||||||
status?: SshKeyStatus;
|
|
||||||
timestamp?: string;
|
|
||||||
|
|
||||||
constructor(options: SshKey) {
|
|
||||||
this.id = options.id;
|
|
||||||
this.alias = options.alias;
|
|
||||||
this.private_key = options.private_key;
|
|
||||||
this.remarks = options.remarks || '';
|
|
||||||
this.status =
|
|
||||||
typeof options.status === 'number' && SshKeyStatus[options.status]
|
|
||||||
? options.status
|
|
||||||
: SshKeyStatus.normal;
|
|
||||||
this.timestamp = new Date().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SshKeyStatus {
|
|
||||||
'normal',
|
|
||||||
'disabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SshKeyInstance extends Model<SshKey, SshKey>, SshKey {}
|
|
||||||
export const SshKeyModel = sequelize.define<SshKeyInstance>('SshKey', {
|
|
||||||
alias: { type: DataTypes.STRING, unique: true },
|
|
||||||
private_key: DataTypes.TEXT,
|
|
||||||
remarks: DataTypes.STRING,
|
|
||||||
status: DataTypes.NUMBER,
|
|
||||||
timestamp: DataTypes.STRING,
|
|
||||||
});
|
|
||||||
|
|
@ -38,6 +38,7 @@ export interface SystemConfigInfo {
|
||||||
pythonMirror?: string;
|
pythonMirror?: string;
|
||||||
linuxMirror?: string;
|
linuxMirror?: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
|
globalSshKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginLogInfo {
|
export interface LoginLogInfo {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { AppModel } from '../data/open';
|
||||||
import { SystemModel } from '../data/system';
|
import { SystemModel } from '../data/system';
|
||||||
import { SubscriptionModel } from '../data/subscription';
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
import { CrontabViewModel } from '../data/cronView';
|
import { CrontabViewModel } from '../data/cronView';
|
||||||
import { SshKeyModel } from '../data/sshKey';
|
|
||||||
import { sequelize } from '../data';
|
import { sequelize } from '../data';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
|
@ -18,7 +17,6 @@ export default async () => {
|
||||||
await EnvModel.sync();
|
await EnvModel.sync();
|
||||||
await SubscriptionModel.sync();
|
await SubscriptionModel.sync();
|
||||||
await CrontabViewModel.sync();
|
await CrontabViewModel.sync();
|
||||||
await SshKeyModel.sync();
|
|
||||||
|
|
||||||
// 初始化新增字段
|
// 初始化新增字段
|
||||||
const migrations = [
|
const migrations = [
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Container } from 'typedi';
|
||||||
import SystemService from '../services/system';
|
import SystemService from '../services/system';
|
||||||
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
||||||
import SubscriptionService from '../services/subscription';
|
import SubscriptionService from '../services/subscription';
|
||||||
import GlobalSshKeyService from '../services/globalSshKey';
|
import SshKeyService from '../services/sshKey';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { fileExist } from '../config/util';
|
import { fileExist } from '../config/util';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
@ -11,7 +11,7 @@ export default async () => {
|
||||||
const systemService = Container.get(SystemService);
|
const systemService = Container.get(SystemService);
|
||||||
const scheduleService = Container.get(ScheduleService);
|
const scheduleService = Container.get(ScheduleService);
|
||||||
const subscriptionService = Container.get(SubscriptionService);
|
const subscriptionService = Container.get(SubscriptionService);
|
||||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
const sshKeyService = Container.get(SshKeyService);
|
||||||
|
|
||||||
// 生成内置token
|
// 生成内置token
|
||||||
let tokenCommand = `ts-node-transpile-only ${join(
|
let tokenCommand = `ts-node-transpile-only ${join(
|
||||||
|
|
@ -59,9 +59,13 @@ export default async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
systemService.updateTimezone(data.info);
|
systemService.updateTimezone(data.info);
|
||||||
|
|
||||||
|
// Apply global SSH key if configured
|
||||||
|
if (data.info.globalSshKey) {
|
||||||
|
await sshKeyService.addGlobalSSHKey(data.info.globalSshKey, 'global');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await globalSshKeyService.applyGlobalSshKeys();
|
|
||||||
await subscriptionService.setSshConfig();
|
await subscriptionService.setSshConfig();
|
||||||
const subs = await subscriptionService.list();
|
const subs = await subscriptionService.list();
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
|
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import { FindOptions, Op } from 'sequelize';
|
|
||||||
import { SshKey, SshKeyModel, SshKeyStatus } from '../data/sshKey';
|
|
||||||
import SshKeyService from './sshKey';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export default class GlobalSshKeyService {
|
|
||||||
constructor(
|
|
||||||
@Inject('logger') private logger: winston.Logger,
|
|
||||||
private sshKeyService: SshKeyService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async create(payloads: SshKey[]): Promise<SshKey[]> {
|
|
||||||
const docs = await this.insert(payloads);
|
|
||||||
await this.applyGlobalSshKeys();
|
|
||||||
return docs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async insert(payloads: SshKey[]): Promise<SshKey[]> {
|
|
||||||
const result: SshKey[] = [];
|
|
||||||
for (const key of payloads) {
|
|
||||||
const doc = await SshKeyModel.create(new SshKey(key), { returning: true });
|
|
||||||
result.push(doc.get({ plain: true }));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update(payload: SshKey): Promise<SshKey> {
|
|
||||||
const doc = await this.getDb({ id: payload.id });
|
|
||||||
const key = new SshKey({ ...doc, ...payload });
|
|
||||||
const newDoc = await this.updateDb(key);
|
|
||||||
await this.applyGlobalSshKeys();
|
|
||||||
return newDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateDb(payload: SshKey): Promise<SshKey> {
|
|
||||||
await SshKeyModel.update({ ...payload }, { where: { id: payload.id } });
|
|
||||||
return await this.getDb({ id: payload.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
|
||||||
const docs = await SshKeyModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
const key = doc.get({ plain: true });
|
|
||||||
await this.sshKeyService.removeGlobalSSHKey(key.alias);
|
|
||||||
}
|
|
||||||
await SshKeyModel.destroy({ where: { id: ids } });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async list(searchText: string = ''): Promise<SshKey[]> {
|
|
||||||
let condition = {};
|
|
||||||
if (searchText) {
|
|
||||||
const encodeText = encodeURI(searchText);
|
|
||||||
const reg = {
|
|
||||||
[Op.or]: [
|
|
||||||
{ [Op.like]: `%${searchText}%` },
|
|
||||||
{ [Op.like]: `%${encodeText}%` },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
condition = {
|
|
||||||
[Op.or]: [
|
|
||||||
{
|
|
||||||
alias: reg,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
remarks: reg,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const result = await this.find(condition);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async find(query: any, sort: any = []): Promise<SshKey[]> {
|
|
||||||
const docs = await SshKeyModel.findAll({
|
|
||||||
where: { ...query },
|
|
||||||
order: [['createdAt', 'DESC'], ...sort],
|
|
||||||
});
|
|
||||||
return docs.map((x) => x.get({ plain: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDb(query: FindOptions<SshKey>['where']): Promise<SshKey> {
|
|
||||||
const doc: any = await SshKeyModel.findOne({ where: { ...query } });
|
|
||||||
if (!doc) {
|
|
||||||
throw new Error(`SshKey ${JSON.stringify(query)} not found`);
|
|
||||||
}
|
|
||||||
return doc.get({ plain: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
|
||||||
const docs = await SshKeyModel.findAll({ where: { id: ids } });
|
|
||||||
for (const doc of docs) {
|
|
||||||
const key = doc.get({ plain: true });
|
|
||||||
await this.sshKeyService.removeGlobalSSHKey(key.alias);
|
|
||||||
}
|
|
||||||
await SshKeyModel.update(
|
|
||||||
{ status: SshKeyStatus.disabled },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async enabled(ids: number[]) {
|
|
||||||
await SshKeyModel.update(
|
|
||||||
{ status: SshKeyStatus.normal },
|
|
||||||
{ where: { id: ids } },
|
|
||||||
);
|
|
||||||
await this.applyGlobalSshKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async applyGlobalSshKeys() {
|
|
||||||
const keys = await this.list();
|
|
||||||
for (const key of keys) {
|
|
||||||
if (key.status === SshKeyStatus.normal) {
|
|
||||||
// For global SSH keys, we generate the key file
|
|
||||||
// Git will automatically use keys from ~/.ssh with standard names
|
|
||||||
await this.sshKeyService.addGlobalSSHKey(
|
|
||||||
key.private_key,
|
|
||||||
key.alias,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -530,6 +530,27 @@ export default class SystemService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateGlobalSshKey(info: SystemModelInfo) {
|
||||||
|
const oDoc = await this.getSystemConfig();
|
||||||
|
const result = await this.updateAuthDb({
|
||||||
|
...oDoc,
|
||||||
|
info: { ...oDoc.info, ...info },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply the global SSH key
|
||||||
|
const SshKeyService = require('./sshKey').default;
|
||||||
|
const Container = require('typedi').Container;
|
||||||
|
const sshKeyService = Container.get(SshKeyService);
|
||||||
|
|
||||||
|
if (info.globalSshKey) {
|
||||||
|
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
|
||||||
|
} else {
|
||||||
|
await sshKeyService.removeGlobalSSHKey('global');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 200, data: result };
|
||||||
|
}
|
||||||
|
|
||||||
public async cleanDependence(type: 'node' | 'python3') {
|
public async cleanDependence(type: 'node' | 'python3') {
|
||||||
if (!type || !['node', 'python3'].includes(type)) {
|
if (!type || !['node', 'python3'].includes(type)) {
|
||||||
return { code: 400, message: '参数错误' };
|
return { code: 400, message: '参数错误' };
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,6 @@ export default {
|
||||||
icon: <IconFont type="ql-icon-env" />,
|
icon: <IconFont type="ql-icon-env" />,
|
||||||
component: '@/pages/env/index',
|
component: '@/pages/env/index',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/sshKey',
|
|
||||||
name: intl.get('SSH密钥'),
|
|
||||||
icon: <IconFont type="ql-icon-key" />,
|
|
||||||
component: '@/pages/sshKey/index',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/config',
|
path: '/config',
|
||||||
name: intl.get('配置文件'),
|
name: intl.get('配置文件'),
|
||||||
|
|
|
||||||
|
|
@ -549,5 +549,8 @@
|
||||||
"请输入SSH私钥": "Please enter SSH private key",
|
"请输入SSH私钥": "Please enter SSH private key",
|
||||||
"请输入SSH私钥内容(以 -----BEGIN 开头)": "Please enter SSH private key content (starts with -----BEGIN)",
|
"请输入SSH私钥内容(以 -----BEGIN 开头)": "Please enter SSH private key content (starts with -----BEGIN)",
|
||||||
"确认删除SSH密钥": "Confirm to delete SSH key",
|
"确认删除SSH密钥": "Confirm to delete SSH key",
|
||||||
"批量": "Batch"
|
"批量": "Batch",
|
||||||
|
"全局SSH私钥": "Global SSH Private Key",
|
||||||
|
"用于访问所有私有仓库的全局SSH私钥": "Global SSH private key for accessing all private repositories",
|
||||||
|
"请输入完整的SSH私钥内容": "Please enter the complete SSH private key content"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -549,5 +549,8 @@
|
||||||
"请输入SSH私钥": "请输入SSH私钥",
|
"请输入SSH私钥": "请输入SSH私钥",
|
||||||
"请输入SSH私钥内容(以 -----BEGIN 开头)": "请输入SSH私钥内容(以 -----BEGIN 开头)",
|
"请输入SSH私钥内容(以 -----BEGIN 开头)": "请输入SSH私钥内容(以 -----BEGIN 开头)",
|
||||||
"确认删除SSH密钥": "确认删除SSH密钥",
|
"确认删除SSH密钥": "确认删除SSH密钥",
|
||||||
"批量": "批量"
|
"批量": "批量",
|
||||||
|
"全局SSH私钥": "全局SSH私钥",
|
||||||
|
"用于访问所有私有仓库的全局SSH私钥": "用于访问所有私有仓库的全局SSH私钥",
|
||||||
|
"请输入完整的SSH私钥内容": "请输入完整的SSH私钥内容"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ const dataMap = {
|
||||||
'log-remove-frequency': 'logRemoveFrequency',
|
'log-remove-frequency': 'logRemoveFrequency',
|
||||||
'cron-concurrency': 'cronConcurrency',
|
'cron-concurrency': 'cronConcurrency',
|
||||||
timezone: 'timezone',
|
timezone: 'timezone',
|
||||||
|
'global-ssh-key': 'globalSshKey',
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportModules = [
|
const exportModules = [
|
||||||
|
|
@ -54,6 +55,7 @@ const Other = ({
|
||||||
logRemoveFrequency?: number | null;
|
logRemoveFrequency?: number | null;
|
||||||
cronConcurrency?: number | null;
|
cronConcurrency?: number | null;
|
||||||
timezone?: string | null;
|
timezone?: string | null;
|
||||||
|
globalSshKey?: string | null;
|
||||||
}>();
|
}>();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [exportLoading, setExportLoading] = useState(false);
|
const [exportLoading, setExportLoading] = useState(false);
|
||||||
|
|
@ -308,6 +310,32 @@ const Other = ({
|
||||||
</Button>
|
</Button>
|
||||||
</Input.Group>
|
</Input.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={intl.get('全局SSH私钥')}
|
||||||
|
name="globalSshKey"
|
||||||
|
tooltip={intl.get('用于访问所有私有仓库的全局SSH私钥')}
|
||||||
|
>
|
||||||
|
<Input.Group compact>
|
||||||
|
<Input.TextArea
|
||||||
|
value={systemConfig?.globalSshKey || ''}
|
||||||
|
style={{ width: 264 }}
|
||||||
|
autoSize={{ minRows: 3, maxRows: 8 }}
|
||||||
|
placeholder={intl.get('请输入完整的SSH私钥内容')}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSystemConfig({ ...systemConfig, globalSshKey: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Input.Group>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
updateSystemConfig('global-ssh-key');
|
||||||
|
}}
|
||||||
|
style={{ width: 264, marginTop: 8 }}
|
||||||
|
>
|
||||||
|
{intl.get('确认')}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={intl.get('语言')} name="lang">
|
<Form.Item label={intl.get('语言')} name="lang">
|
||||||
<Select
|
<Select
|
||||||
defaultValue={localStorage.getItem('lang') || ''}
|
defaultValue={localStorage.getItem('lang') || ''}
|
||||||
|
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
|
||||||
import { SharedContext } from '@/layouts';
|
|
||||||
import config from '@/utils/config';
|
|
||||||
import { request } from '@/utils/http';
|
|
||||||
import {
|
|
||||||
CheckCircleOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
StopOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
|
||||||
import { useOutletContext } from '@umijs/max';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
message,
|
|
||||||
} from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { useVT } from 'virtualizedtableforantd4';
|
|
||||||
import Copy from '../../components/copy';
|
|
||||||
import SshKeyModal from './modal';
|
|
||||||
|
|
||||||
const { Paragraph, Text } = Typography;
|
|
||||||
const { Search } = Input;
|
|
||||||
|
|
||||||
enum Status {
|
|
||||||
'已启用',
|
|
||||||
'已禁用',
|
|
||||||
}
|
|
||||||
|
|
||||||
enum StatusColor {
|
|
||||||
'success',
|
|
||||||
'error',
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OperationName {
|
|
||||||
'启用',
|
|
||||||
'禁用',
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OperationPath {
|
|
||||||
'enable',
|
|
||||||
'disable',
|
|
||||||
}
|
|
||||||
|
|
||||||
const SshKey = () => {
|
|
||||||
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
|
||||||
const columns: any = [
|
|
||||||
{
|
|
||||||
title: intl.get('序号'),
|
|
||||||
width: 80,
|
|
||||||
render: (text: string, record: any, index: number) => {
|
|
||||||
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('别名'),
|
|
||||||
dataIndex: 'alias',
|
|
||||||
key: 'alias',
|
|
||||||
sorter: (a: any, b: any) => a.alias.localeCompare(b.alias),
|
|
||||||
render: (text: string, record: any) => {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<Tooltip title={text} placement="topLeft">
|
|
||||||
<div className="text-ellipsis">{text}</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Copy text={text} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('备注'),
|
|
||||||
dataIndex: 'remarks',
|
|
||||||
key: 'remarks',
|
|
||||||
width: '35%',
|
|
||||||
sorter: (a: any, b: any) => (a.remarks || '').localeCompare(b.remarks || ''),
|
|
||||||
render: (text: string, record: any) => {
|
|
||||||
return (
|
|
||||||
<Tooltip title={text} placement="topLeft">
|
|
||||||
<div className="text-ellipsis">{text}</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('状态'),
|
|
||||||
key: 'status',
|
|
||||||
dataIndex: 'status',
|
|
||||||
width: 90,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
text: intl.get('已启用'),
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: intl.get('已禁用'),
|
|
||||||
value: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onFilter: (value: number, record: any) => record.status === value,
|
|
||||||
render: (value: number, record: any) => {
|
|
||||||
return <Tag color={StatusColor[value]}>{Status[value]}</Tag>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('创建时间'),
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 185,
|
|
||||||
sorter: (a: any, b: any) => {
|
|
||||||
return (
|
|
||||||
dayjs(a.createdAt || 0).valueOf() - dayjs(b.createdAt || 0).valueOf()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
render: (text: string, record: any) => {
|
|
||||||
const d = dayjs(text);
|
|
||||||
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: intl.get('操作'),
|
|
||||||
key: 'action',
|
|
||||||
width: 120,
|
|
||||||
render: (text: string, record: any, index: number) => {
|
|
||||||
return (
|
|
||||||
<Space size="middle">
|
|
||||||
<Tooltip title={OperationName[record.status]}>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
operateSSHKey(
|
|
||||||
[record.id],
|
|
||||||
record.status === 0 ? OperationPath[1] : OperationPath[0],
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{OperationName[record.status]}
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('编辑')}>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
editSSHKey(record);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditOutlined />
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={intl.get('删除')}>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
deleteSSHKey(record);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</a>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const [value, setValue] = useState<any>();
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
||||||
const tableRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [data, setData] = useState<any[]>([]);
|
|
||||||
const scrollHeight = useTableScrollHeight(tableRef, data, [
|
|
||||||
headerStyle.marginTop,
|
|
||||||
]);
|
|
||||||
const [vt] = useVT(() => ({ scroll: { y: scrollHeight } }), [scrollHeight]);
|
|
||||||
|
|
||||||
const getSSHKeys = (needLoading = true) => {
|
|
||||||
setLoading(needLoading);
|
|
||||||
request
|
|
||||||
.get(`${config.apiPrefix}sshKeys?searchValue=${searchValue}`)
|
|
||||||
.then(({ code, data }) => {
|
|
||||||
if (code === 200) {
|
|
||||||
setData(data);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const editSSHKey = (record: any) => {
|
|
||||||
setValue(record);
|
|
||||||
setIsModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSSHKey = (record: any) => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: intl.get('确认删除'),
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
{intl.get('确认删除SSH密钥')}
|
|
||||||
<Text style={{ wordBreak: 'break-all' }} type="warning">
|
|
||||||
{record.alias}
|
|
||||||
</Text>
|
|
||||||
{intl.get('吗')}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
onOk() {
|
|
||||||
request
|
|
||||||
.delete(`${config.apiPrefix}sshKeys`, [record.id])
|
|
||||||
.then(({ code, data }) => {
|
|
||||||
if (code === 200) {
|
|
||||||
message.success(intl.get('删除成功'));
|
|
||||||
getSSHKeys();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onCancel() {
|
|
||||||
console.log('Cancel');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const operateSSHKey = (ids: any[], operationPath: string) => {
|
|
||||||
request
|
|
||||||
.put(`${config.apiPrefix}sshKeys/${operationPath}`, ids)
|
|
||||||
.then(({ code, data }) => {
|
|
||||||
if (code === 200) {
|
|
||||||
message.success(`${intl.get('批量')}${OperationName[OperationPath[operationPath]]}${intl.get('成功')}`);
|
|
||||||
getSSHKeys(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSearch = (value: string) => {
|
|
||||||
setSearchValue(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = (keys?: any[]) => {
|
|
||||||
setIsModalVisible(false);
|
|
||||||
if (keys) {
|
|
||||||
getSSHKeys();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addSSHKey = () => {
|
|
||||||
setValue(null);
|
|
||||||
setIsModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getSSHKeys();
|
|
||||||
}, [searchValue]);
|
|
||||||
|
|
||||||
const rowSelection = {
|
|
||||||
selectedRowKeys,
|
|
||||||
onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
|
|
||||||
setSelectedRowKeys(selectedRowKeys);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer
|
|
||||||
className="ql-container-wrapper"
|
|
||||||
title={intl.get('SSH密钥')}
|
|
||||||
extra={[
|
|
||||||
<Button key="1" type="primary" onClick={addSSHKey}>
|
|
||||||
{intl.get('新建')}
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
header={{
|
|
||||||
style: headerStyle,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div ref={tableRef}>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
dataSource={data}
|
|
||||||
rowKey="id"
|
|
||||||
loading={loading}
|
|
||||||
pagination={false}
|
|
||||||
scroll={{ x: 768 }}
|
|
||||||
sticky
|
|
||||||
components={vt}
|
|
||||||
size="middle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isModalVisible && (
|
|
||||||
<SshKeyModal sshKey={value} handleCancel={handleCancel} />
|
|
||||||
)}
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SshKey;
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Modal, message, Input, Form } from 'antd';
|
|
||||||
import { request } from '@/utils/http';
|
|
||||||
import config from '@/utils/config';
|
|
||||||
|
|
||||||
const SshKeyModal = ({
|
|
||||||
sshKey,
|
|
||||||
handleCancel,
|
|
||||||
}: {
|
|
||||||
sshKey?: any;
|
|
||||||
handleCancel: (keys?: any[]) => void;
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleOk = async (values: any) => {
|
|
||||||
setLoading(true);
|
|
||||||
const method = sshKey ? 'put' : 'post';
|
|
||||||
const payload = sshKey ? { ...values, id: sshKey.id } : [values];
|
|
||||||
try {
|
|
||||||
const { code, data } = await request[method](
|
|
||||||
`${config.apiPrefix}sshKeys`,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (code === 200) {
|
|
||||||
message.success(
|
|
||||||
sshKey ? intl.get('更新SSH密钥成功') : intl.get('创建SSH密钥成功'),
|
|
||||||
);
|
|
||||||
handleCancel(data);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
} catch (error: any) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={sshKey ? intl.get('编辑SSH密钥') : intl.get('创建SSH密钥')}
|
|
||||||
open={true}
|
|
||||||
forceRender
|
|
||||||
centered
|
|
||||||
maskClosable={false}
|
|
||||||
onOk={() => {
|
|
||||||
form
|
|
||||||
.validateFields()
|
|
||||||
.then((values) => {
|
|
||||||
handleOk(values);
|
|
||||||
})
|
|
||||||
.catch((info) => {
|
|
||||||
console.log('Validate Failed:', info);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onCancel={() => handleCancel()}
|
|
||||||
confirmLoading={loading}
|
|
||||||
>
|
|
||||||
<Form form={form} layout="vertical" name="ssh_key_modal" initialValues={sshKey}>
|
|
||||||
<Form.Item
|
|
||||||
name="alias"
|
|
||||||
label={intl.get('别名')}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: intl.get('请输入SSH密钥别名'),
|
|
||||||
whitespace: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder={intl.get('请输入SSH密钥别名')} disabled={!!sshKey} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="private_key"
|
|
||||||
label={intl.get('私钥')}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: intl.get('请输入SSH私钥'),
|
|
||||||
whitespace: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
autoSize={{ minRows: 4, maxRows: 12 }}
|
|
||||||
placeholder={intl.get('请输入SSH私钥内容(以 -----BEGIN 开头)')}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="remarks" label={intl.get('备注')}>
|
|
||||||
<Input placeholder={intl.get('请输入备注')} />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SshKeyModal;
|
|
||||||
Loading…
Reference in New Issue
Block a user