Loading CMakeLists.txt +5 −2 Changes for CMakeLists.txt: 5 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,9 +13,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_ENABLE_EXPORTS 1) set(DATAPATH "${CMAKE_INSTALL_DATADIR}/fenrirds" CACHE FILEPATH "path where stored webinterface files are located") set(PLUGINPATH "${CMAKE_INSTALL_LIBDIR}/fenrirds" CACHE FILEPATH "default plugin search path") set(PLUGINPATH "${CMAKE_INSTALL_FULL_LIBDIR}/fenrirds" CACHE FILEPATH "default plugin search path") set(STATEPATH "${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/fenrirds" CACHE FILEPATH "default path of the databases in a config written by the setup") add_definitions(-D${CMAKE_HOST_SYSTEM_NAME} -DDATAPATH="${DATAPATH}" -DPLUGINPATH="${PLUGINPATH}") add_definitions(-D${CMAKE_HOST_SYSTEM_NAME} -DDATAPATH="${DATAPATH}" -DPLUGINPATH="${PLUGINPATH}" -DSTATEPATH="${STATEPATH}") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") Loading Loading @@ -86,6 +87,8 @@ install(FILES data/fenrirds.js data/style.css data/wizzard.html data/setup.html data/serverconfig.js data/login.html data/index.html data/fenrirds.png Loading data/admin.html +117 −0 Changes for data/admin.html: 117 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ <head> <title>FenrirDirectoryServer Admin</title> <link rel="stylesheet" href="/style.css"> <script src="/serverconfig.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> .admin-table { border-collapse: separate; border-spacing: 0; width: 100%; margin: 1em 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } Loading Loading @@ -68,6 +69,7 @@ <li id="nav-services" style="display:none"><a href="#" onclick="loadServices(); return false;">Services</a></li> <li id="nav-gpos"><a href="#" onclick="loadGpos(); return false;">Policies</a></li> <li id="nav-sessions"><a href="#" onclick="loadSessions(); return false;">Sessions</a></li> <li id="nav-server" style="display:none"><a href="#" onclick="loadServerConfig(); return false;">Server</a></li> <li id="nav-cluster" style="display:none"><a href="#" onclick="loadClusterStatus(); return false;">Cluster</a></li> <li id="nav-export" style="display:none"><a href="#" onclick="loadExport(); return false;">Export/Import</a></li> <li class="nav-logout"><a href="#" onclick="doLogout(); return false;">Logout</a></li> Loading Loading @@ -1244,6 +1246,119 @@ function submitEditGpo(gpid) { }); } /* ---- Server settings (config file, written by the root config helper) ---- */ function loadServerConfig() { currentView = loadServerConfig; setActiveNav('nav-server'); document.getElementById("domain-select").innerHTML = ''; ajaxGet("/settings/admin/api/getconfig", function(err, data) { if (err) { showError(err); return; } var html = '<div class="view-header"><h3>Server</h3></div>'; html += '<p>Settings in <span class="mono">' + escapeHtml(data.path) + '</span>. ' + 'Port, address and TLS changes take effect after a restart.</p>'; html += ServerConfig.render(data); html += '<p id="srvcfgmsg"></p>'; html += '<ul class="toolbar">'; html += '<li><a class="button" href="#" onclick="saveServerConfig(false); return false;">Save</a></li>'; html += '<li><a class="button" href="#" onclick="saveServerConfig(true); return false;">Save and restart</a></li>'; html += '</ul>'; html += '<h4>Unconfigured nodes</h4>'; html += '<p>Nodes in setup mode on the local network (mDNS). Adding one sends it this node\'s ' + 'replication or cluster settings, including their keys, over plain HTTP.</p>'; html += '<ul class="toolbar"><li><a class="button" href="#" onclick="discoverNodes(); return false;">Search</a></li></ul>'; html += '<div id="nodelist"></div><p id="nodemsg"></p>'; setContent(html); ServerConfig.updateRole(); }); } var g_foundNodes = []; function nodeMsg(text, isError) { var el = document.getElementById("nodemsg"); if (!el) return; el.className = isError ? "msg-error" : "msg-ok"; el.textContent = text; } function discoverNodes() { document.getElementById("nodelist").innerHTML = '<p>Searching…</p>'; nodeMsg("", false); ajaxGet("/settings/admin/api/discovernodes", function(err, data) { if (err) { document.getElementById("nodelist").innerHTML = ''; nodeMsg(err, true); return; } g_foundNodes = data.nodes || []; if (!g_foundNodes.length) { document.getElementById("nodelist").innerHTML = '<p>No unconfigured node found.</p>'; return; } var html = '<div class="table-wrap"><table class="admin-table"><tr><th>Host</th><th>Address</th><th>ID</th><th></th></tr>'; for (var i = 0; i < g_foundNodes.length; i++) { var n = g_foundNodes[i]; html += '<tr><td>' + escapeHtml(n.host || n.instance) + '</td>' + '<td class="mono">' + escapeHtml(n.address + ':' + n.port) + '</td>' + '<td class="mono">' + escapeHtml(n.id) + '</td><td class="actions">' + '<a class="button" href="#" onclick="joinNode(' + i + ', \'slave\'); return false;">Add as replication slave</a> '; if (g_permissions && g_permissions.cluster) html += '<a class="button" href="#" onclick="joinNode(' + i + ', \'cluster\'); return false;">Add to cluster</a>'; html += '</td></tr>'; } document.getElementById("nodelist").innerHTML = html + '</table></div>'; }); } function joinNode(idx, mode) { var n = g_foundNodes[idx]; var what = mode === 'cluster' ? 'Add ' + (n.host || n.address) + ' (' + n.address + ') to the cluster? All cluster nodes restart.' : 'Make ' + (n.host || n.address) + ' (' + n.address + ') a replication slave of this node?'; if (!confirm(what + '\n\nOnly continue if you recognise this node (ID ' + n.id + ').')) return; nodeMsg("Configuring " + n.address + " ...", false); ajaxPost("/settings/admin/api/joinnode", { address: n.address, port: n.port, host: n.host, mode: mode }, function(err, data) { if (err) { var m = err; try { m = JSON.parse(err.substring(err.indexOf(':') + 2)).error || err; } catch (e) {} nodeMsg(m, true); return; } var text = n.address + ' is configured and restarts' + (data.name ? ' as slave "' + data.name + '"' : '') + '.'; if (data.warnings && data.warnings.length) text += ' Note: ' + data.warnings.join('; '); if (data.restart) { nodeMsg(text + ' This node restarts too ...', false); ServerConfig.waitFor(window.location.protocol + '//' + window.location.host + '/settings/index', nodeMsg); } else { nodeMsg(text, false); } }); } function saveServerConfig(restart) { var values = ServerConfig.collect(); var msg = function(text, isError) { var el = document.getElementById("srvcfgmsg"); if (!el) return; el.className = isError ? "msg-error" : "msg-ok"; el.textContent = text; }; ajaxPost("/settings/admin/api/setconfig", { values: values, restart: restart }, function(err, data) { if (err) { var m = err; try { m = JSON.parse(err.substring(err.indexOf(':') + 2)).error || err; } catch (e) {} msg(m, true); return; } if (!restart) { msg("Saved. The changes take effect after the next restart.", false); return; } var url = ServerConfig.newUrl(values) + "settings/index"; msg("Saved, the server restarts ...", false); ServerConfig.waitFor(url, msg); }); } /* ---- Cluster Status ---- */ function loadClusterStatus() { currentView = loadClusterStatus; Loading Loading @@ -1353,6 +1468,8 @@ window.onload = function() { if (data.superadmin) { document.getElementById('nav-domains').style.display = ''; } if (data.serverconfig) document.getElementById('nav-server').style.display = ''; if (data.superadmin || gpo[GPO_MANAGE_USERS]) document.getElementById('nav-users').style.display = ''; if (data.superadmin || gpo[GPO_MANAGE_GROUPS]) Loading data/serverconfig.js 0 → 100644 +167 −0 Changes for data/serverconfig.js: 167 added lines, 0 removed lines. Original line number Diff line number Diff line /* Server settings form, shared by the setup wizard (setup.html, no config * file yet) and the "Server" view of the admin console. The keys are the * config paths below FENRIRDS/ that the root config helper accepts. */ var ServerConfig = (function() { var SECTIONS = [ { title: "Web interface", fields: [ { key: "BIND", label: "Bind address", hint: "0.0.0.0 = all interfaces" }, { key: "PORT", label: "Port", type: "number" }, { key: "MAXCONN", label: "Max. connections", type: "number" }, { key: "SSL/CERT", label: "TLS certificate", hint: "optional, a file below the config directory" }, { key: "SSL/KEY", label: "TLS key", hint: "optional, a file below the config directory" } ]}, { title: "Domain controller", fields: [ { key: "DNS/HOSTNAME", label: "DC host name", hint: "e.g. dc1.example.com" }, { key: "DNS/ADDRESS", label: "DC IP address", hint: "the address clients reach this server on" }, { key: "AD/ADMIN_PASSWORD", label: "Administrator password", type: "secret", hint: "set on the Administrator account when a domain is provisioned" } ]}, { title: "Service ports", fields: [ { key: "KDC/BIND", label: "Kerberos bind" }, { key: "KDC/PORT", label: "Kerberos port", type: "number" }, { key: "AD/BIND", label: "LDAP bind" }, { key: "AD/PORT", label: "LDAP port", type: "number" }, { key: "AD/MAXCONN", label: "LDAP max. connections", type: "number" }, { key: "DNS/BIND", label: "DNS bind" }, { key: "DNS/PORT", label: "DNS port", type: "number" }, { key: "SMB/BIND", label: "SMB bind" }, { key: "SMB/PORT", label: "SMB port", type: "number" } ]}, { title: "Replication", fields: [ { key: "REPLICATION/ROLE", label: "Role", type: "select", options: ["none", "master", "slave"] }, { key: "REPLICATION/SLAVES", label: "Slaves", type: "list", role: "master", hint: "one name:key per line; a name alone keeps its key" }, { key: "REPLICATION/NAME", label: "Slave name", role: "slave" }, { key: "REPLICATION/KEY", label: "Slave key", type: "secret", role: "slave" }, { key: "REPLICATION/MASTERS", label: "Masters", type: "list", role: "slave", hint: "one URL per line, e.g. https://master:9090" }, { key: "REPLICATION/INTERVAL", label: "Interval (s)", type: "number", role: "slave" }, { key: "REPLICATION/SSLVERIFY", label: "Verify TLS", type: "bool", role: "slave" } ]}, { title: "Cluster", fields: [ { key: "CLUSTER/ENABLE", label: "Enabled", type: "bool", hint: "needs TLS (certificate and key above)" }, { key: "CLUSTER/BIND", label: "Bind address" }, { key: "CLUSTER/PORT", label: "Port", type: "number" }, { key: "CLUSTER/AUTH_NAME", label: "Auth name" }, { key: "CLUSTER/AUTH_KEY", label: "Auth key", type: "secret" }, { key: "CLUSTER/DATA_BLOCKS", label: "Data blocks", type: "number" }, { key: "CLUSTER/PARITY_BLOCKS", label: "Parity blocks", type: "number" }, { key: "CLUSTER/SSLVERIFY", label: "Verify TLS", type: "bool" }, { key: "CLUSTER/PEERS", label: "Peers", type: "list", hint: "one ip:port per line, this node included" } ]}, { title: "Maintenance", fields: [ { key: "VACUUM_INTERVAL", label: "Vacuum interval (h)", type: "number" } ]} ]; function esc(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function fid(key) { return "srvcfg_" + key.replace(/\//g, "_"); } function fieldHtml(f, values, secrets) { var id = fid(f.key), v = values[f.key], input; if (f.type === "select") { input = '<select id="' + id + '" onchange="ServerConfig.updateRole()">'; for (var i = 0; i < f.options.length; i++) { var o = f.options[i]; input += '<option value="' + o + '"' + ((v || "none") === o ? ' selected' : '') + '>' + o + '</option>'; } input += '</select>'; } else if (f.type === "bool") { input = '<input type="checkbox" id="' + id + '"' + (v === "true" ? ' checked' : '') + '>'; } else if (f.type === "list") { input = '<textarea id="' + id + '" rows="3">' + esc((v || []).join("\n")) + '</textarea>'; } else if (f.type === "secret") { var isSet = secrets && secrets[f.key]; input = '<input type="password" id="' + id + '" autocomplete="new-password"' + (isSet ? ' placeholder="unchanged"' : '') + '>'; } else { input = '<input type="' + (f.type === "number" ? 'number' : 'text') + '" id="' + id + '" value="' + esc(v) + '">'; } return '<tr' + (f.role ? ' class="srvcfg-role" data-role="' + f.role + '"' : '') + '>' + '<td><label for="' + id + '">' + esc(f.label) + '</label></td>' + '<td>' + input + (f.hint ? '<br><small>' + esc(f.hint) + '</small>' : '') + '</td></tr>'; } /* data: the reply of getconfig / setup/api/get */ function render(data) { var values = (data && data.values) || {}, secrets = (data && data.secrets) || {}; var html = '<div class="srvcfg">'; for (var s = 0; s < SECTIONS.length; s++) { html += '<fieldset><legend>' + esc(SECTIONS[s].title) + '</legend><table>'; for (var i = 0; i < SECTIONS[s].fields.length; i++) html += fieldHtml(SECTIONS[s].fields[i], values, secrets); html += '</table></fieldset>'; } return html + '</div>'; } function role() { var el = document.getElementById(fid("REPLICATION/ROLE")); return el ? el.value : "none"; } function updateRole() { var r = role(), rows = document.querySelectorAll('.srvcfg-role'); for (var i = 0; i < rows.length; i++) rows[i].style.display = rows[i].getAttribute('data-role') === r ? '' : 'none'; } /* the values to send; fields of the other replication role are left out * so switching the role doesn't wipe them */ function collect() { var out = {}, r = role(); for (var s = 0; s < SECTIONS.length; s++) { for (var i = 0; i < SECTIONS[s].fields.length; i++) { var f = SECTIONS[s].fields[i]; if (f.role && f.role !== r) continue; var el = document.getElementById(fid(f.key)); if (!el) continue; if (f.type === "bool") out[f.key] = el.checked ? "true" : "false"; else if (f.type === "list") out[f.key] = el.value.split("\n").map(function(l) { return l.trim(); }) .filter(function(l) { return l !== ""; }); else out[f.key] = el.value; } } return out; } /* where the web interface will answer once the server restarted */ function newUrl(values) { var ssl = values["SSL/CERT"] && values["SSL/KEY"]; var port = values["PORT"] || window.location.port; return (ssl ? "https://" : "http://") + window.location.hostname + (port ? ":" + port : "") + "/"; } /* polls url until the restarted server answers, then opens it */ function waitFor(url, onStatus) { var tries = 0; function attempt() { if (++tries > 60) { onStatus("The server did not come back. Check the log and open " + url + " by hand.", true); return; } fetch(url, { mode: "no-cors", cache: "no-store" }).then(function() { window.location.href = url; }, function() { setTimeout(attempt, 2000); }); } /* the old server answers for about a second after saving */ setTimeout(attempt, 3000); } return { render: render, updateRole: updateRole, collect: collect, newUrl: newUrl, waitFor: waitFor }; })(); data/setup.html 0 → 100644 +68 −0 Changes for data/setup.html: 68 added lines, 0 removed lines. Original line number Diff line number Diff line <!DOCTYPE html> <html lang="en"> <head> <title>FenrirDirectoryServer Setup</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/style.css"> <script src="/serverconfig.js"></script> </head> <body> <div id="setup"> <p><span>FenrirDirectoryServer Setup</span></p> <p>There is no configuration file yet. Enter the server settings; they are written to <span class="mono" id="cfgpath"></span> and the server restarts. After that you create the first administrator account.</p> <div id="setupform">Loading…</div> <p id="setupmsg"></p> <input type="submit" id="setupsave" value="Save and start" onclick="saveSetup()" disabled> </div> <script> function setupMsg(text, isError) { var el = document.getElementById("setupmsg"); el.className = isError ? "msg-error" : "msg-ok"; el.textContent = text; } function saveSetup() { var values = ServerConfig.collect(); var btn = document.getElementById("setupsave"); btn.disabled = true; fetch("/setup/api/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: values }) }).then(function(r) { return r.json().then(function(j) { return { ok: r.ok, data: j }; }); }).then(function(res) { if (!res.ok) { setupMsg(res.data.error || "saving failed", true); btn.disabled = false; return; } var url = ServerConfig.newUrl(values); setupMsg("Configuration saved, the server restarts and opens " + url + " ...", false); ServerConfig.waitFor(url, setupMsg); }).catch(function(e) { setupMsg("saving failed: " + e, true); btn.disabled = false; }); } fetch("/setup/api/get", { cache: "no-store" }).then(function(r) { return r.json().then(function(j) { return { ok: r.ok, data: j }; }); }).then(function(res) { if (!res.ok) { document.getElementById("setupform").textContent = ""; setupMsg(res.data.error || "loading the defaults failed", true); return; } document.getElementById("cfgpath").textContent = res.data.path || ""; document.getElementById("setupform").innerHTML = ServerConfig.render(res.data); ServerConfig.updateRole(); document.getElementById("setupsave").disabled = false; }).catch(function(e) { setupMsg("loading the defaults failed: " + e, true); }); </script> </body> </html> data/style.css +36 −0 Changes for data/style.css: 36 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -261,6 +261,42 @@ select[multiple] option { padding: 0.3em 0.5em; border-radius: 4px; } #login input[type="submit"], #wizzard input[type="submit"] { width: 100%; padding: 0.6em; margin-top: 0.5em; } /* ---- Setup wizard / server settings ---- */ #setup { width: min(720px, calc(100vw - 32px)); margin: 6vh auto 4vh; padding: 2em; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); } #setup p span { font-size: 1.75em !important; font-weight: 700; } #setup input[type="submit"] { width: 100%; padding: 0.6em; margin-top: 0.5em; } .srvcfg fieldset { border: 1px solid var(--border); border-radius: var(--radius); margin: 0 0 1em; padding: 0.5em 1em 0.75em; } .srvcfg legend { font-weight: 600; padding: 0 0.4em; } .srvcfg table { width: 100%; border-collapse: collapse; } .srvcfg td { padding: 0.3em 0.5em 0.3em 0; vertical-align: top; } .srvcfg td:first-child { width: 35%; color: var(--text-muted); padding-top: 0.6em; } .srvcfg input[type="text"], .srvcfg input[type="number"], .srvcfg input[type="password"], .srvcfg select, .srvcfg textarea { width: 100%; } .srvcfg small { color: var(--text-muted); } #login > span { display: block; margin-top: 1em; Loading Loading
CMakeLists.txt +5 −2 Changes for CMakeLists.txt: 5 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,9 +13,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_ENABLE_EXPORTS 1) set(DATAPATH "${CMAKE_INSTALL_DATADIR}/fenrirds" CACHE FILEPATH "path where stored webinterface files are located") set(PLUGINPATH "${CMAKE_INSTALL_LIBDIR}/fenrirds" CACHE FILEPATH "default plugin search path") set(PLUGINPATH "${CMAKE_INSTALL_FULL_LIBDIR}/fenrirds" CACHE FILEPATH "default plugin search path") set(STATEPATH "${CMAKE_INSTALL_FULL_LOCALSTATEDIR}/lib/fenrirds" CACHE FILEPATH "default path of the databases in a config written by the setup") add_definitions(-D${CMAKE_HOST_SYSTEM_NAME} -DDATAPATH="${DATAPATH}" -DPLUGINPATH="${PLUGINPATH}") add_definitions(-D${CMAKE_HOST_SYSTEM_NAME} -DDATAPATH="${DATAPATH}" -DPLUGINPATH="${PLUGINPATH}" -DSTATEPATH="${STATEPATH}") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") Loading Loading @@ -86,6 +87,8 @@ install(FILES data/fenrirds.js data/style.css data/wizzard.html data/setup.html data/serverconfig.js data/login.html data/index.html data/fenrirds.png Loading
data/admin.html +117 −0 Changes for data/admin.html: 117 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ <head> <title>FenrirDirectoryServer Admin</title> <link rel="stylesheet" href="/style.css"> <script src="/serverconfig.js"></script> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> .admin-table { border-collapse: separate; border-spacing: 0; width: 100%; margin: 1em 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } Loading Loading @@ -68,6 +69,7 @@ <li id="nav-services" style="display:none"><a href="#" onclick="loadServices(); return false;">Services</a></li> <li id="nav-gpos"><a href="#" onclick="loadGpos(); return false;">Policies</a></li> <li id="nav-sessions"><a href="#" onclick="loadSessions(); return false;">Sessions</a></li> <li id="nav-server" style="display:none"><a href="#" onclick="loadServerConfig(); return false;">Server</a></li> <li id="nav-cluster" style="display:none"><a href="#" onclick="loadClusterStatus(); return false;">Cluster</a></li> <li id="nav-export" style="display:none"><a href="#" onclick="loadExport(); return false;">Export/Import</a></li> <li class="nav-logout"><a href="#" onclick="doLogout(); return false;">Logout</a></li> Loading Loading @@ -1244,6 +1246,119 @@ function submitEditGpo(gpid) { }); } /* ---- Server settings (config file, written by the root config helper) ---- */ function loadServerConfig() { currentView = loadServerConfig; setActiveNav('nav-server'); document.getElementById("domain-select").innerHTML = ''; ajaxGet("/settings/admin/api/getconfig", function(err, data) { if (err) { showError(err); return; } var html = '<div class="view-header"><h3>Server</h3></div>'; html += '<p>Settings in <span class="mono">' + escapeHtml(data.path) + '</span>. ' + 'Port, address and TLS changes take effect after a restart.</p>'; html += ServerConfig.render(data); html += '<p id="srvcfgmsg"></p>'; html += '<ul class="toolbar">'; html += '<li><a class="button" href="#" onclick="saveServerConfig(false); return false;">Save</a></li>'; html += '<li><a class="button" href="#" onclick="saveServerConfig(true); return false;">Save and restart</a></li>'; html += '</ul>'; html += '<h4>Unconfigured nodes</h4>'; html += '<p>Nodes in setup mode on the local network (mDNS). Adding one sends it this node\'s ' + 'replication or cluster settings, including their keys, over plain HTTP.</p>'; html += '<ul class="toolbar"><li><a class="button" href="#" onclick="discoverNodes(); return false;">Search</a></li></ul>'; html += '<div id="nodelist"></div><p id="nodemsg"></p>'; setContent(html); ServerConfig.updateRole(); }); } var g_foundNodes = []; function nodeMsg(text, isError) { var el = document.getElementById("nodemsg"); if (!el) return; el.className = isError ? "msg-error" : "msg-ok"; el.textContent = text; } function discoverNodes() { document.getElementById("nodelist").innerHTML = '<p>Searching…</p>'; nodeMsg("", false); ajaxGet("/settings/admin/api/discovernodes", function(err, data) { if (err) { document.getElementById("nodelist").innerHTML = ''; nodeMsg(err, true); return; } g_foundNodes = data.nodes || []; if (!g_foundNodes.length) { document.getElementById("nodelist").innerHTML = '<p>No unconfigured node found.</p>'; return; } var html = '<div class="table-wrap"><table class="admin-table"><tr><th>Host</th><th>Address</th><th>ID</th><th></th></tr>'; for (var i = 0; i < g_foundNodes.length; i++) { var n = g_foundNodes[i]; html += '<tr><td>' + escapeHtml(n.host || n.instance) + '</td>' + '<td class="mono">' + escapeHtml(n.address + ':' + n.port) + '</td>' + '<td class="mono">' + escapeHtml(n.id) + '</td><td class="actions">' + '<a class="button" href="#" onclick="joinNode(' + i + ', \'slave\'); return false;">Add as replication slave</a> '; if (g_permissions && g_permissions.cluster) html += '<a class="button" href="#" onclick="joinNode(' + i + ', \'cluster\'); return false;">Add to cluster</a>'; html += '</td></tr>'; } document.getElementById("nodelist").innerHTML = html + '</table></div>'; }); } function joinNode(idx, mode) { var n = g_foundNodes[idx]; var what = mode === 'cluster' ? 'Add ' + (n.host || n.address) + ' (' + n.address + ') to the cluster? All cluster nodes restart.' : 'Make ' + (n.host || n.address) + ' (' + n.address + ') a replication slave of this node?'; if (!confirm(what + '\n\nOnly continue if you recognise this node (ID ' + n.id + ').')) return; nodeMsg("Configuring " + n.address + " ...", false); ajaxPost("/settings/admin/api/joinnode", { address: n.address, port: n.port, host: n.host, mode: mode }, function(err, data) { if (err) { var m = err; try { m = JSON.parse(err.substring(err.indexOf(':') + 2)).error || err; } catch (e) {} nodeMsg(m, true); return; } var text = n.address + ' is configured and restarts' + (data.name ? ' as slave "' + data.name + '"' : '') + '.'; if (data.warnings && data.warnings.length) text += ' Note: ' + data.warnings.join('; '); if (data.restart) { nodeMsg(text + ' This node restarts too ...', false); ServerConfig.waitFor(window.location.protocol + '//' + window.location.host + '/settings/index', nodeMsg); } else { nodeMsg(text, false); } }); } function saveServerConfig(restart) { var values = ServerConfig.collect(); var msg = function(text, isError) { var el = document.getElementById("srvcfgmsg"); if (!el) return; el.className = isError ? "msg-error" : "msg-ok"; el.textContent = text; }; ajaxPost("/settings/admin/api/setconfig", { values: values, restart: restart }, function(err, data) { if (err) { var m = err; try { m = JSON.parse(err.substring(err.indexOf(':') + 2)).error || err; } catch (e) {} msg(m, true); return; } if (!restart) { msg("Saved. The changes take effect after the next restart.", false); return; } var url = ServerConfig.newUrl(values) + "settings/index"; msg("Saved, the server restarts ...", false); ServerConfig.waitFor(url, msg); }); } /* ---- Cluster Status ---- */ function loadClusterStatus() { currentView = loadClusterStatus; Loading Loading @@ -1353,6 +1468,8 @@ window.onload = function() { if (data.superadmin) { document.getElementById('nav-domains').style.display = ''; } if (data.serverconfig) document.getElementById('nav-server').style.display = ''; if (data.superadmin || gpo[GPO_MANAGE_USERS]) document.getElementById('nav-users').style.display = ''; if (data.superadmin || gpo[GPO_MANAGE_GROUPS]) Loading
data/serverconfig.js 0 → 100644 +167 −0 Changes for data/serverconfig.js: 167 added lines, 0 removed lines. Original line number Diff line number Diff line /* Server settings form, shared by the setup wizard (setup.html, no config * file yet) and the "Server" view of the admin console. The keys are the * config paths below FENRIRDS/ that the root config helper accepts. */ var ServerConfig = (function() { var SECTIONS = [ { title: "Web interface", fields: [ { key: "BIND", label: "Bind address", hint: "0.0.0.0 = all interfaces" }, { key: "PORT", label: "Port", type: "number" }, { key: "MAXCONN", label: "Max. connections", type: "number" }, { key: "SSL/CERT", label: "TLS certificate", hint: "optional, a file below the config directory" }, { key: "SSL/KEY", label: "TLS key", hint: "optional, a file below the config directory" } ]}, { title: "Domain controller", fields: [ { key: "DNS/HOSTNAME", label: "DC host name", hint: "e.g. dc1.example.com" }, { key: "DNS/ADDRESS", label: "DC IP address", hint: "the address clients reach this server on" }, { key: "AD/ADMIN_PASSWORD", label: "Administrator password", type: "secret", hint: "set on the Administrator account when a domain is provisioned" } ]}, { title: "Service ports", fields: [ { key: "KDC/BIND", label: "Kerberos bind" }, { key: "KDC/PORT", label: "Kerberos port", type: "number" }, { key: "AD/BIND", label: "LDAP bind" }, { key: "AD/PORT", label: "LDAP port", type: "number" }, { key: "AD/MAXCONN", label: "LDAP max. connections", type: "number" }, { key: "DNS/BIND", label: "DNS bind" }, { key: "DNS/PORT", label: "DNS port", type: "number" }, { key: "SMB/BIND", label: "SMB bind" }, { key: "SMB/PORT", label: "SMB port", type: "number" } ]}, { title: "Replication", fields: [ { key: "REPLICATION/ROLE", label: "Role", type: "select", options: ["none", "master", "slave"] }, { key: "REPLICATION/SLAVES", label: "Slaves", type: "list", role: "master", hint: "one name:key per line; a name alone keeps its key" }, { key: "REPLICATION/NAME", label: "Slave name", role: "slave" }, { key: "REPLICATION/KEY", label: "Slave key", type: "secret", role: "slave" }, { key: "REPLICATION/MASTERS", label: "Masters", type: "list", role: "slave", hint: "one URL per line, e.g. https://master:9090" }, { key: "REPLICATION/INTERVAL", label: "Interval (s)", type: "number", role: "slave" }, { key: "REPLICATION/SSLVERIFY", label: "Verify TLS", type: "bool", role: "slave" } ]}, { title: "Cluster", fields: [ { key: "CLUSTER/ENABLE", label: "Enabled", type: "bool", hint: "needs TLS (certificate and key above)" }, { key: "CLUSTER/BIND", label: "Bind address" }, { key: "CLUSTER/PORT", label: "Port", type: "number" }, { key: "CLUSTER/AUTH_NAME", label: "Auth name" }, { key: "CLUSTER/AUTH_KEY", label: "Auth key", type: "secret" }, { key: "CLUSTER/DATA_BLOCKS", label: "Data blocks", type: "number" }, { key: "CLUSTER/PARITY_BLOCKS", label: "Parity blocks", type: "number" }, { key: "CLUSTER/SSLVERIFY", label: "Verify TLS", type: "bool" }, { key: "CLUSTER/PEERS", label: "Peers", type: "list", hint: "one ip:port per line, this node included" } ]}, { title: "Maintenance", fields: [ { key: "VACUUM_INTERVAL", label: "Vacuum interval (h)", type: "number" } ]} ]; function esc(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function fid(key) { return "srvcfg_" + key.replace(/\//g, "_"); } function fieldHtml(f, values, secrets) { var id = fid(f.key), v = values[f.key], input; if (f.type === "select") { input = '<select id="' + id + '" onchange="ServerConfig.updateRole()">'; for (var i = 0; i < f.options.length; i++) { var o = f.options[i]; input += '<option value="' + o + '"' + ((v || "none") === o ? ' selected' : '') + '>' + o + '</option>'; } input += '</select>'; } else if (f.type === "bool") { input = '<input type="checkbox" id="' + id + '"' + (v === "true" ? ' checked' : '') + '>'; } else if (f.type === "list") { input = '<textarea id="' + id + '" rows="3">' + esc((v || []).join("\n")) + '</textarea>'; } else if (f.type === "secret") { var isSet = secrets && secrets[f.key]; input = '<input type="password" id="' + id + '" autocomplete="new-password"' + (isSet ? ' placeholder="unchanged"' : '') + '>'; } else { input = '<input type="' + (f.type === "number" ? 'number' : 'text') + '" id="' + id + '" value="' + esc(v) + '">'; } return '<tr' + (f.role ? ' class="srvcfg-role" data-role="' + f.role + '"' : '') + '>' + '<td><label for="' + id + '">' + esc(f.label) + '</label></td>' + '<td>' + input + (f.hint ? '<br><small>' + esc(f.hint) + '</small>' : '') + '</td></tr>'; } /* data: the reply of getconfig / setup/api/get */ function render(data) { var values = (data && data.values) || {}, secrets = (data && data.secrets) || {}; var html = '<div class="srvcfg">'; for (var s = 0; s < SECTIONS.length; s++) { html += '<fieldset><legend>' + esc(SECTIONS[s].title) + '</legend><table>'; for (var i = 0; i < SECTIONS[s].fields.length; i++) html += fieldHtml(SECTIONS[s].fields[i], values, secrets); html += '</table></fieldset>'; } return html + '</div>'; } function role() { var el = document.getElementById(fid("REPLICATION/ROLE")); return el ? el.value : "none"; } function updateRole() { var r = role(), rows = document.querySelectorAll('.srvcfg-role'); for (var i = 0; i < rows.length; i++) rows[i].style.display = rows[i].getAttribute('data-role') === r ? '' : 'none'; } /* the values to send; fields of the other replication role are left out * so switching the role doesn't wipe them */ function collect() { var out = {}, r = role(); for (var s = 0; s < SECTIONS.length; s++) { for (var i = 0; i < SECTIONS[s].fields.length; i++) { var f = SECTIONS[s].fields[i]; if (f.role && f.role !== r) continue; var el = document.getElementById(fid(f.key)); if (!el) continue; if (f.type === "bool") out[f.key] = el.checked ? "true" : "false"; else if (f.type === "list") out[f.key] = el.value.split("\n").map(function(l) { return l.trim(); }) .filter(function(l) { return l !== ""; }); else out[f.key] = el.value; } } return out; } /* where the web interface will answer once the server restarted */ function newUrl(values) { var ssl = values["SSL/CERT"] && values["SSL/KEY"]; var port = values["PORT"] || window.location.port; return (ssl ? "https://" : "http://") + window.location.hostname + (port ? ":" + port : "") + "/"; } /* polls url until the restarted server answers, then opens it */ function waitFor(url, onStatus) { var tries = 0; function attempt() { if (++tries > 60) { onStatus("The server did not come back. Check the log and open " + url + " by hand.", true); return; } fetch(url, { mode: "no-cors", cache: "no-store" }).then(function() { window.location.href = url; }, function() { setTimeout(attempt, 2000); }); } /* the old server answers for about a second after saving */ setTimeout(attempt, 3000); } return { render: render, updateRole: updateRole, collect: collect, newUrl: newUrl, waitFor: waitFor }; })();
data/setup.html 0 → 100644 +68 −0 Changes for data/setup.html: 68 added lines, 0 removed lines. Original line number Diff line number Diff line <!DOCTYPE html> <html lang="en"> <head> <title>FenrirDirectoryServer Setup</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/style.css"> <script src="/serverconfig.js"></script> </head> <body> <div id="setup"> <p><span>FenrirDirectoryServer Setup</span></p> <p>There is no configuration file yet. Enter the server settings; they are written to <span class="mono" id="cfgpath"></span> and the server restarts. After that you create the first administrator account.</p> <div id="setupform">Loading…</div> <p id="setupmsg"></p> <input type="submit" id="setupsave" value="Save and start" onclick="saveSetup()" disabled> </div> <script> function setupMsg(text, isError) { var el = document.getElementById("setupmsg"); el.className = isError ? "msg-error" : "msg-ok"; el.textContent = text; } function saveSetup() { var values = ServerConfig.collect(); var btn = document.getElementById("setupsave"); btn.disabled = true; fetch("/setup/api/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ values: values }) }).then(function(r) { return r.json().then(function(j) { return { ok: r.ok, data: j }; }); }).then(function(res) { if (!res.ok) { setupMsg(res.data.error || "saving failed", true); btn.disabled = false; return; } var url = ServerConfig.newUrl(values); setupMsg("Configuration saved, the server restarts and opens " + url + " ...", false); ServerConfig.waitFor(url, setupMsg); }).catch(function(e) { setupMsg("saving failed: " + e, true); btn.disabled = false; }); } fetch("/setup/api/get", { cache: "no-store" }).then(function(r) { return r.json().then(function(j) { return { ok: r.ok, data: j }; }); }).then(function(res) { if (!res.ok) { document.getElementById("setupform").textContent = ""; setupMsg(res.data.error || "loading the defaults failed", true); return; } document.getElementById("cfgpath").textContent = res.data.path || ""; document.getElementById("setupform").innerHTML = ServerConfig.render(res.data); ServerConfig.updateRole(); document.getElementById("setupsave").disabled = false; }).catch(function(e) { setupMsg("loading the defaults failed: " + e, true); }); </script> </body> </html>
data/style.css +36 −0 Changes for data/style.css: 36 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -261,6 +261,42 @@ select[multiple] option { padding: 0.3em 0.5em; border-radius: 4px; } #login input[type="submit"], #wizzard input[type="submit"] { width: 100%; padding: 0.6em; margin-top: 0.5em; } /* ---- Setup wizard / server settings ---- */ #setup { width: min(720px, calc(100vw - 32px)); margin: 6vh auto 4vh; padding: 2em; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); } #setup p span { font-size: 1.75em !important; font-weight: 700; } #setup input[type="submit"] { width: 100%; padding: 0.6em; margin-top: 0.5em; } .srvcfg fieldset { border: 1px solid var(--border); border-radius: var(--radius); margin: 0 0 1em; padding: 0.5em 1em 0.75em; } .srvcfg legend { font-weight: 600; padding: 0 0.4em; } .srvcfg table { width: 100%; border-collapse: collapse; } .srvcfg td { padding: 0.3em 0.5em 0.3em 0; vertical-align: top; } .srvcfg td:first-child { width: 35%; color: var(--text-muted); padding-top: 0.6em; } .srvcfg input[type="text"], .srvcfg input[type="number"], .srvcfg input[type="password"], .srvcfg select, .srvcfg textarea { width: 100%; } .srvcfg small { color: var(--text-muted); } #login > span { display: block; margin-top: 1em; Loading