diff --git a/editor/js/ui/projectUserSettings.js b/editor/js/ui/projectUserSettings.js index 6af0dcd59..021d47736 100644 --- a/editor/js/ui/projectUserSettings.js +++ b/editor/js/ui/projectUserSettings.js @@ -50,15 +50,27 @@ RED.projects.userSettings = (function() { .appendTo(title) .click(function(evt) { addKeyButton.attr('disabled',true); - addKeyDialog.slideDown(200, function() { - // addKeyDialog[0].scrollIntoView(); - }); + addKeyDialog.slideDown(200); + keyNameInput.focus(); + saveButton.attr('disabled',true); }); var validateForm = function() { var validName = /^[a-zA-Z0-9\-_]+$/.test(keyNameInput.val()); - saveButton.attr('disabled',!validName); + var passphrase = passphraseInput.val(); + var validPassphrase = passphrase.length === 0 || passphrase.length >= 8; + + saveButton.attr('disabled',!validName || !validPassphrase); keyNameInput.toggleClass('input-error',keyNameInputChanged&&!validName); + passphraseInput.toggleClass('input-error',!validPassphrase); + if (!validPassphrase) { + passphraseInputSubLabel.text("Passphrase too short"); + } else if (passphrase.length === 0) { + passphraseInputSubLabel.text("Optional"); + } else { + passphraseInputSubLabel.text(""); + } + if (popover) { popover.close(); popover = null; @@ -68,8 +80,8 @@ RED.projects.userSettings = (function() { var row = $('
').appendTo(container); var addKeyDialog = $('
').hide().appendTo(row); $('
').text('Generate SSH Key').appendTo(addKeyDialog); - - row = $('
').appendTo(addKeyDialog); + var addKeyDialogBody = $('
').appendTo(addKeyDialog); + row = $('').appendTo(addKeyDialogBody); $('').text('Name').appendTo(row); var keyNameInput = $('').appendTo(row).on("change keyup paste",function() { keyNameInputChanged = true; @@ -78,10 +90,10 @@ RED.projects.userSettings = (function() { var keyNameInputChanged = false; $('').appendTo(row).find("small"); - row = $('').appendTo(addKeyDialog); + row = $('').appendTo(addKeyDialogBody); $('').text('Passphrase').appendTo(row); passphraseInput = $('').appendTo(row).on("change keyup paste",validateForm); - $('').appendTo(row).find("small"); + var passphraseInputSubLabel = $('').appendTo(row).find("small"); var hideEditForm = function() { addKeyButton.attr('disabled',false); @@ -128,7 +140,7 @@ RED.projects.userSettings = (function() { done(error); }, 200: function(data) { - refreshSSHKeyList(); + refreshSSHKeyList(payload.name); done(); }, 400: { @@ -166,7 +178,7 @@ RED.projects.userSettings = (function() { utils.sendRequest(options); var formButtons = $('').appendTo(row); - $('') + $('') .appendTo(formButtons) .click(function(evt) { evt.preventDefault(); @@ -258,10 +270,13 @@ RED.projects.userSettings = (function() { ] }); }); + if (entry.expand) { + expandedRow = expandKey(container,entry); + } } }); - var refreshSSHKeyList = function() { + var refreshSSHKeyList = function(justAdded) { $.getJSON("settings/user/keys",function(result) { if (result.keys) { result.keys.sort(function(A,B) { @@ -269,6 +284,9 @@ RED.projects.userSettings = (function() { }); keyList.editableList('empty'); result.keys.forEach(function(key) { + if (key.name === justAdded) { + key.expand = true; + } keyList.editableList('addItem',key); }) } @@ -278,137 +296,10 @@ RED.projects.userSettings = (function() { } - - - function sendSSHKeyManagementAPI(type, param, overlay, successCallback, failCallback) { - var url; - var method; - var payload; - switch(type) { - case 'GET_KEY_LIST': - method = 'GET'; - url = "settings/user/keys"; - break; - case 'GET_KEY_DETAIL': - method = 'GET'; - url = "settings/user/keys/" + param; - break; - case 'GENERATE_KEY': - method = 'POST'; - url = "settings/user/keys"; - payload= param; - break; - case 'DELETE_KEY': - method = 'DELETE'; - url = "settings/user/keys/" + param; - break; - default: - console.error('Unexpected type....'); - return; - } - // var spinner = utils.addSpinnerOverlay(gitconfigContainer); - var spinner = overlay ? utils.addSpinnerOverlay(overlay) : null; - - var done = function(err) { - if ( spinner ) { - spinner.remove(); - } - if (err) { - console.log(err); - return; - } - }; - - console.log('method:', method); - console.log('url:', url); - - utils.sendRequest({ - url: url, - type: method, - responses: { - 0: function(error) { - if ( failCallback ) { - failCallback(error); - } - done(error); - }, - 200: function(data) { - if ( successCallback ) { - successCallback(data); - } - done(); - }, - 400: { - 'unexpected_error': function(error) { - console.log(error); - if ( failCallback ) { - failCallback(error); - } - done(error); - } - }, - } - },payload); - } - - var dialog; - var dialogBody; - function createPublicKeyDialog() { - dialog = $('
') - .appendTo("body") - .dialog({ - modal: true, - autoOpen: false, - width: 600, - resize: false, - open: function(e) { - $(this).parent().find(".ui-dialog-titlebar-close").hide(); - }, - close: function(e) { - - } - }); - dialogBody = dialog.find("form"); - dialog.dialog('option', 'title', 'SSH public key'); - dialog.dialog('option', 'buttons', [ - { - text: RED._("common.label.close"), - click: function() { - $( this ).dialog( "close" ); - } - }, - { - text: "Copy to Clipboard", - click: function() { - var target = document.getElementById('public-key-data'); - document.getSelection().selectAllChildren(target); - var ret = document.execCommand('copy'); - var msg = ret ? 'successful' : 'unsuccessful'; - console.log('Copy text command was ' + msg); - $( this ).dialog("close"); - } - } - ]); - dialog.dialog({position: { 'my': 'center', 'at': 'center', 'of': window }}); - var container = $('
'); - $('
').appendTo(container); - $('
').appendTo(container); - dialogBody.append(container); - } - - function setDialogContext(name, data) { - var title = dialog.find("div.projects-dialog-ssh-public-key-name"); - title.text(name); - var context = dialog.find("div.projects-dialog-ssh-public-key>pre"); - context.text(data); - } - function createSettingsPane(activeProject) { var pane = $('
'); createGitUserSection(pane); createSSHKeySection(pane); - - createPublicKeyDialog(); return pane; } diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index a5e59be56..0b819877a 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -539,9 +539,9 @@ RED.projects = (function() { $(".projects-dialog-screen-create-row-creds").hide(); $(".projects-dialog-screen-create-row-passphrase").show(); $(".projects-dialog-screen-create-row-sshkey").show(); - if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) { - valid = false; - } + // if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) { + // valid = false; + // } } else if (/^https?:\/\//.test(repo)) { $(".projects-dialog-screen-create-row-creds").show(); $(".projects-dialog-screen-create-row-passphrase").hide(); @@ -576,7 +576,7 @@ RED.projects = (function() { } } } - + $("#projects-dialog-create").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid); } @@ -757,14 +757,21 @@ RED.projects = (function() { projectRepoPasswordInput = $('').appendTo(subrow); row = $('
').hide().appendTo(container); - $('').appendTo(row); - projectRepoSSHKeySelect = createSSHKeyList({ - height: "120px", - selectAction: function(entry, header) { - $('.projects-dialog-sshkey-list-entry').removeClass('selected'); - header.addClass('selected'); + + $('').appendTo(row); + projectRepoSSHKeySelect = $("
'+ - '
'+ '
'); + + var isSSH = false; + if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(url)) { + isSSH = true; + var row = $('
').appendTo(message); + $('').appendTo(row); + var projectRepoSSHKeySelect = $('').appendTo(row); + } else { + $('
'+ + '
').appendTo(message); + } + var notification = RED.notify(message,{ type:"error", fixed: true, @@ -1327,14 +1280,15 @@ RED.projects = (function() { },{ text: $(' Retry'), click: function() { - var username = $('#projects-user-auth-username').val(); - var password = $('#projects-user-auth-password').val(); body = body || {}; - var authBody = {git:{remotes:{}}}; - authBody.git.remotes[options.remote||'origin'] = { - username: username, - password: password - }; + var authBody = {}; + if (isSSH) { + authBody.keyFile = $('#projects-user-auth-key').val(); + authBody.passphrase = $('#projects-user-auth-passphrase').val(); + } else { + authBody.username = $('#projects-user-auth-username').val(); + authBody.password = $('#projects-user-auth-password').val(); + } var done = function(err) { if (err) { console.log("Failed to update auth"); @@ -1346,7 +1300,7 @@ RED.projects = (function() { } sendRequest({ - url: "projects/"+activeProject.name, + url: "projects/"+activeProject.name+"/remotes/"+(options.remote||'origin'), type: "PUT", responses: { 0: function(error) { @@ -1361,7 +1315,7 @@ RED.projects = (function() { } }, } - },authBody); + },{auth:authBody}); } } ] @@ -1553,9 +1507,7 @@ RED.projects = (function() { var projectsAPI = { sendRequest:sendRequest, createBranchList:createBranchList, - addSpinnerOverlay:addSpinnerOverlay, - createSSHKeyList:createSSHKeyList, - refreshSSHKeyList:refreshSSHKeyList + addSpinnerOverlay:addSpinnerOverlay }; RED.projects.settings.init(projectsAPI); RED.projects.userSettings.init(projectsAPI); diff --git a/editor/sass/projects.scss b/editor/sass/projects.scss index ce10a3466..e10198a7c 100644 --- a/editor/sass/projects.scss +++ b/editor/sass/projects.scss @@ -816,6 +816,7 @@ div.projects-dialog-ssh-public-key { } } .projects-dialog-list-dialog { + position: relative; margin-top: 10px; margin-bottom: 20px; background: white; diff --git a/red/api/editor/projects/index.js b/red/api/editor/projects/index.js index a3fdc29a2..072abc8d5 100644 --- a/red/api/editor/projects/index.js +++ b/red/api/editor/projects/index.js @@ -512,6 +512,23 @@ module.exports = { }); }); + // Update a remote + app.put("/:id/remotes/:remoteName", needsPermission("projects.write"), function(req,res) { + var projectName = req.params.id; + var remoteName = req.params.remoteName; + runtime.storage.projects.updateRemote(req.user, projectName, remoteName, req.body).then(function(data) { + res.status(204).end(); + }) + .catch(function(err) { + if (err.code) { + res.status(400).json({error:err.code, message: err.message}); + } else { + res.status(400).json({error:"unexpected_error", message:err.toString()}); + } + }); + + }); + return app; } } diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js index abfd49aa8..f4c8a3982 100644 --- a/red/api/editor/sshkeys.js +++ b/red/api/editor/sshkeys.js @@ -63,12 +63,15 @@ module.exports = { // console.log('username:', username); runtime.storage.sshkeys.getSSHKey(username, req.params.id) .then(function(data) { - res.json({ - publickey: data - }); + if (data) { + res.json({ + publickey: data + }); + } else { + res.status(404).end(); + } }) .catch(function(err) { - console.log(err.stack); if (err.code) { res.status(400).json({error:err.code, message: err.message}); } else { @@ -90,7 +93,6 @@ module.exports = { }); }) .catch(function(err) { - console.log(err.stack); if (err.code) { res.status(400).json({error:err.code, message: err.message}); } else { @@ -107,11 +109,10 @@ module.exports = { app.delete("/:id", needsPermission("settings.write"), function(req,res) { var username = getUsername(req.user); runtime.storage.sshkeys.deleteSSHKey(username, req.params.id) - .then(function(ret) { + .then(function() { res.status(204).end(); }) .catch(function(err) { - console.log(err.stack); if (err.code) { res.status(400).json({error:err.code, message: err.message}); } else { diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 579fc02f9..934b97ca5 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -120,12 +120,14 @@ Project.prototype.loadRemotes = function() { }).then(function() { var allRemotes = Object.keys(project.remotes); var match = ""; - allRemotes.forEach(function(remote) { - if (project.branches.remote.indexOf(remote) === 0 && match.length < remote.length) { - match = remote; - } - }); - project.currentRemote = project.parseRemoteBranch(project.branches.remote).remote; + if (project.branches.remote) { + allRemotes.forEach(function(remote) { + if (project.branches.remote.indexOf(remote) === 0 && match.length < remote.length) { + match = remote; + } + }); + project.currentRemote = project.parseRemoteBranch(project.branches.remote).remote; + } }); } @@ -541,8 +543,21 @@ Project.prototype.addRemote = function(user,remote,options) { }); } Project.prototype.updateRemote = function(user,remote,options) { - // TODO: once the sshkey support is added, move the updating of remotes, - // including their auth details, down here. + var username; + if (!user) { + username = "_"; + } else { + username = user.username; + } + + if (options.auth) { + var url = this.remotes[remote].fetch; + if (options.auth.keyFile) { + options.auth.key_path = fspath.join(projectsDir, ".sshkeys", ((username === '_')?'__default':username) + '_' + options.auth.keyFile); + } + authCache.set(this.name,url,username,options.auth); + } + return Promise.resolve(); } Project.prototype.removeRemote = function(user, remote) { // TODO: if this was the last remote using this url, then remove the authCache @@ -764,7 +779,7 @@ function createProject(user, metadata) { auth = authCache.get(project,originRemote.url,username); } else if (originRemote.hasOwnProperty("key_file") && originRemote.hasOwnProperty("passphrase")) { - var key_file_name = (username === '_') ? '.default' + '_' + originRemote.key_file : username + '_' + originRemote.key_file; + var key_file_name = (username === '_') ? '__default' + '_' + originRemote.key_file : username + '_' + originRemote.key_file; authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name key_path: fspath.join(projectsDir, ".sshkeys", key_file_name), passphrase: originRemote.passphrase diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js index 5d9218bdf..bb012aa09 100644 --- a/red/runtime/storage/localfilesystem/projects/index.js +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -266,6 +266,10 @@ function removeRemote(user, project, remote) { checkActiveProject(project); return activeProject.removeRemote(user, remote); } +function updateRemote(user, project, remote, body) { + checkActiveProject(project); + return activeProject.updateRemote(user, remote, body); +} function getActiveProject(user) { return activeProject; @@ -491,6 +495,7 @@ module.exports = { getRemotes: getRemotes, addRemote: addRemote, removeRemote: removeRemote, + updateRemote: updateRemote, getFlows: getFlows, saveFlows: saveFlows, diff --git a/red/runtime/storage/localfilesystem/sshkeys.js b/red/runtime/storage/localfilesystem/sshkeys.js index d2d67daf3..fa4916ba1 100644 --- a/red/runtime/storage/localfilesystem/sshkeys.js +++ b/red/runtime/storage/localfilesystem/sshkeys.js @@ -80,6 +80,8 @@ function getSSHKey(username, name) { return checkSSHKeyFileAndGetPublicKeyFileName(username, name) .then(function(publicSSHKeyPath) { return fs.readFile(publicSSHKeyPath, 'utf-8'); + }).catch(function() { + return null; }); } @@ -89,29 +91,32 @@ function generateSSHKey(username, options) { return checkExistSSHKeyFiles(username, name) .then(function(result) { if ( result ) { - throw new Error('Some SSH Keyfile exists'); - } - else { + var e = new Error("SSH Key name exists"); + e.code = "key_exists"; + throw e; + } else { var comment = options.comment || ""; var password = options.password || ""; + if (password.length > 0 && password.length < 5) { + var e2 = new Error("SSH Key passphrase too short"); + e2.code = "key_passphrase_too_short"; + throw e2; + } var size = options.size || 2048; var sshKeyFileBasename = username + '_' + name; var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); - return generateSSHKeyPair(privateKeyFilePath, comment, password, size) - .then(function() { - return name; - }); + return generateSSHKeyPair(name, privateKeyFilePath, comment, password, size) } }) - .then(function(keyfile_name) { - return checkSSHKeyFileAndGetPublicKeyFileName(username, name) - .then(function() { - return keyfile_name; - }) - .catch(function() { - throw new Error('Failed to generate ssh key files'); - }); - }); + // .then(function(keyfile_name) { + // return checkSSHKeyFileAndGetPublicKeyFileName(username, name) + // .then(function() { + // return keyfile_name; + // }) + // .catch(function(err) { + // throw new Error('Failed to generate ssh key files'); + // }); + // }); } function deleteSSHKey(username, name) { @@ -125,7 +130,7 @@ function checkExistSSHKeyFiles(username, name) { var sshKeyFileBasename = username + '_' + name; var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); - return Promise.race([ + return Promise.all([ fs.access(privateKeyFilePath, (fs.constants || fs).R_OK), fs.access(publicKeyFilePath , (fs.constants || fs).R_OK) ]) @@ -157,13 +162,11 @@ function deleteSSHKeyFiles(username, name) { return Promise.all([ fs.remove(privateKeyFilePath), fs.remove(publicKeyFilePath) - ]) - .then(function(retArray) { - return true; - }); + ]); } -function generateSSHKeyPair(privateKeyPath, comment, password, size) { +function generateSSHKeyPair(name, privateKeyPath, comment, password, size) { + log.trace("ssh-keygen["+[name,privateKeyPath,comment,size,"hasPassword?"+!!password].join(",")+"]"); return new Promise(function(resolve, reject) { keygen({ location: privateKeyPath, @@ -172,10 +175,11 @@ function generateSSHKeyPair(privateKeyPath, comment, password, size) { size: size }, function(err, out) { if ( err ) { + err.code = "key_generation_failed"; reject(err); } else { - resolve(); + resolve(name); } }); }); diff --git a/test/red/api/editor/sshkeys_spec.js b/test/red/api/editor/sshkeys_spec.js index ade1e7a07..bc9ea4ee6 100644 --- a/test/red/api/editor/sshkeys_spec.js +++ b/test/red/api/editor/sshkeys_spec.js @@ -151,6 +151,18 @@ describe("api/editor/sshkeys", function() { }); }); + it('GET /settings/user/keys/ --- return 404', function(done) { + mockRuntime.storage.sshkeys.getSSHKey.returns(Promise.resolve(null)); + request(app) + .get("/settings/user/keys/NOT_REAL") + .expect(404) + .end(function(err,res) { + if (err) { + return done(err); + } + done(); + }); + }); it('GET /settings/user/keys --- return Unexpected Error', function(done) { var errInstance = new Error("Messages....."); mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.reject(errInstance));