mirror of
https://github.com/whyour/qinglong.git
synced 2026-02-12 22:16:42 +08:00
Add backend support for global SSH keys
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
parent
e41a5facda
commit
43aaac4bcc
|
|
@ -11,6 +11,7 @@ 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();
|
||||||
|
|
@ -26,6 +27,7 @@ export default () => {
|
||||||
subscription(app);
|
subscription(app);
|
||||||
update(app);
|
update(app);
|
||||||
health(app);
|
health(app);
|
||||||
|
sshKey(app);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
142
back/api/sshKey.ts
Normal file
142
back/api/sshKey.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
37
back/data/sshKey.ts
Normal file
37
back/data/sshKey.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
@ -2,6 +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 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';
|
||||||
|
|
@ -10,6 +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);
|
||||||
|
|
||||||
// 生成内置token
|
// 生成内置token
|
||||||
let tokenCommand = `ts-node-transpile-only ${join(
|
let tokenCommand = `ts-node-transpile-only ${join(
|
||||||
|
|
@ -59,6 +61,7 @@ export default async () => {
|
||||||
systemService.updateTimezone(data.info);
|
systemService.updateTimezone(data.info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
|
||||||
130
back/services/globalSshKey.ts
Normal file
130
back/services/globalSshKey.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,4 +131,12 @@ export default class SshKeyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async addGlobalSSHKey(key: string, alias: string): Promise<void> {
|
||||||
|
await this.generatePrivateKeyFile(`global_${alias}`, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeGlobalSSHKey(alias: string): Promise<void> {
|
||||||
|
await this.removePrivateKeyFile(`global_${alias}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user