@@ -60,10 +45,6 @@
caller = exports.caller;
};
- exports.generate = function() {
- exports.set('admin_pass', GUID(10));
- };
-
exports.submit = function(hide) {
var form = exports.form;
exports.tapi('infrastructures_{0} ERROR'.format(form.id ? ('update/' + form.id) : 'create'), form, function() {
diff --git a/ui/public/forms/settings.html b/ui/public/forms/settings.html
new file mode 100644
index 0000000..5d221b0
--- /dev/null
+++ b/ui/public/forms/settings.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ @(Set a password to encrypt your import)
+
+
+ Import data
+
+
+ Import data
+
+
+
+
+ @(Set a password to crypt your export)
+
+
+ Catalog items
+
+ Projects
+
+
+ Export data
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/public/forms/settings_export.html b/ui/public/forms/settings_export.html
new file mode 100644
index 0000000..3c4927d
--- /dev/null
+++ b/ui/public/forms/settings_export.html
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/schemas/infrastructures.js b/ui/schemas/infrastructures.js
index 6f06a13..d994072 100644
--- a/ui/schemas/infrastructures.js
+++ b/ui/schemas/infrastructures.js
@@ -62,15 +62,12 @@ NEWSCHEMA('Infrastructures', function (schema) {
schema.action('create', {
name: 'Create project',
- input: '*name:String, *icon:Icon, *color:Color, *description:String, *admin_ip:String, *admin_login:String, *admin_pass:String',
+ input: '*name:String, *icon:Icon, *color:Color, *description:String',
action: async function ($, model) {
// Input validation – early exit on first failure
const validators = [
{ fn: REGEX_PROJECTS.name, field: 'name' },
- { fn: REGEX_PROJECTS.description, field: 'description' },
- { fn: REGEX_PROJECTS.admin_ip, field: 'admin_ip' },
- { fn: REGEX_PROJECTS.admin_login, field: 'admin_login' },
- { fn: REGEX_PROJECTS.admin_pass, field: 'admin_pass' }
+ { fn: REGEX_PROJECTS.description, field: 'description' }
];
for (const v of validators) {
if (v.optional && (model[v.field] === '' || model[v.field] == null)) continue;
@@ -83,7 +80,6 @@ NEWSCHEMA('Infrastructures', function (schema) {
// Populate system fields
model.id = UID();
model.uid = $.user.id;
- model.admin_pass = model.admin_pass.sha256(process.env.AUTH_SECRET);
model.dtcreated = new Date();
model.isarchived = false;
model.tfstate = { version: 4 };
@@ -102,7 +98,7 @@ NEWSCHEMA('Infrastructures', function (schema) {
.read('nosql/infrastructures')
.where('uid', $.user.id)
.where('id', id)
- .fields('id,name,description,admin_login,admin_ip,icon,color')
+ .fields('id,name,description,icon,color')
.error('@(Error)')
.promise($);
$.callback(result);
@@ -112,7 +108,7 @@ NEWSCHEMA('Infrastructures', function (schema) {
schema.action('update', {
name: 'Update project',
params: '*id:UID',
- input: '*name:String, *icon:Icon, *color:Color, *description:String, *admin_ip:String, *admin_login:String, admin_pass:String',
+ input: '*name:String, *icon:Icon, *color:Color, *description:String',
action: async function ($, model) {
const { id } = $.params;
@@ -120,8 +116,6 @@ NEWSCHEMA('Infrastructures', function (schema) {
const validators = [
{ fn: REGEX_PROJECTS.name, field: 'name' },
{ fn: REGEX_PROJECTS.description, field: 'description' },
- { fn: REGEX_PROJECTS.admin_ip, field: 'admin_ip' },
- { fn: REGEX_PROJECTS.admin_login, field: 'admin_login' }
];
for (const v of validators) {
if (!FUNC.regex(v.fn, model[v.field])) {
@@ -129,25 +123,6 @@ NEWSCHEMA('Infrastructures', function (schema) {
return;
}
}
-
- // Password handling – only hash when a new password is supplied
- if (model.admin_pass) {
- if (!FUNC.regex(REGEX_PROJECTS.admin_pass, model.admin_pass)) {
- $.invalid(`${REGEX_PROJECTS.admin_pass.comment}`);
- return;
- }
- model.admin_pass = model.admin_pass.sha256(process.env.AUTH_SECRET);
- } else {
- // Preserve existing hash
- const existing = await DATA
- .read('nosql/infrastructures')
- .where('uid', $.user.id)
- .where('id', id)
- .fields('admin_pass')
- .error('@(Error)')
- .promise($);
- model.admin_pass = existing.admin_pass;
- }
model.dtupdated = new Date();
@@ -202,4 +177,35 @@ NEWSCHEMA('Infrastructures', function (schema) {
$.success();
}
});
+
+ schema.action('export', {
+ name: 'Export all infrastructures',
+ params: '*ids:String',
+ action: async function ($) {
+ const result = await DATA
+ .list('nosql/infrastructures')
+ .where('uid', $.user.id)
+ .in('id', $.params.ids.split(','))
+ .error('@(Error)')
+ .promise($);
+ $.callback(result.items);
+ }
+ });
+
+ schema.action('import', {
+ name: 'Import an infrastructure',
+ params: '*id:UID',
+ input: '*color:Color, *description:String, *dtcreated:String, *icon:Icon, isarchived:Boolean, *name:String, *tfstate:Json',
+ action: async function ($, model) {
+ const { id } = $.params;
+ model.tfstate = JSON.parse(model.tfstate);
+
+ DATA.modify('nosql/infrastructures', model, true).where('id', id).insert(function(doc) {
+ doc.uid = $.user.id;
+ doc.id = id;
+ doc.dtupdated = NOW;
+ });
+ $.success();
+ }
+ });
});
\ No newline at end of file
diff --git a/ui/schemas/settings.js b/ui/schemas/settings.js
new file mode 100644
index 0000000..2a0d306
--- /dev/null
+++ b/ui/schemas/settings.js
@@ -0,0 +1,197 @@
+NEWSCHEMA('Settings', function (schema) {
+
+ schema.action('import', {
+ name: 'Import backup',
+ input: '*import:String, *password:String',
+ action: async function ($, model) {
+
+ let projects = DECRYPT(model.import, model.password);
+ if(!projects){
+ $.invalid('invalid import');
+ return;
+ }
+
+ // Process each project sequentially
+ for (const project of projects) {
+
+ // Infrastructure
+ project.infrastructure.tfstate = JSON.stringify(project.infrastructure.tfstate);
+
+ await ACTION('Infrastructures/import', project.infrastructure)
+ .params({ id: project.infrastructure.id })
+ .user($.user)
+ .promise($);
+
+ // Softwares
+ await Promise.all(
+ (project.softwares ?? []).map(software =>
+ ACTION('Softwares/import', software)
+ .params({ id: software.id })
+ .user($.user)
+ .promise($)
+ )
+ );
+
+ // Variables
+ await Promise.all(
+ (project.variables ?? [])
+ .map(variable => {
+ // Guard against undefined/null values
+ if (variable.value == null) return null;
+
+ // Store the value as a JSON string for consistency
+ variable.value = JSON.stringify(variable.value);
+
+ return ACTION('Variables/import', variable)
+ .params({ id: variable.id })
+ .user($.user)
+ .promise($);
+ })
+ .filter(Boolean) // remove the `null` placeholders
+ );
+ }
+ $.success();
+ }
+ });
+
+ schema.action('export', {
+ name: 'Export backup',
+ input: '*projects:Array, *password:String',
+ action: async function ($, model) {
+
+ const decryptValue = (value) => {
+ const decrypted = DECRYPT(value, process.env.AUTH_SECRET);
+ try {
+ return JSON.parse(decrypted);
+ } catch (_) {
+ return decrypted;
+ }
+ };
+
+ const logError = (err, ctx) => {
+ console.error(`⚠️ ${ctx}:`, err);
+ };
+
+ const fetchVariables = async (key, cache = new Map()) => {
+ if (cache.has(key)) return cache.get(key);
+ const { items } = await DATA.list('nosql/variables')
+ .where('key', key)
+ .error('@(Error)')
+ .promise($);
+ cache.set(key, items);
+ return items;
+ };
+
+ const fetchSoftwares = async (instanceName) => {
+ const { items } = await DATA.list('nosql/softwares')
+ .where('uid', $.user.id)
+ .where('instance', instanceName)
+ .error('@(Error)')
+ .promise($);
+ return items;
+ };
+
+ const resolveHostData = async (hostName) => {
+ const [vars, softs] = await Promise.all([
+ fetchVariables(hostName),
+ fetchSoftwares(hostName),
+ ]);
+ return { vars, softs };
+ };
+
+ const resolveSoftwareVars = async (softwares) => {
+ const varPromises = softwares.map(async (software) => {
+ if (!software.domain) return [];
+ return fetchVariables(software.domain);
+ });
+ const results = await Promise.all(varPromises);
+ return results.flat();
+ };
+
+ const buildProject = async (infra) => {
+ if (!infra?.tfstate?.resources) return null;
+
+ // Decrypt infrastructure‑level variables once
+ let infraVars = [];
+ try {
+ infraVars = JSON.parse(DECRYPT(infra.variables, process.env.AUTH_SECRET));
+ } catch (e) {
+ logError(e, 'infra.variables decryption');
+ }
+
+ const project = {
+ infrastructure: { ...infra, variables: infraVars },
+ softwares: [],
+ variables: [],
+ };
+
+ // Cache for variable look‑ups across hosts
+ const varCache = new Map();
+
+ // Process only ansible_host resources
+ const hostResources = infra.tfstate.resources.filter(
+ (r) => r.type === 'ansible_host' && r.instances
+ );
+
+ await Promise.all(
+ hostResources.map(async (resource) => {
+ await Promise.all(
+ resource.instances.map(async (instance) => {
+ const hostName = instance.attributes?.name;
+ if (!hostName) return;
+
+ // ---- a️ Hierarchical domain‑part variables ----
+ const parts = hostName.split('.');
+ const hierarchicalVars = [];
+ for (let i = parts.length - 1; i >= 0; i--) {
+ const id = parts.slice(i).join('.');
+ const vars = await fetchVariables(id, varCache);
+ if (vars?.length) hierarchicalVars.push(...vars);
+ }
+ project.variables.push(...hierarchicalVars);
+
+ // ---- b️ Host‑level data ----
+ const { vars: hostVars, softs: hostSofts } = await resolveHostData(hostName);
+ project.variables.push(...hostVars);
+ project.softwares.push(...hostSofts);
+
+ // ---- c️ Software‑level variables ----
+ const swVars = await resolveSoftwareVars(hostSofts);
+ project.variables.push(...swVars);
+ })
+ );
+ })
+ );
+
+ // Final flatten & decryption of variable values
+ project.softwares = project.softwares.flat();
+ project.variables = project.variables
+ .flat()
+ .map((v) => ({
+ ...v,
+ value: decryptValue(v.value),
+ }));
+
+ return project;
+ };
+
+ const output = [];
+ try {
+ const infrastructures = await ACTION('Infrastructures/export')
+ .params({ ids: model.projects })
+ .user($.user)
+ .promise($);
+
+ const projects = await Promise.all(infrastructures.map(buildProject));
+
+ projects.forEach((proj) => {
+ if (proj) output.push(proj);
+ });
+ } catch (err) {
+ logError(err, 'settings schema execution');
+ }
+
+ $.callback(ENCRYPT(output, model.password));
+ }
+ });
+});
diff --git a/ui/schemas/softwares.js b/ui/schemas/softwares.js
index 9e5ffbd..13328cf 100644
--- a/ui/schemas/softwares.js
+++ b/ui/schemas/softwares.js
@@ -242,4 +242,20 @@ NEWSCHEMA('Softwares', function (schema) {
});
}
});
+
+ schema.action('import', {
+ name: 'Import a software',
+ params: '*id:UID',
+ input: '*domain:String,domain_alias:String,*exposition:String,*instance:String,*size:String,*software:String,*version:String',
+ action: async function ($, model) {
+
+ const { id } = $.params;
+ DATA.modify('nosql/softwares', model, true).where('id', id).insert(function(doc) {
+ doc.uid = $.user.id;
+ doc.id = id;
+ doc.dtupdated = NOW;
+ });
+ $.success();
+ }
+ });
});
diff --git a/ui/schemas/spotlight.js b/ui/schemas/spotlight.js
index 672657b..84eb139 100644
--- a/ui/schemas/spotlight.js
+++ b/ui/schemas/spotlight.js
@@ -12,6 +12,8 @@ NEWACTION('Spotlight/search', {
if ($.user.sa) {
ORIGIN.push({ id: 'users', form: 'formusers', search: 'users', name: TRANSLATE($.user.language || '', '@(Users)'), icon: 'users', color: '#EB73F8' });
ORIGIN.push('-');
+ ORIGIN.push({ id: 'import', form: 'formsettings', search: 'settings', name: TRANSLATE($.user.language || '', '@(Settings)'), icon: 'cog', color: '#EB73F8' });
+ ORIGIN.push('-');
}
ORIGIN.push({ id: 'profile', form: 'formprofile', search: 'profile', name: TRANSLATE($.user.language || '', '@(Your profile)'), icon: 'user', color: '#EB73F8' });
ORIGIN.push({ id: 'password', form: 'formpassword', search: 'user password', name: TRANSLATE($.user.language || '', '@(Your password)'), icon: 'key', color: '#EB73F8' });
diff --git a/ui/schemas/variables.js b/ui/schemas/variables.js
index 2d3b80b..937a9f8 100644
--- a/ui/schemas/variables.js
+++ b/ui/schemas/variables.js
@@ -244,4 +244,19 @@ NEWSCHEMA('Variables', function (schema) {
}
}
});
+
+ schema.action('import', {
+ name: 'Import a variable',
+ params: '*id:UID',
+ input: '*key:String, *key2:String, *type:String, *value:Json',
+ action: async function ($, model) {
+ const { id } = $.params;
+ model.value = ENCRYPT(model.value, process.env.AUTH_SECRET);
+ DATA.modify('nosql/variables', model, true).where('id', id).insert(function(doc) {
+ doc.id = id;
+ doc.dtupdated = NOW;
+ });
+ $.success();
+ }
+ });
});
\ No newline at end of file
diff --git a/ui/views/index.html b/ui/views/index.html
index e11e688..b2becd2 100644
--- a/ui/views/index.html
+++ b/ui/views/index.html
@@ -48,6 +48,7 @@
+
@{json(model, 'pluginsdata')}
@@ -92,6 +93,9 @@
ROUTE('/', function() {
SET('common.page', 'graph');
+
+ SET('formupgrade @reset @hideloading', { version: DEF.versionhtml, currentstep: 'step1' });
+ SET('common.form', 'formupgrade');
}, 'init');
SETTER(true, 'shortcuts/register', 'esc', function(e) {