From c3322fb7ad10d46797992a815a3a8112b3a1e29e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:59:10 +0000 Subject: [PATCH] Simplify to single global SSH key in system settings Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> --- back/api/index.ts | 2 - back/api/sshKey.ts | 142 ---------------- back/api/system.ts | 18 ++ back/data/sshKey.ts | 37 ----- back/data/system.ts | 1 + back/loaders/db.ts | 2 - back/loaders/initTask.ts | 10 +- back/services/globalSshKey.ts | 130 --------------- back/services/system.ts | 21 +++ src/layouts/defaultProps.tsx | 6 - src/locales/en-US.json | 5 +- src/locales/zh-CN.json | 5 +- src/pages/setting/other.tsx | 28 ++++ src/pages/sshKey/index.tsx | 300 ---------------------------------- src/pages/sshKey/modal.tsx | 97 ----------- 15 files changed, 83 insertions(+), 721 deletions(-) delete mode 100644 back/api/sshKey.ts delete mode 100644 back/data/sshKey.ts delete mode 100644 back/services/globalSshKey.ts delete mode 100644 src/pages/sshKey/index.tsx delete mode 100644 src/pages/sshKey/modal.tsx diff --git a/back/api/index.ts b/back/api/index.ts index 4d2d96a7..6dcab62f 100644 --- a/back/api/index.ts +++ b/back/api/index.ts @@ -11,7 +11,6 @@ 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(); @@ -27,7 +26,6 @@ export default () => { subscription(app); update(app); health(app); - sshKey(app); return app; }; diff --git a/back/api/sshKey.ts b/back/api/sshKey.ts deleted file mode 100644 index 4c088a85..00000000 --- a/back/api/sshKey.ts +++ /dev/null @@ -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); - } - }, - ); -}; diff --git a/back/api/system.ts b/back/api/system.ts index 30742a52..f51c6575 100644 --- a/back/api/system.ts +++ b/back/api/system.ts @@ -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( '/config/dependence-clean', celebrate({ diff --git a/back/data/sshKey.ts b/back/data/sshKey.ts deleted file mode 100644 index 78824aa2..00000000 --- a/back/data/sshKey.ts +++ /dev/null @@ -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 {} -export const SshKeyModel = sequelize.define('SshKey', { - alias: { type: DataTypes.STRING, unique: true }, - private_key: DataTypes.TEXT, - remarks: DataTypes.STRING, - status: DataTypes.NUMBER, - timestamp: DataTypes.STRING, -}); diff --git a/back/data/system.ts b/back/data/system.ts index 84b6aae2..2dc14f50 100644 --- a/back/data/system.ts +++ b/back/data/system.ts @@ -38,6 +38,7 @@ export interface SystemConfigInfo { pythonMirror?: string; linuxMirror?: string; timezone?: string; + globalSshKey?: string; } export interface LoginLogInfo { diff --git a/back/loaders/db.ts b/back/loaders/db.ts index 561923a6..49fefb6b 100644 --- a/back/loaders/db.ts +++ b/back/loaders/db.ts @@ -6,7 +6,6 @@ import { AppModel } from '../data/open'; import { SystemModel } from '../data/system'; import { SubscriptionModel } from '../data/subscription'; import { CrontabViewModel } from '../data/cronView'; -import { SshKeyModel } from '../data/sshKey'; import { sequelize } from '../data'; export default async () => { @@ -18,7 +17,6 @@ export default async () => { await EnvModel.sync(); await SubscriptionModel.sync(); await CrontabViewModel.sync(); - await SshKeyModel.sync(); // 初始化新增字段 const migrations = [ diff --git a/back/loaders/initTask.ts b/back/loaders/initTask.ts index f8c5c235..bdc57d42 100644 --- a/back/loaders/initTask.ts +++ b/back/loaders/initTask.ts @@ -2,7 +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 SshKeyService from '../services/sshKey'; import config from '../config'; import { fileExist } from '../config/util'; import { join } from 'path'; @@ -11,7 +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); + const sshKeyService = Container.get(SshKeyService); // 生成内置token let tokenCommand = `ts-node-transpile-only ${join( @@ -59,9 +59,13 @@ export default async () => { } 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(); const subs = await subscriptionService.list(); for (const sub of subs) { diff --git a/back/services/globalSshKey.ts b/back/services/globalSshKey.ts deleted file mode 100644 index fe01b55e..00000000 --- a/back/services/globalSshKey.ts +++ /dev/null @@ -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 { - const docs = await this.insert(payloads); - await this.applyGlobalSshKeys(); - return docs; - } - - public async insert(payloads: SshKey[]): Promise { - 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 { - 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 { - 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 { - 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 { - const docs = await SshKeyModel.findAll({ - where: { ...query }, - order: [['createdAt', 'DESC'], ...sort], - }); - return docs.map((x) => x.get({ plain: true })); - } - - public async getDb(query: FindOptions['where']): Promise { - 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, - ); - } - } - } -} diff --git a/back/services/system.ts b/back/services/system.ts index f7d83797..ecc2a732 100644 --- a/back/services/system.ts +++ b/back/services/system.ts @@ -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') { if (!type || !['node', 'python3'].includes(type)) { return { code: 400, message: '参数错误' }; diff --git a/src/layouts/defaultProps.tsx b/src/layouts/defaultProps.tsx index 70e43d65..c747b11e 100644 --- a/src/layouts/defaultProps.tsx +++ b/src/layouts/defaultProps.tsx @@ -42,12 +42,6 @@ export default { icon: , component: '@/pages/env/index', }, - { - path: '/sshKey', - name: intl.get('SSH密钥'), - icon: , - component: '@/pages/sshKey/index', - }, { path: '/config', name: intl.get('配置文件'), diff --git a/src/locales/en-US.json b/src/locales/en-US.json index e9edb7c0..bf4ec2dc 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -549,5 +549,8 @@ "请输入SSH私钥": "Please enter SSH private key", "请输入SSH私钥内容(以 -----BEGIN 开头)": "Please enter SSH private key content (starts with -----BEGIN)", "确认删除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" } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 4ba77dd5..f69f3a29 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -549,5 +549,8 @@ "请输入SSH私钥": "请输入SSH私钥", "请输入SSH私钥内容(以 -----BEGIN 开头)": "请输入SSH私钥内容(以 -----BEGIN 开头)", "确认删除SSH密钥": "确认删除SSH密钥", - "批量": "批量" + "批量": "批量", + "全局SSH私钥": "全局SSH私钥", + "用于访问所有私有仓库的全局SSH私钥": "用于访问所有私有仓库的全局SSH私钥", + "请输入完整的SSH私钥内容": "请输入完整的SSH私钥内容" } diff --git a/src/pages/setting/other.tsx b/src/pages/setting/other.tsx index 570dc0ad..e2ef1973 100644 --- a/src/pages/setting/other.tsx +++ b/src/pages/setting/other.tsx @@ -30,6 +30,7 @@ const dataMap = { 'log-remove-frequency': 'logRemoveFrequency', 'cron-concurrency': 'cronConcurrency', timezone: 'timezone', + 'global-ssh-key': 'globalSshKey', }; const exportModules = [ @@ -54,6 +55,7 @@ const Other = ({ logRemoveFrequency?: number | null; cronConcurrency?: number | null; timezone?: string | null; + globalSshKey?: string | null; }>(); const [form] = Form.useForm(); const [exportLoading, setExportLoading] = useState(false); @@ -308,6 +310,32 @@ const Other = ({ + + + { + setSystemConfig({ ...systemConfig, globalSshKey: e.target.value }); + }} + /> + + + - - - - - - - - - - ); -}; - -export default SshKeyModal;