n8n/packages/cli/src/environments/versionControl/versionControlPreferences.service.ee.ts

183 lines
5.7 KiB
TypeScript
Raw Normal View History

feat: Version control mvp (#6271) * implement basic git service * cleanup connected prop * add skeleton of git functions * initial import/export setup * split out export service * refactor and improve export * begin import * more commands and basic import * clean up imports with transactions * work folder import functions * reintroduce versionid * add missing import to pull workfolder * add get-status endpoint * add cleanup to disconnect * add initRepo options * add more checks and cleanup * minor cleanup * refactor prefs * fix server.ts * fix sending deleted files * rename files to ee * add variable override and fix critical cred import bug * fix mkdir race condition * make initRepo default to true * fix front back integration * improve connect flow * add comment to generated ssh key * fix(editor): use useToast composable * fix buttons size * commenting out repo init for now * fix(editor): update UI logic * fix(editor): remove console.log * fix(editor): remove unused ref * adjust endpoints for improved UI * fix(editor): add push and pull buttons * keep or not ssh key * switching file name to id * fix(editor): add success messages, fix save button * fixed faulty diff preventing pull * fix build * fix(editor): adding loader to VC components * removing duplicate exports * improve conflict finding on push pull * manage pull conflict * alternate push pull * fix pull confirmation * fix rm and credential export/import * switch to alternative pull implementation * fix initial commit * fix(editor): subscribing to VC store action to refresh lists * fix(editor): wrap VC store actions with try * feat: add fine-grained file selection for push action * fix: close modal after successful push * fix(editor): VC preferences validation * fix confirm * fix: update endpoint to /get-status * feat: update pull modal override changes message * fix missing wf error * undo * removing connect endpoint * fix(editor): add button titles * fix(editor): cleaning up store action * add version-control/set-read-only protection * fix(editor): adding set branch readonly * fix(editor): remove Push button if branch set to readonly * fix(editor): fix some styles * fix(editor): remove duplicate and delete actions in WF list when branch is readonly * fix: load status before opening selective push modal * fix(editor): extend readonly logic * add cleanup after failed initRepo * fix deleted files crashing get-status * fix n8n-checkbox in staging dialog * fix(editor): fix loading * fix(editor): resize buttons * fix(editor): fix translation * fix(editor): fix copy text size * fix(editor): fix copy text size * fix(editor): add disconnection confirmation * fix(editor): add disconnection confirmation * fix(editor): set large buttons * add public api Pull endpoint * feat: add refresh ssh key * return prefs when new keys are generated * fix(editor): adding readOnly mode to main header * fix(editor): adding readOnly mode to workflow settings * improve credential owner import * add middleware to endpoints * improve public api error/doc * do not create branch if one already exists * update wordings for connect toasts * fix(editor): updating and separating readonly modes * fix(editor): fix readonly mode in WF list * fix(editor): disable elements dragging on canvas in readonly mode (WIP: not working when NodeView page is loaded first) * fix(editor): fix canvas draggables in readonly env * fix(editor): remove unused variables * fix(editor): hide actions in node connections when readonly * fix(editor): hide actions in node connections when readonly * fix(editor): disable Save button when readonly * fix(editor): disable Save settings if no branch is selected * fix(editor): lint fix * fix(editor): update snapshots * fix(editor): replace Loading... text * fix(editor): reset Loading... text * reset branchname on disconnect * fix(editor): adding some translations * fix(editor): fix unit test * fix(editor): fix loading * fix(editor): set settings saved message * fix(editor): update connection flag * fix branchName not returning after connect * temporary (but still breaking) fix for postgres * fix(editor): adding tooltip to Push/Pull buttons when they're collapsed * fix(editor): enabled activator in readonly mode * fix test * fix(editor): disabling new item addition for workflows in readonly mode * fix(editor): modify Pull/Push button tooltips * do not commit empty variables file --------- Co-authored-by: Michael Auerswald <michael.auerswald@gmail.com> Co-authored-by: Romain Minaud <romain.minaud@gmail.com> Co-authored-by: Alex Grozav <alex@grozav.com>
2023-05-31 06:01:57 -07:00
import { Service } from 'typedi';
import { VersionControlPreferences } from './types/versionControlPreferences';
import type { ValidationError } 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 {
generateSshKeyPair,
isVersionControlLicensed,
versionControlFoldersExistCheck,
} from './versionControlHelper.ee';
import { UserSettings } from 'n8n-core';
import { LoggerProxy, jsonParse } from 'n8n-workflow';
import * as Db from '@/Db';
import {
VERSION_CONTROL_SSH_FOLDER,
VERSION_CONTROL_GIT_FOLDER,
VERSION_CONTROL_SSH_KEY_NAME,
VERSION_CONTROL_PREFERENCES_DB_KEY,
} from './constants';
import path from 'path';
@Service()
export class VersionControlPreferencesService {
private _versionControlPreferences: VersionControlPreferences = new VersionControlPreferences();
private sshKeyName: string;
private sshFolder: string;
private gitFolder: string;
constructor() {
const userFolder = UserSettings.getUserN8nFolderPath();
this.sshFolder = path.join(userFolder, VERSION_CONTROL_SSH_FOLDER);
this.gitFolder = path.join(userFolder, VERSION_CONTROL_GIT_FOLDER);
this.sshKeyName = path.join(this.sshFolder, VERSION_CONTROL_SSH_KEY_NAME);
}
public get versionControlPreferences(): VersionControlPreferences {
return {
...this._versionControlPreferences,
connected: this._versionControlPreferences.connected ?? false,
publicKey: this.getPublicKey(),
};
}
public set versionControlPreferences(preferences: Partial<VersionControlPreferences>) {
this._versionControlPreferences = VersionControlPreferences.merge(
preferences,
this._versionControlPreferences,
);
}
getPublicKey(): string {
try {
return fsReadFileSync(this.sshKeyName + '.pub', { encoding: 'utf8' });
} catch (error) {
LoggerProxy.error(`Failed to read public key: ${(error as Error).message}`);
}
return '';
}
hasKeyPairFiles(): boolean {
return fsExistsSync(this.sshKeyName) && fsExistsSync(this.sshKeyName + '.pub');
}
async deleteKeyPairFiles(): Promise<void> {
try {
await fsRm(this.sshFolder, { recursive: true });
} catch (error) {
LoggerProxy.error(`Failed to delete ssh folder: ${(error as Error).message}`);
}
}
/**
* Will generate an ed25519 key pair and save it to the database and the file system
* Note: this will overwrite any existing key pair
*/
async generateAndSaveKeyPair(): Promise<VersionControlPreferences> {
versionControlFoldersExistCheck([this.gitFolder, this.sshFolder]);
const keyPair = generateSshKeyPair('ed25519');
if (keyPair.publicKey && keyPair.privateKey) {
try {
await fsWriteFile(this.sshKeyName + '.pub', keyPair.publicKey, {
encoding: 'utf8',
mode: 0o666,
});
await fsWriteFile(this.sshKeyName, keyPair.privateKey, { encoding: 'utf8', mode: 0o600 });
} catch (error) {
throw Error(`Failed to save key pair: ${(error as Error).message}`);
}
}
return this.getPreferences();
}
isBranchReadOnly(): boolean {
return this._versionControlPreferences.branchReadOnly;
}
isVersionControlConnected(): boolean {
return this.versionControlPreferences.connected;
}
isVersionControlLicensedAndEnabled(): boolean {
return this.isVersionControlConnected() && isVersionControlLicensed();
}
getBranchName(): string {
return this.versionControlPreferences.branchName;
}
getPreferences(): VersionControlPreferences {
return this.versionControlPreferences;
}
async validateVersionControlPreferences(
preferences: Partial<VersionControlPreferences>,
allowMissingProperties = true,
): Promise<ValidationError[]> {
const preferencesObject = new VersionControlPreferences(preferences);
const validationResult = await validate(preferencesObject, {
forbidUnknownValues: false,
skipMissingProperties: allowMissingProperties,
stopAtFirstError: false,
validationError: { target: false },
});
if (validationResult.length > 0) {
throw new Error(`Invalid version control preferences: ${JSON.stringify(validationResult)}`);
}
return validationResult;
}
async setPreferences(
preferences: Partial<VersionControlPreferences>,
saveToDb = true,
): Promise<VersionControlPreferences> {
versionControlFoldersExistCheck([this.gitFolder, this.sshFolder]);
if (!this.hasKeyPairFiles()) {
LoggerProxy.debug('No key pair files found, generating new pair');
await this.generateAndSaveKeyPair();
}
this.versionControlPreferences = preferences;
if (saveToDb) {
const settingsValue = JSON.stringify(this._versionControlPreferences);
try {
await Db.collections.Settings.save({
key: VERSION_CONTROL_PREFERENCES_DB_KEY,
value: settingsValue,
loadOnStartup: true,
});
} catch (error) {
throw new Error(`Failed to save version control preferences: ${(error as Error).message}`);
}
}
return this.versionControlPreferences;
}
async loadFromDbAndApplyVersionControlPreferences(): Promise<
VersionControlPreferences | undefined
> {
const loadedPreferences = await Db.collections.Settings.findOne({
where: { key: VERSION_CONTROL_PREFERENCES_DB_KEY },
});
if (loadedPreferences) {
try {
const preferences = jsonParse<VersionControlPreferences>(loadedPreferences.value);
if (preferences) {
// set local preferences but don't write back to db
await this.setPreferences(preferences, false);
return preferences;
}
} catch (error) {
LoggerProxy.warn(
`Could not parse Version Control settings from database: ${(error as Error).message}`,
);
}
}
await this.setPreferences(new VersionControlPreferences(), true);
return this.versionControlPreferences;
}
}