mirror of
https://github.com/whyour/qinglong.git
synced 2026-02-12 14:05:38 +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 update from './update';
|
||||
import health from './health';
|
||||
import sshKey from './sshKey';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
|
|
@ -26,6 +27,7 @@ export default () => {
|
|||
subscription(app);
|
||||
update(app);
|
||||
health(app);
|
||||
sshKey(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 ScheduleService, { ScheduleTaskType } from '../services/schedule';
|
||||
import SubscriptionService from '../services/subscription';
|
||||
import GlobalSshKeyService from '../services/globalSshKey';
|
||||
import config from '../config';
|
||||
import { fileExist } from '../config/util';
|
||||
import { join } from 'path';
|
||||
|
|
@ -10,6 +11,7 @@ export default async () => {
|
|||
const systemService = Container.get(SystemService);
|
||||
const scheduleService = Container.get(ScheduleService);
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const globalSshKeyService = Container.get(GlobalSshKeyService);
|
||||
|
||||
// 生成内置token
|
||||
let tokenCommand = `ts-node-transpile-only ${join(
|
||||
|
|
@ -59,6 +61,7 @@ export default async () => {
|
|||
systemService.updateTimezone(data.info);
|
||||
}
|
||||
|
||||
await globalSshKeyService.applyGlobalSshKeys();
|
||||
await subscriptionService.setSshConfig();
|
||||
const subs = await subscriptionService.list();
|
||||
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