This commit is contained in:
NuklearRabbit
2026-07-27 23:15:35 +02:00
parent 03c2608a35
commit f990c26014
18 changed files with 2439 additions and 938 deletions
+58 -12
View File
@@ -4,10 +4,10 @@ const fs = require('node:fs/promises');
const path = require('node:path');
const crypto = require('node:crypto');
const { safeStorage } = require('electron');
const { assertHttpUrl, assertWorkflowFileName, assertBranchName, assertEnvironmentName, assertCloneRemote, assertRepositoryRelativePaths } = require('../shared/validation.cjs');
const { assertHttpUrl, assertWorkflowFileName, assertBranchName, assertEnvironmentName, assertCloneRemote, assertRepositoryRelativePath, assertRepositoryRelativePaths } = require('../shared/validation.cjs');
const DEFAULT_CONFIG = {
schemaVersion: 8,
schemaVersion: 9,
setupComplete: false,
appearance: 'dark',
gitea: { baseUrl: '', user: null, encryptedToken: null },
@@ -76,7 +76,28 @@ class ConfigStore {
: iconFilePath ? 'upload' : iconUrl && !/itworx\.tech\/assets\/itworx-icon\.png/i.test(iconUrl) ? 'url' : 'builtin';
const visibleName = String(profile.containerName || profile.remoteFolder || '').trim();
const internalService = String(profile.composeService || profile.remoteFolder || 'app').trim().toLowerCase().replace(/[^a-z0-9._-]/g, '-') || 'app';
return { ...profile, composeService: internalService, containerName: visibleName || internalService, iconMode };
const requestedDeploymentMode = String(profile.deploymentMode || '').trim();
const deploymentMode = ['push-bundle', 'server-git', 'monitor-only'].includes(requestedDeploymentMode)
? requestedDeploymentMode
: 'server-git';
const composeFiles = uniqueStrings(profile.composeFiles || [profile.composeFile || 'docker-compose.yml']);
const composeServices = uniqueStrings(profile.composeServices || [internalService]).map((value) => value.toLowerCase());
return {
...profile,
deploymentMode,
composeFile: composeFiles[0] || 'docker-compose.yml',
composeFiles: composeFiles.length ? composeFiles : ['docker-compose.yml'],
composeServices,
composeProject: String(profile.composeProject || '').trim(),
composeWorkingDir: String(profile.composeWorkingDir || '').trim(),
composeService: internalService,
containerName: visibleName || internalService,
iconMode,
manageDockerMan: profile.manageDockerMan === true,
forceRecreate: profile.forceRecreate === true,
removeOrphans: profile.removeOrphans === true,
workloadIdentity: profile.workloadIdentity && typeof profile.workloadIdentity === 'object' ? structuredClone(profile.workloadIdentity) : null
};
})]))
: {},
deploymentStates: source.deploymentStates && typeof source.deploymentStates === 'object' ? source.deploymentStates : {},
@@ -223,10 +244,13 @@ class ConfigStore {
async deleteServer(serverId) {
this.data.servers = this.data.servers.filter((item) => item.id !== serverId);
const removedProfileIds = new Set();
for (const [key, profiles] of Object.entries(this.data.deploymentProfiles)) {
for (const profile of profiles) if (profile.serverId === serverId) removedProfileIds.add(profile.id);
this.data.deploymentProfiles[key] = profiles.filter((profile) => profile.serverId !== serverId);
if (!this.data.deploymentProfiles[key].length) delete this.data.deploymentProfiles[key];
}
for (const profileId of removedProfileIds) delete this.data.deploymentStates[profileId];
await this.save();
}
@@ -357,21 +381,39 @@ class ConfigStore {
inputs: {}
};
if (provider === 'ssh-unraid') {
const remoteFolder = String(profile.remoteFolder || '').trim();
if (!remoteFolder || !/^[a-zA-Z0-9._-]+$/.test(remoteFolder)) throw new Error('Remote folder must contain only letters, numbers, dots, underscores and dashes.');
const remoteFolder = assertRepositoryRelativePath(String(profile.remoteFolder || '').trim());
if (!remoteFolder || remoteFolder === '.' || remoteFolder.split('/').some((part) => !part || part === '.')) throw new Error('Remote folder must be a safe path relative to the configured server base path.');
const preservePaths = assertRepositoryRelativePaths(uniqueStrings(profile.preservePaths || ['.env', 'appdata', 'data', 'logs', 'config', 'compose.override.yml']));
const composeFiles = assertRepositoryRelativePaths(uniqueStrings(profile.composeFiles || [profile.composeFile || 'docker-compose.yml']));
if (!composeFiles.length && profile.generatedCompose !== true) throw new Error('Select at least one Compose file.');
const composeService = (() => {
const value = String(profile.composeService || profile.composeServices?.[0] || remoteFolder.split('/').pop()).trim().toLowerCase();
if (!/^[a-z0-9._-]+$/.test(value)) throw new Error('Compose service must be lowercase and contain only letters, numbers, dots, underscores and dashes.');
return value;
})();
const composeServices = uniqueStrings(profile.composeServices || [composeService]).map((value) => {
const normalized = String(value).trim().toLowerCase();
if (!/^[a-z0-9._-]+$/.test(normalized)) throw new Error('Compose services must be lowercase and contain only letters, numbers, dots, underscores and dashes.');
return normalized;
});
const composeProject = String(profile.composeProject || '').trim();
if (composeProject && !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(composeProject)) throw new Error('Compose project name contains unsupported characters.');
const composeWorkingDir = String(profile.composeWorkingDir || '').trim();
if (composeWorkingDir && (!composeWorkingDir.startsWith('/') || /[\r\n\0]/.test(composeWorkingDir))) throw new Error('Compose working directory must be an absolute safe Unix path.');
const deploymentMode = ['push-bundle', 'server-git', 'monitor-only'].includes(profile.deploymentMode) ? profile.deploymentMode : 'push-bundle';
return {
...common,
serverId: String(profile.serverId || '').trim(),
remoteFolder,
composeFile: String(profile.composeFile || 'docker-compose.yml').trim(),
composeService: (() => {
const value = String(profile.composeService || remoteFolder).trim().toLowerCase();
if (!/^[a-z0-9._-]+$/.test(value)) throw new Error('Compose service must be lowercase and contain only letters, numbers, dots, underscores and dashes.');
return value;
})(),
deploymentMode,
composeFile: composeFiles[0] || 'docker-compose.yml',
composeFiles: composeFiles.length ? composeFiles : ['docker-compose.yml'],
composeProject,
composeWorkingDir,
composeService,
composeServices,
containerName: (() => {
const value = String(profile.containerName || remoteFolder).trim();
const value = String(profile.containerName || remoteFolder.split('/').pop()).trim();
if (!/^[A-Za-z0-9._-]+$/.test(value)) throw new Error('Container name must contain only letters, numbers, dots, underscores and dashes.');
return value;
})(),
@@ -390,6 +432,10 @@ class ConfigStore {
generatedCompose: profile.generatedCompose === true,
adoptedFromServer: profile.adoptedFromServer === true,
serverSourceOfTruth: profile.serverSourceOfTruth === true,
manageDockerMan: profile.manageDockerMan === true,
forceRecreate: profile.forceRecreate === true,
removeOrphans: profile.removeOrphans === true,
workloadIdentity: profile.workloadIdentity && typeof profile.workloadIdentity === 'object' ? structuredClone(profile.workloadIdentity) : null,
detectedAt: profile.detectedAt || null,
provenance: profile.provenance && typeof profile.provenance === 'object' ? structuredClone(profile.provenance) : {},
detectedMetadata: profile.detectedMetadata && typeof profile.detectedMetadata === 'object' ? structuredClone(profile.detectedMetadata) : {},