mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
refactor(core): Store SSH key pair for source control in DB settings (#8965)
This commit is contained in:
parent
19d9e71cb9
commit
ddc0f57116
|
@ -0,0 +1,106 @@
|
||||||
|
import path from 'node:path';
|
||||||
|
import { readFile, writeFile, rm } from 'node:fs/promises';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import { Cipher, InstanceSettings } from 'n8n-core';
|
||||||
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
import type { MigrationContext, ReversibleMigration } from '@db/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move SSH key pair from file system to database, to enable SSH connections
|
||||||
|
* when running n8n in multiple containers - mains, webhooks, workers, etc.
|
||||||
|
*/
|
||||||
|
export class MoveSshKeysToDatabase1711390882123 implements ReversibleMigration {
|
||||||
|
private readonly settingsKey = 'features.sourceControl.sshKeys';
|
||||||
|
|
||||||
|
private readonly privateKeyPath = path.join(
|
||||||
|
Container.get(InstanceSettings).n8nFolder,
|
||||||
|
'ssh',
|
||||||
|
'key',
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly publicKeyPath = this.privateKeyPath + '.pub';
|
||||||
|
|
||||||
|
private readonly cipher = Container.get(Cipher);
|
||||||
|
|
||||||
|
async up({ escape, runQuery, logger, migrationName }: MigrationContext) {
|
||||||
|
let privateKey, publicKey;
|
||||||
|
|
||||||
|
try {
|
||||||
|
[privateKey, publicKey] = await Promise.all([
|
||||||
|
readFile(this.privateKeyPath, { encoding: 'utf8' }),
|
||||||
|
readFile(this.publicKeyPath, { encoding: 'utf8' }),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
logger.info(`[${migrationName}] No SSH keys in filesystem, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = escape.tableName('settings');
|
||||||
|
|
||||||
|
const rows: Array<{ value: string }> = await runQuery(
|
||||||
|
`SELECT value FROM ${settings} WHERE key = '${this.settingsKey}';`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length === 1) {
|
||||||
|
logger.info(`[${migrationName}] SSH keys already in database, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = JSON.stringify({
|
||||||
|
encryptedPrivateKey: this.cipher.encrypt(privateKey),
|
||||||
|
publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runQuery(
|
||||||
|
`INSERT INTO ${settings} (key, value) VALUES ('${this.settingsKey}', '${value}');`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([rm(this.privateKeyPath), rm(this.publicKeyPath)]);
|
||||||
|
} catch (e) {
|
||||||
|
const error = e instanceof Error ? e : new Error(`${e}`);
|
||||||
|
logger.error(
|
||||||
|
`[${migrationName}] Failed to remove SSH keys from filesystem: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async down({ escape, runQuery, logger, migrationName }: MigrationContext) {
|
||||||
|
const settings = escape.tableName('settings');
|
||||||
|
|
||||||
|
const rows: Array<{ value: string }> = await runQuery(
|
||||||
|
`SELECT value FROM ${settings} WHERE key = '${this.settingsKey}';`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length !== 1) {
|
||||||
|
logger.info(`[${migrationName}] No SSH keys in database, skipping revert`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = rows;
|
||||||
|
|
||||||
|
type KeyPair = { publicKey: string; encryptedPrivateKey: string };
|
||||||
|
|
||||||
|
const dbKeyPair = jsonParse<KeyPair | null>(row.value, { fallbackValue: null });
|
||||||
|
|
||||||
|
if (!dbKeyPair) {
|
||||||
|
logger.info(`[${migrationName}] Malformed SSH keys in database, skipping revert`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKey = this.cipher.decrypt(dbKeyPair.encryptedPrivateKey);
|
||||||
|
const { publicKey } = dbKeyPair;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
writeFile(this.privateKeyPath, privateKey, { encoding: 'utf8', mode: 0o600 }),
|
||||||
|
writeFile(this.publicKeyPath, publicKey, { encoding: 'utf8', mode: 0o600 }),
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
logger.error(`[${migrationName}] Failed to write SSH keys to filesystem, skipping revert`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runQuery(`DELETE ${settings} WHERE WHERE key = 'features.sourceControl.sshKeys';`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,6 +53,7 @@ import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common
|
||||||
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
||||||
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
|
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
|
||||||
import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus';
|
import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus';
|
||||||
|
import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-MoveSshKeysToDatabase';
|
||||||
|
|
||||||
export const mysqlMigrations: Migration[] = [
|
export const mysqlMigrations: Migration[] = [
|
||||||
InitialMigration1588157391238,
|
InitialMigration1588157391238,
|
||||||
|
@ -109,4 +110,5 @@ export const mysqlMigrations: Migration[] = [
|
||||||
AddGlobalAdminRole1700571993961,
|
AddGlobalAdminRole1700571993961,
|
||||||
DropRoleMapping1705429061930,
|
DropRoleMapping1705429061930,
|
||||||
RemoveFailedExecutionStatus1711018413374,
|
RemoveFailedExecutionStatus1711018413374,
|
||||||
|
MoveSshKeysToDatabase1711390882123,
|
||||||
];
|
];
|
||||||
|
|
|
@ -52,6 +52,7 @@ import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common
|
||||||
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
||||||
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
|
import { DropRoleMapping1705429061930 } from '../common/1705429061930-DropRoleMapping';
|
||||||
import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus';
|
import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus';
|
||||||
|
import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-MoveSshKeysToDatabase';
|
||||||
|
|
||||||
export const postgresMigrations: Migration[] = [
|
export const postgresMigrations: Migration[] = [
|
||||||
InitialMigration1587669153312,
|
InitialMigration1587669153312,
|
||||||
|
@ -107,4 +108,5 @@ export const postgresMigrations: Migration[] = [
|
||||||
AddGlobalAdminRole1700571993961,
|
AddGlobalAdminRole1700571993961,
|
||||||
DropRoleMapping1705429061930,
|
DropRoleMapping1705429061930,
|
||||||
RemoveFailedExecutionStatus1711018413374,
|
RemoveFailedExecutionStatus1711018413374,
|
||||||
|
MoveSshKeysToDatabase1711390882123,
|
||||||
];
|
];
|
||||||
|
|
|
@ -50,6 +50,7 @@ import { ModifyWorkflowHistoryNodesAndConnections1695829275184 } from '../common
|
||||||
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
import { AddGlobalAdminRole1700571993961 } from '../common/1700571993961-AddGlobalAdminRole';
|
||||||
import { DropRoleMapping1705429061930 } from './1705429061930-DropRoleMapping';
|
import { DropRoleMapping1705429061930 } from './1705429061930-DropRoleMapping';
|
||||||
import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus';
|
import { RemoveFailedExecutionStatus1711018413374 } from '../common/1711018413374-RemoveFailedExecutionStatus';
|
||||||
|
import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-MoveSshKeysToDatabase';
|
||||||
|
|
||||||
const sqliteMigrations: Migration[] = [
|
const sqliteMigrations: Migration[] = [
|
||||||
InitialMigration1588102412422,
|
InitialMigration1588102412422,
|
||||||
|
@ -103,6 +104,7 @@ const sqliteMigrations: Migration[] = [
|
||||||
AddGlobalAdminRole1700571993961,
|
AddGlobalAdminRole1700571993961,
|
||||||
DropRoleMapping1705429061930,
|
DropRoleMapping1705429061930,
|
||||||
RemoveFailedExecutionStatus1711018413374,
|
RemoveFailedExecutionStatus1711018413374,
|
||||||
|
MoveSshKeysToDatabase1711390882123,
|
||||||
];
|
];
|
||||||
|
|
||||||
export { sqliteMigrations };
|
export { sqliteMigrations };
|
||||||
|
|
|
@ -28,7 +28,8 @@ export class SourceControlController {
|
||||||
@Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true })
|
@Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true })
|
||||||
async getPreferences(): Promise<SourceControlPreferences> {
|
async getPreferences(): Promise<SourceControlPreferences> {
|
||||||
// returns the settings with the privateKey property redacted
|
// returns the settings with the privateKey property redacted
|
||||||
return this.sourceControlPreferencesService.getPreferences();
|
const publicKey = await this.sourceControlPreferencesService.getPublicKey();
|
||||||
|
return { ...this.sourceControlPreferencesService.getPreferences(), publicKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/preferences', { middlewares: [sourceControlLicensedMiddleware] })
|
@Post('/preferences', { middlewares: [sourceControlLicensedMiddleware] })
|
||||||
|
@ -238,7 +239,8 @@ export class SourceControlController {
|
||||||
try {
|
try {
|
||||||
const keyPairType = req.body.keyGeneratorType;
|
const keyPairType = req.body.keyGeneratorType;
|
||||||
const result = await this.sourceControlPreferencesService.generateAndSaveKeyPair(keyPairType);
|
const result = await this.sourceControlPreferencesService.generateAndSaveKeyPair(keyPairType);
|
||||||
return result;
|
const publicKey = await this.sourceControlPreferencesService.getPublicKey();
|
||||||
|
return { ...result, publicKey };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BadRequestError((error as { message: string }).message);
|
throw new BadRequestError((error as { message: string }).message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SourceControlService {
|
export class SourceControlService {
|
||||||
|
/** Path to SSH private key in filesystem. */
|
||||||
private sshKeyName: string;
|
private sshKeyName: string;
|
||||||
|
|
||||||
private sshFolder: string;
|
private sshFolder: string;
|
||||||
|
@ -112,7 +113,7 @@ export class SourceControlService {
|
||||||
});
|
});
|
||||||
await this.sourceControlExportService.deleteRepositoryFolder();
|
await this.sourceControlExportService.deleteRepositoryFolder();
|
||||||
if (!options.keepKeyPair) {
|
if (!options.keepKeyPair) {
|
||||||
await this.sourceControlPreferencesService.deleteKeyPairFiles();
|
await this.sourceControlPreferencesService.deleteKeyPair();
|
||||||
}
|
}
|
||||||
this.gitService.resetService();
|
this.gitService.resetService();
|
||||||
return this.sourceControlPreferencesService.sourceControlPreferences;
|
return this.sourceControlPreferencesService.sourceControlPreferences;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import type { User } from '@db/entities/User';
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
import { ApplicationError } from 'n8n-workflow';
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
import { OwnershipService } from '@/services/ownership.service';
|
import { OwnershipService } from '@/services/ownership.service';
|
||||||
|
import { SourceControlPreferencesService } from './sourceControlPreferences.service.ee';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SourceControlGitService {
|
export class SourceControlGitService {
|
||||||
|
@ -33,6 +34,7 @@ export class SourceControlGitService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly ownershipService: OwnershipService,
|
private readonly ownershipService: OwnershipService,
|
||||||
|
private readonly sourceControlPreferencesService: SourceControlPreferencesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,12 +68,7 @@ export class SourceControlGitService {
|
||||||
sshFolder: string;
|
sshFolder: string;
|
||||||
sshKeyName: string;
|
sshKeyName: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const {
|
const { sourceControlPreferences: sourceControlPreferences, gitFolder, sshFolder } = options;
|
||||||
sourceControlPreferences: sourceControlPreferences,
|
|
||||||
gitFolder,
|
|
||||||
sshKeyName,
|
|
||||||
sshFolder,
|
|
||||||
} = options;
|
|
||||||
this.logger.debug('GitService.init');
|
this.logger.debug('GitService.init');
|
||||||
if (this.git !== null) {
|
if (this.git !== null) {
|
||||||
return;
|
return;
|
||||||
|
@ -82,8 +79,10 @@ export class SourceControlGitService {
|
||||||
|
|
||||||
sourceControlFoldersExistCheck([gitFolder, sshFolder]);
|
sourceControlFoldersExistCheck([gitFolder, sshFolder]);
|
||||||
|
|
||||||
|
const privateKeyPath = await this.sourceControlPreferencesService.getPrivateKeyPath();
|
||||||
|
|
||||||
const sshKnownHosts = path.join(sshFolder, 'known_hosts');
|
const sshKnownHosts = path.join(sshFolder, 'known_hosts');
|
||||||
const sshCommand = `ssh -o UserKnownHostsFile=${sshKnownHosts} -o StrictHostKeyChecking=no -i ${sshKeyName}`;
|
const sshCommand = `ssh -o UserKnownHostsFile=${sshKnownHosts} -o StrictHostKeyChecking=no -i ${privateKeyPath}`;
|
||||||
|
|
||||||
this.gitOptions = {
|
this.gitOptions = {
|
||||||
baseDir: gitFolder,
|
baseDir: gitFolder,
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
|
import os from 'node:os';
|
||||||
|
import { writeFile, chmod, readFile } from 'node:fs/promises';
|
||||||
import Container, { Service } from 'typedi';
|
import Container, { Service } from 'typedi';
|
||||||
import { SourceControlPreferences } from './types/sourceControlPreferences';
|
import { SourceControlPreferences } from './types/sourceControlPreferences';
|
||||||
import type { ValidationError } from 'class-validator';
|
import type { ValidationError } from 'class-validator';
|
||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from 'fs';
|
|
||||||
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises';
|
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises';
|
||||||
import {
|
import {
|
||||||
generateSshKeyPair,
|
generateSshKeyPair,
|
||||||
isSourceControlLicensed,
|
isSourceControlLicensed,
|
||||||
sourceControlFoldersExistCheck,
|
sourceControlFoldersExistCheck,
|
||||||
} from './sourceControlHelper.ee';
|
} from './sourceControlHelper.ee';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
import { Cipher, InstanceSettings } from 'n8n-core';
|
||||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
SOURCE_CONTROL_SSH_FOLDER,
|
SOURCE_CONTROL_SSH_FOLDER,
|
||||||
|
@ -36,6 +37,7 @@ export class SourceControlPreferencesService {
|
||||||
constructor(
|
constructor(
|
||||||
instanceSettings: InstanceSettings,
|
instanceSettings: InstanceSettings,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
private readonly cipher: Cipher,
|
||||||
) {
|
) {
|
||||||
this.sshFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_SSH_FOLDER);
|
this.sshFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_SSH_FOLDER);
|
||||||
this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
|
this.gitFolder = path.join(instanceSettings.n8nFolder, SOURCE_CONTROL_GIT_FOLDER);
|
||||||
|
@ -46,7 +48,6 @@ export class SourceControlPreferencesService {
|
||||||
return {
|
return {
|
||||||
...this._sourceControlPreferences,
|
...this._sourceControlPreferences,
|
||||||
connected: this._sourceControlPreferences.connected ?? false,
|
connected: this._sourceControlPreferences.connected ?? false,
|
||||||
publicKey: this.getPublicKey(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,24 +67,71 @@ export class SourceControlPreferencesService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPublicKey(): string {
|
private async getKeyPairFromDatabase() {
|
||||||
|
const dbSetting = await Container.get(SettingsRepository).findByKey(
|
||||||
|
'features.sourceControl.sshKeys',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dbSetting?.value) return null;
|
||||||
|
|
||||||
|
type KeyPair = { publicKey: string; encryptedPrivateKey: string };
|
||||||
|
|
||||||
|
return jsonParse<KeyPair | null>(dbSetting.value, { fallbackValue: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPrivateKeyFromDatabase() {
|
||||||
|
const dbKeyPair = await this.getKeyPairFromDatabase();
|
||||||
|
|
||||||
|
if (!dbKeyPair) return null;
|
||||||
|
|
||||||
|
return this.cipher.decrypt(dbKeyPair.encryptedPrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPublicKeyFromDatabase() {
|
||||||
|
const dbKeyPair = await this.getKeyPairFromDatabase();
|
||||||
|
|
||||||
|
if (!dbKeyPair) return null;
|
||||||
|
|
||||||
|
return dbKeyPair.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPrivateKeyPath() {
|
||||||
|
const dbPrivateKey = await this.getPrivateKeyFromDatabase();
|
||||||
|
|
||||||
|
if (dbPrivateKey) {
|
||||||
|
const tempFilePath = path.join(os.tmpdir(), 'ssh_private_key_temp');
|
||||||
|
|
||||||
|
await writeFile(tempFilePath, dbPrivateKey);
|
||||||
|
|
||||||
|
await chmod(tempFilePath, 0o600);
|
||||||
|
|
||||||
|
return tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sshKeyName; // fall back to key in filesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicKey() {
|
||||||
try {
|
try {
|
||||||
return fsReadFileSync(this.sshKeyName + '.pub', { encoding: 'utf8' });
|
const dbPublicKey = await this.getPublicKeyFromDatabase();
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to read public key: ${(error as Error).message}`);
|
if (dbPublicKey) return dbPublicKey;
|
||||||
|
|
||||||
|
return await readFile(this.sshKeyName + '.pub', { encoding: 'utf8' });
|
||||||
|
} catch (e) {
|
||||||
|
const error = e instanceof Error ? e : new Error(`${e}`);
|
||||||
|
this.logger.error(`Failed to read SSH public key: ${error.message}`);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
hasKeyPairFiles(): boolean {
|
async deleteKeyPair() {
|
||||||
return fsExistsSync(this.sshKeyName) && fsExistsSync(this.sshKeyName + '.pub');
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteKeyPairFiles(): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
await fsRm(this.sshFolder, { recursive: true });
|
await fsRm(this.sshFolder, { recursive: true });
|
||||||
} catch (error) {
|
await Container.get(SettingsRepository).delete({ key: 'features.sourceControl.sshKeys' });
|
||||||
this.logger.error(`Failed to delete ssh folder: ${(error as Error).message}`);
|
} catch (e) {
|
||||||
|
const error = e instanceof Error ? e : new Error(`${e}`);
|
||||||
|
this.logger.error(`Failed to delete SSH key pair: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,13 +156,27 @@ export class SourceControlPreferencesService {
|
||||||
});
|
});
|
||||||
await fsWriteFile(this.sshKeyName, keyPair.privateKey, { encoding: 'utf8', mode: 0o600 });
|
await fsWriteFile(this.sshKeyName, keyPair.privateKey, { encoding: 'utf8', mode: 0o600 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ApplicationError('Failed to save key pair', { cause: error });
|
throw new ApplicationError('Failed to save key pair to disk', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update preferences only after generating key pair to prevent endless loop
|
// update preferences only after generating key pair to prevent endless loop
|
||||||
if (keyPairType !== this.getPreferences().keyGeneratorType) {
|
if (keyPairType !== this.getPreferences().keyGeneratorType) {
|
||||||
await this.setPreferences({ keyGeneratorType: keyPairType });
|
await this.setPreferences({ keyGeneratorType: keyPairType });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Container.get(SettingsRepository).save({
|
||||||
|
key: 'features.sourceControl.sshKeys',
|
||||||
|
value: JSON.stringify({
|
||||||
|
encryptedPrivateKey: this.cipher.encrypt(keyPair.privateKey),
|
||||||
|
publicKey: keyPair.publicKey,
|
||||||
|
}),
|
||||||
|
loadOnStartup: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new ApplicationError('Failed to write key pair to database', { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
return this.getPreferences();
|
return this.getPreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,14 +223,6 @@ export class SourceControlPreferencesService {
|
||||||
preferences: Partial<SourceControlPreferences>,
|
preferences: Partial<SourceControlPreferences>,
|
||||||
saveToDb = true,
|
saveToDb = true,
|
||||||
): Promise<SourceControlPreferences> {
|
): Promise<SourceControlPreferences> {
|
||||||
sourceControlFoldersExistCheck([this.gitFolder, this.sshFolder]);
|
|
||||||
if (!this.hasKeyPairFiles()) {
|
|
||||||
const keyPairType =
|
|
||||||
preferences.keyGeneratorType ??
|
|
||||||
(config.get('sourceControl.defaultKeyPairType') as KeyPairType);
|
|
||||||
this.logger.debug(`No key pair files found, generating new pair using type: ${keyPairType}`);
|
|
||||||
await this.generateAndSaveKeyPair(keyPairType);
|
|
||||||
}
|
|
||||||
this.sourceControlPreferences = preferences;
|
this.sourceControlPreferences = preferences;
|
||||||
if (saveToDb) {
|
if (saveToDb) {
|
||||||
const settingsValue = JSON.stringify(this._sourceControlPreferences);
|
const settingsValue = JSON.stringify(this._sourceControlPreferences);
|
||||||
|
|
Loading…
Reference in a new issue