diff --git a/README.md b/README.md index aba6b1a..c86c108 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ This project is currently a work in progress. The following list provides a high - [x] Block Storage (Volumes) - [x] Images - [x] DNS - - [x] Account Details (exluding PayPal support) + - [x] Account Details (excluding PayPal support) + - [x] User Profile Settings - [ ] PayPal payments - [ ] Graphs - [ ] StackScripts - [ ] NodeBalancers - [ ] Longview - - [ ] User Profile Settings - [ ] Support Tickets Eventually I plan to also implement features which were never available in the original manager, including Object Storage, One-Click Apps, and LKE. diff --git a/account/oauth_apps/app/app.css b/account/oauth_apps/app/app.css new file mode 100644 index 0000000..db0a3c1 --- /dev/null +++ b/account/oauth_apps/app/app.css @@ -0,0 +1,27 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#app { + padding: 15px 15px 15px; +} + +tbody:not(.lmc-tbody-head) tr td:first-of-type { + font-weight: bold; + text-align: right; +} diff --git a/account/oauth_apps/app/app.js b/account/oauth_apps/app/app.js new file mode 100644 index 0000000..af1e3a1 --- /dev/null +++ b/account/oauth_apps/app/app.js @@ -0,0 +1,137 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiGet, apiPost, apiPut, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.callback = "callback"; + elements.label = "label"; + elements.pub = "public"; + elements.saveButton = "save-button"; + elements.thumbnail = "thumbnail"; + + // Data received from API calls + var data = {}; + data.app = {}; + + // Static references to UI elements + var ui = {}; + ui.callback = {}; + ui.label = {}; + ui.pub = {}; + ui.saveButton = {}; + ui.thumbnail = {}; + + // Temporary state + var state = {}; + state.sentApp = false; + state.sentThumb = false; + + // Callback for app details API call + var displayApp = function(response) + { + data.app = response; + + ui.label.value = data.app.label; + ui.callback.value = data.app.redirect_uri; + ui.pub.checked = data.app.public; + ui.saveButton.disabled = false; + }; + + // Click handler for save button + var handleSave = function(event) + { + if (event.currentTarget.disabled) + return; + + var req = { + "label": ui.label.value, + "redirect_uri": ui.callback.value + }; + + if (data.params.aid != "0") { + if (ui.thumbnail.files.length && ui.thumbnail.files[0].type.startsWith("image/")) { + apiPut("/account/oauth-clients/" + data.params.aid + "/thumbnail", ui.thumbnail.files[0], function(response) + { + state.sentThumb = true; + if (state.sentApp) + location.href = "/account/oauth_apps"; + }); + } else { + state.sentThumb = true; + } + apiPut("/account/oauth-clients/" + data.params.aid, req, function(response) + { + state.sentApp = true; + if (state.sentThumb) + location.href = "/account/oauth_apps"; + }); + } else { + req.public = ui.pub.checked; + apiPost("/account/oauth-clients", req, function(response) + { + alert("Here is your client secret! Store it securely, as it won't be shown again.\n" + response.secret); + if (ui.thumbnail.files.length && ui.thumbnail.files[0].type.startsWith("image/")) { + apiPut("/account/oauth-clients/" + response.id + "/thumbnail", ui.thumbnail.files[0], function(response) + { + location.href = "/account/oauth_apps"; + }); + } else { + location.href = "/account/oauth_apps"; + } + }); + } + + ui.saveButton.disabled = true; + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + // Assume 0 if no app ID supplied + if (!data.params.aid) + data.params.aid = "0"; + + setupHeader(); + + // Get element references + ui.callback = document.getElementById(elements.callback); + ui.label = document.getElementById(elements.label); + ui.pub = document.getElementById(elements.pub); + ui.saveButton = document.getElementById(elements.saveButton); + ui.thumbnail = document.getElementById(elements.thumbnail); + + // Fetch app details if specified + if (data.params.aid != "0") { + apiGet("/account/oauth-clients/" + data.params.aid, displayApp, null); + } else { + ui.pub.disabled = false; + ui.saveButton.disabled = false; + } + + // Register event handlers + ui.saveButton.addEventListener("click", handleSave); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/account/oauth_apps/app/index.shtml b/account/oauth_apps/app/index.shtml new file mode 100644 index 0000000..42e92e1 --- /dev/null +++ b/account/oauth_apps/app/index.shtml @@ -0,0 +1,63 @@ + + + + + + LMC - Account // Edit OAuth App + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Add/Edit an OAuth App
Label
Callback URL
Public
Thumbnail
+
+
+ + diff --git a/account/oauth_apps/index.shtml b/account/oauth_apps/index.shtml new file mode 100644 index 0000000..181e72e --- /dev/null +++ b/account/oauth_apps/index.shtml @@ -0,0 +1,53 @@ + + + + + + LMC - Account // OAuth Apps + + + + + + + +
+
+ + + + + + + + + + + + + + + +
OAuth Apps
LabelAccessIDCallback URLOptions
+ +
+
+ + diff --git a/account/oauth_apps/oauth_apps.css b/account/oauth_apps/oauth_apps.css new file mode 100644 index 0000000..f3434d5 --- /dev/null +++ b/account/oauth_apps/oauth_apps.css @@ -0,0 +1,32 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +.app-img { + height: 36px; + vertical-align: middle; + width: 36px; +} + +#oauth_apps { + padding: 15px 15px 15px; +} + +td:nth-of-type(6) { + text-align: center; +} diff --git a/account/oauth_apps/oauth_apps.js b/account/oauth_apps/oauth_apps.js new file mode 100644 index 0000000..5d3de06 --- /dev/null +++ b/account/oauth_apps/oauth_apps.js @@ -0,0 +1,163 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiDelete, apiGet, apiPost, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.appImg = "app-img"; + elements.lmcRow = "lmc-tr1"; + elements.lmcRowAlt = "lmc-tr2"; + elements.oauthApps = "oauth-apps"; + + // Data received from API calls + var data = {}; + data.apps = []; + + // Static references to UI elements + var ui = {}; + ui.oauthApps = {}; + + // Creates a row in the authorized apps table + var createAppRow = function(app, alt) + { + var row = document.createElement("tr"); + if (alt) + row.className = elements.lmcRowAlt; + else + row.className = elements.lmcRow; + + var thumb = document.createElement("td"); + if (app.thumbnail_url && app.thumbnail_url.length) { + var thumbImg = document.createElement("img"); + thumbImg.className = elements.appImg; + thumbImg.src = settings.apiURL + app.thumbnail_url; + thumbImg.alt = app.label; + thumb.appendChild(thumbImg); + } + row.appendChild(thumb); + + var label = document.createElement("td"); + label.innerHTML = app.label; + row.appendChild(label); + + var access = document.createElement("td"); + if (app.public) + access.innerHTML = "Public"; + else + access.innerHTML = "Private"; + row.appendChild(access); + + var id = document.createElement("td"); + id.innerHTML = app.id; + row.appendChild(id); + + var callback = document.createElement("td"); + callback.innerHTML = app.redirect_uri; + row.appendChild(callback); + + var options = document.createElement("td"); + var editLink = document.createElement("a"); + editLink.href = "/account/oauth_apps/app?aid=" + app.id; + editLink.innerHTML = "Edit"; + var separator = document.createElement("span"); + separator.innerHTML = " | "; + var resetLink = document.createElement("a"); + resetLink.id = "reset-app-" + app.id; + resetLink.href = "#"; + resetLink.innerHTML = "Reset Secret"; + resetLink.addEventListener("click", resetApp); + var removeLink = document.createElement("a"); + removeLink.id = "remove-app-" + app.id; + removeLink.href = "#"; + removeLink.innerHTML = "Remove"; + removeLink.addEventListener("click", removeApp); + options.appendChild(editLink); + options.appendChild(separator); + options.appendChild(resetLink); + options.appendChild(separator.cloneNode(true)); + options.appendChild(removeLink); + row.appendChild(options); + + return row; + }; + + // Callback for authorized apps API call + var displayApps = function(response) + { + data.apps = data.apps.concat(response.data); + + // Request the next page if there are more + if (response.page != response.pages) { + apiGet("/account/oauth-clients?page=" + (response.page + 1), displayApps, null); + return; + } + + // Redirect to add page if there are no OAuth apps + if (!data.apps.length) + location.href = "/account/oauth_apps/app?aid=0"; + + // Add apps to table + for (var i = 0; i < data.apps.length; i++) + ui.oauthApps.appendChild(createAppRow(data.apps[i], i % 2)); + }; + + // Handle removing an OAuth app + var removeApp = function(event) + { + var id = event.currentTarget.id.split("-")[2]; + if (!confirm("Are you sure you want to permanently delete this app?")) + return; + + apiDelete("/account/oauth-clients/" + id, function(response) + { + location.reload(); + }); + }; + + // Handle resetting an OAuth app secret + var resetApp = function(event) + { + var id = event.currentTarget.id.split("-")[2]; + if (!confirm("Are you sure you want to permanently reset the secret for this app?")) + return; + + apiPost("/account/oauth-clients/" + id + "/reset-secret", {}, function(response) + { + alert("Here is your client secret! Store it securely, as it won't be shown again.\n" + response.secret); + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.oauthApps = document.getElementById(elements.oauthApps); + + // Get data from API + apiGet("/account/oauth-clients", displayApps, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/favicon.ico b/favicon.ico index c7eccdf..94001e9 100644 Binary files a/favicon.ico and b/favicon.ico differ diff --git a/global.css b/global.css index 42e4d89..72837d3 100644 --- a/global.css +++ b/global.css @@ -90,7 +90,7 @@ header { border-bottom: 1px solid #E8E8E8; } -.lmc-table tbody:not(.lmc-tbody-head) tr:last-of-type { +.lmc-table tbody:not(.lmc-tbody-head):last-of-type tr:last-of-type { border: none; } diff --git a/global.js b/global.js index 4eefab0..40f94ce 100644 --- a/global.js +++ b/global.js @@ -77,6 +77,13 @@ var eventTitles = { "dns_zone_delete": "Delete DNS Zone", "dns_zone_import": "Import DNS Zone", "dns_zone_update": "Update DNS Zone", + "firewall_create": "Create Firewall", + "firewall_delete": "Delete Firewall", + "firewall_disable": "Disable Firewall", + "firewall_enable": "Enable Firewall", + "firewall_update": "Update Firewall", + "firewall_device_add": "Add Firewall Device", + "firewall_device_remove": "Remove Firewall Device", "host_reboot": "Host Initiated Restart", "image_delete": "Delete Image", "image_update": "Update Image", @@ -104,6 +111,10 @@ var eventTitles = { "linode_config_create": "Create Configuration Profile", "linode_config_delete": "Delete Configuration Profile", "linode_config_update": "Update Configuration Profile", + "lke_node_create": "Create LKE Node", + "longviewclient_create": "Create Longview Client", + "longviewclient_delete": "Delete Longview Client", + "longviewclient_update": "Update Longview Client", "managed_disabled": "Managed Service Disabled", "managed_enabled": "Managed Service Enabled", "managed_service_create": "Create Managed Service", @@ -117,6 +128,10 @@ var eventTitles = { "nodebalancer_node_create": "Create NodeBalancer Node", "nodebalancer_node_delete": "Delete NodeBalancer Node", "nodebalancer_node_update": "Update NodeBalancer Node", + "oauth_client_create": "Create OAuth Client", + "oauth_client_delete": "Delete OAuth Client", + "oauth_client_secret_reset": "Reset OAuth Client Secret", + "oauth_client_update": "Update OAuth Client", "password_reset": "Change Root Password", "payment_submitted": "Payment Submitted", "profile_update": "Update Profile", @@ -125,12 +140,21 @@ var eventTitles = { "stackscript_update": "Update StackScript", "stackscript_publicize": "Publish StackScript", "stackscript_revise": "Revise StackScript", + "tag_create": "Create Tag", + "tag_delete": "Delete Tag", "tfa_disabled": "2FA Disabled", "tfs_enabled": "2FA Enabled", "ticket_attachment_upload": "Uploaded Ticket Attachment", "ticket_create": "Create Ticket", "ticket_update": "Update Ticket", + "token_create": "Create API Token", + "token_delete": "Delete API Token", + "token_update": "Update API Token", + "user_create": "Create User", + "user_delete": "Delete User", + "user_update": "Update User", "user_ssh_key_add": "Add SSH Key", + "user_ssh_key_delete": "Delete SSH Key", "user_ssh_key_update": "Update SSH Key", "vlan_attach": "Attach VLAN", "vlan_detach": "Detach VLAN", @@ -143,6 +167,24 @@ var eventTitles = { "volume_resize": "Resize Volume" }; +// A list of OAuth scopes +var oauthScopes = { + "account": "Account", + "databases": "Databases", + "domains": "Domains", + "events": "Events", + "firewall": "Firewalls", + "images": "Images", + "ips": "IPs", + "linodes": "Linodes", + "lke": "Kubernetes", + "longview": "Longview", + "nodebalancers": "NodeBalancers", + "object_storage": "Object Storage", + "stackscripts": "StackScripts", + "volumes": "Volumes" +}; + // Make an HTTP DELETE request to the Linode API function apiDelete(endpoint, callback) { @@ -249,6 +291,10 @@ function apiGet(endpoint, callback, filters) location.href = "/"; } + // Redirect to linodes page if unauthorized + if (xmlhttp.status == 403) + location.href = "/linodes"; + return; } @@ -281,7 +327,10 @@ function apiPost(endpoint, data, callback) var xmlhttp = new XMLHttpRequest(); xmlhttp.open("POST", settings.apiURL + endpoint, true); xmlhttp.setRequestHeader("Authorization", localStorage.apiKey); - xmlhttp.setRequestHeader("Content-Type", "application/json"); + if (data instanceof File) + xmlhttp.setRequestHeader("Content-Type", data.type); + else + xmlhttp.setRequestHeader("Content-Type", "application/json"); xmlhttp.onreadystatechange = function() { @@ -314,7 +363,10 @@ function apiPost(endpoint, data, callback) callback(response); }; - xmlhttp.send(JSON.stringify(data)); + if (data instanceof File) + xmlhttp.send(data); + else + xmlhttp.send(JSON.stringify(data)); } // Make an HTTP PUT request to the Linode API @@ -339,7 +391,10 @@ function apiPut(endpoint, data, callback) var xmlhttp = new XMLHttpRequest(); xmlhttp.open("PUT", settings.apiURL + endpoint, true); xmlhttp.setRequestHeader("Authorization", localStorage.apiKey); - xmlhttp.setRequestHeader("Content-Type", "application/json"); + if (data instanceof File) + xmlhttp.setRequestHeader("Content-Type", data.type); + else + xmlhttp.setRequestHeader("Content-Type", "application/json"); xmlhttp.onreadystatechange = function() { @@ -372,7 +427,10 @@ function apiPut(endpoint, data, callback) callback(response); }; - xmlhttp.send(JSON.stringify(data)); + if (data instanceof File) + xmlhttp.send(data); + else + xmlhttp.send(JSON.stringify(data)); } // Callback for user info API call @@ -389,15 +447,23 @@ function displayUser(response) // Display profile pic var profilePic = document.getElementById(elements.profilePic); - profilePic.src = "https://www.gravatar.com/avatar/" + md5(response.email); + profilePic.src = "https://www.gravatar.com/avatar/" + md5(response.email, false); profilePic.style = "display: initial;"; } // Return an MD5 hash of the given string -function md5(str) +function md5(str, binary) { // Convert string to bytes - var data = new TextEncoder("utf-8").encode(str); + var data; + if (binary) { + var strBytes = []; + for (var i = 0; i < str.length; i++) + strBytes[i] = str.charCodeAt(i); + data = new Uint8Array(strBytes); + } else { + data = new TextEncoder("utf-8").encode(str); + } // Constants var s = new Uint32Array([ @@ -504,6 +570,8 @@ function md5(str) if (bytes[j] < 0x10) md5Str += "0"; md5Str += bytes[j].toString(16); + if (binary && (i != 3 || j != 3)) + md5Str += ":"; } } @@ -698,4 +766,4 @@ function translateKernel(slug, element) apiGet("/linode/kernels/" + slug, callback, null); } -export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, md5, migrateETA, oauthPost, parseParams, setupHeader, eventTitles, timeString, translateKernel }; +export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, md5, migrateETA, oauthPost, oauthScopes, parseParams, setupHeader, eventTitles, timeString, translateKernel }; diff --git a/include/account_subnav.html b/include/account_subnav.html index ff9fc05..db148e8 100644 --- a/include/account_subnav.html +++ b/include/account_subnav.html @@ -5,7 +5,8 @@ Update Credit Card Make A Payment Billing History - Users and Permissions + Users + OAuth Apps Account Settings diff --git a/include/profile_subnav.html b/include/profile_subnav.html new file mode 100644 index 0000000..7e22b3e --- /dev/null +++ b/include/profile_subnav.html @@ -0,0 +1,10 @@ + + + diff --git a/linodes/backup_details/backup_details.css b/linodes/backup_details/backup_details.css index 829d793..57ced42 100644 --- a/linodes/backup_details/backup_details.css +++ b/linodes/backup_details/backup_details.css @@ -34,10 +34,6 @@ table:first-of-type tbody tr td:first-of-type { text-align: right; } -td { - white-space: nowrap; -} - ul { display: inline; list-style-position: inside; diff --git a/linodes/backup_details/index.shtml b/linodes/backup_details/index.shtml index 17105e5..fb57754 100644 --- a/linodes/backup_details/index.shtml +++ b/linodes/backup_details/index.shtml @@ -77,7 +77,7 @@ along with Linode Manager Classic. If not, see . Linode Plan Location - Unallocated/Free Space + Free Space Select diff --git a/linodes/dashboard/dashboard.js b/linodes/dashboard/dashboard.js index ebdd715..8f32cf8 100644 --- a/linodes/dashboard/dashboard.js +++ b/linodes/dashboard/dashboard.js @@ -618,6 +618,15 @@ import { settings, elements, apiDelete, apiGet, apiPost, eventTitles, parseParam if (!data.notifications[i].entity || data.notifications[i].entity.type != "linode" || data.notifications[i].entity.id != data.params.lid) continue; + if (data.notifications[i].type == "maintenance") { + var now = new Date(); + var maintStart = new Date(data.notifications[i].when + "Z"); + data.notifications[i].label = "Maintenance Scheduled"; + data.notifications[i].message = "This Linode's physical host will be undergoing maintenance on " + maintStart.toLocaleString() + " (in " + timeString(maintStart - now, false) + ")."; + data.notifications[i].message += " During this time, your Linode will be shut down and remain offline, then returned to its last state (running or powered off)."; + data.notifications[i].message += " For more information, please see your open support tickets."; + } + var notification = document.createElement("div"); notification.className = elements.notification; var header = document.createElement("h1"); diff --git a/linodes/linodes.js b/linodes/linodes.js index f03e3f2..04ba141 100644 --- a/linodes/linodes.js +++ b/linodes/linodes.js @@ -56,6 +56,11 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro ui.loading = {}; ui.notifications = {}; + // Temporary state + var state = {}; + state.haveLinodes = false; + state.haveNotifications = false; + var createLinodeRow = function(linode, alt) { var row = document.createElement("tr"); @@ -69,7 +74,19 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro nameLink.innerHTML = linode.label; name.appendChild(nameLink); var status = document.createElement("td"); - status.innerHTML = linode.status.charAt(0).toUpperCase() + linode.status.slice(1).replace(/_/g, " "); + if (linode.lmc_maint) { + var maintStart = new Date(linode.lmc_maint + "Z"); + var line1 = document.createElement("strong"); + line1.innerHTML = "Maintenance Scheduled"; + var br = document.createElement("br"); + var line2 = document.createElement("span"); + line2.innerHTML = maintStart.toLocaleString(); + status.appendChild(line1); + status.appendChild(br); + status.appendChild(line2); + } else { + status.innerHTML = linode.status.charAt(0).toUpperCase() + linode.status.slice(1).replace(/_/g, " "); + } var plan = document.createElement("td"); if (linode.type) plan.innerHTML = getPlanLabel(linode.type); @@ -248,6 +265,8 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); }); + state.haveLinodes = true; + // Create tables ui.loading.remove(); if (data.noTag) @@ -256,13 +275,8 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro createLinodeTable(data.linodeTags[i]); // Insert linodes - for (var i = 0; i < data.linodes.length; i++) { - if (data.linodes[i].tags.length == 0) - ui.linodeTables[""].appendChild(createLinodeRow(data.linodes[i], ui.linodeTables[""].children.length % 2)); - - for (var j = 0; j < data.linodes[i].tags.length; j++) - ui.linodeTables[data.linodes[i].tags[j]].appendChild(createLinodeRow(data.linodes[i], ui.linodeTables[data.linodes[i].tags[j]].children.length % 2)); - } + if (state.haveNotifications) + insertLinodes(); }; var displayNotifications = function(response) @@ -276,6 +290,8 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro return; } + state.haveNotifications = true; + // Display notifications for (var i = 0; i < data.notifications.length; i++) { if (data.notifications[i].entity && data.notifications[i].entity.type == "linode") @@ -286,11 +302,25 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro var header = document.createElement("h1"); header.innerHTML = data.notifications[i].label; var body = document.createElement("p"); - body.innerHTML = data.notifications[i].message; + // Insert ticket link for ticket notifications + if (data.notifications[i].type == "ticket_important" && data.notifications[i].entity) { + var ticketLink = document.createElement("a"); + ticketLink.href = "/support/ticket?tid=" + data.notifications[i].entity.id; + ticketLink.innerHTML = data.notifications[i].entity.label; + body.appendChild(ticketLink); + } else { + body.innerHTML = data.notifications[i].message; + } + // Replace "this facility" with actual location for outages + if (data.notifications[i].type == "outage" && data.notifications[i].entity && data.notifications[i].entity.type == "region" && regionNames[data.notifications[i].entity.id]) + header.innerHTML = header.innerHTML.replace("this facility", regionNames[data.notifications[i].entity.id]); notification.appendChild(header); notification.appendChild(body); ui.notifications.appendChild(notification); } + + if (state.haveLinodes) + insertLinodes(); }; var displayTransfer = function(response) @@ -358,6 +388,31 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader } fro apiGet("/linode/instances", displayLinodes, filters); }; + var insertLinodes = function() + { + // Add maintenance windows from notifications + for (var i = 0; i < data.notifications.length; i++) { + if (data.notifications[i].type != "maintenance") + continue; + + for (var j = 0; j < data.linodes.length; j++) { + if (data.linodes[j].id == data.notifications[i].entity.id) { + data.linodes[j].lmc_maint = data.notifications[i].when; + break; + } + } + } + + // Insert linodes into tables + for (var i = 0; i < data.linodes.length; i++) { + if (data.linodes[i].tags.length == 0) + ui.linodeTables[""].appendChild(createLinodeRow(data.linodes[i], ui.linodeTables[""].children.length % 2)); + + for (var j = 0; j < data.linodes[i].tags.length; j++) + ui.linodeTables[data.linodes[i].tags[j]].appendChild(createLinodeRow(data.linodes[i], ui.linodeTables[data.linodes[i].tags[j]].children.length % 2)); + } + }; + var setup = function() { // Parse URL parameters diff --git a/profile/api/add/add.css b/profile/api/add/add.css new file mode 100644 index 0000000..a938722 --- /dev/null +++ b/profile/api/add/add.css @@ -0,0 +1,57 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#add { + padding: 15px 15px 15px; +} + +.lmc-table>tbody:not(.lmc-tbody-head)>tr>td:first-of-type { + font-weight: bold; + text-align: right; +} + +#scopes-table { + border-bottom: 1px solid #4C4C4C; + border-collapse: separate; + border-left: 1px solid #B2B2B2; + border-right: 1px solid #4C4C4C; + border-spacing: 2px; + border-top: 1px solid #B2B2B2; + margin-bottom: 30px; +} + +#scopes-table label { + display: block; +} + +#scopes-table tbody td { + text-align: center; +} + +#scopes-table tbody td:first-of-type { + text-align: left; +} + +#scopes-table td { + border-bottom: 1px solid #B2B2B2; + border-left: 1px solid #4C4C4C; + border-right: 1px solid #B2B2B2; + border-top: 1px solid #4C4C4C; + padding: 2px; +} diff --git a/profile/api/add/add.js b/profile/api/add/add.js new file mode 100644 index 0000000..6950545 --- /dev/null +++ b/profile/api/add/add.js @@ -0,0 +1,191 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiPost, oauthScopes, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.addButton = "add-button"; + elements.expiry = "expiry"; + elements.label = "label"; + elements.scopeBox = "scope-box"; + elements.scopes = "scopes"; + elements.selectNone = "select-none"; + elements.selectRo = "select-read_only"; + elements.selectRw = "select-read_write"; + + // Data received from API calls + var data = {}; + + // Static references to UI elements + var ui = {}; + ui.addButton = {}; + ui.expiry = {}; + ui.label = {}; + ui.scopes = {}; + ui.selectNone = {}; + ui.selectRo = {}; + ui.selectRw = {}; + + // Display OAuth scopes in scope selector + var displayScopes = function() + { + for (var scope in oauthScopes) { + var row = document.createElement("tr"); + + var label = document.createElement("td"); + label.innerHTML = oauthScopes[scope]; + row.appendChild(label); + + var none = document.createElement("td"); + var noneLabel = document.createElement("label"); + noneLabel.htmlFor = "scope-" + scope + "-none"; + var noneBox = document.createElement("input"); + noneBox.id = "scope-" + scope + "-none"; + noneBox.className = elements.scopeBox; + noneBox.type = "radio"; + noneBox.name = "scope-" + scope; + noneBox.checked = true; + noneBox.addEventListener("input", handleSelectScope); + noneLabel.appendChild(noneBox); + none.appendChild(noneLabel); + row.appendChild(none); + + var ro = document.createElement("td"); + var roLabel = document.createElement("label"); + roLabel.htmlFor = "scope-" + scope + "-read_only"; + var roBox = document.createElement("input"); + roBox.id = "scope-" + scope + "-read_only"; + roBox.className = elements.scopeBox; + roBox.type = "radio"; + roBox.name = "scope-" + scope; + roBox.addEventListener("input", handleSelectScope); + roLabel.appendChild(roBox); + ro.appendChild(roLabel); + row.appendChild(ro); + + var rw = document.createElement("td"); + var rwLabel = document.createElement("label"); + rwLabel.htmlFor = "scope-" + scope + "-read_write"; + var rwBox = document.createElement("input"); + rwBox.id = "scope-" + scope + "-read_write"; + rwBox.className = elements.scopeBox; + rwBox.type = "radio"; + rwBox.name = "scope-" + scope; + rwBox.addEventListener("input", handleSelectScope); + rwLabel.appendChild(rwBox); + rw.appendChild(rwLabel); + row.appendChild(rw); + + ui.scopes.appendChild(row); + } + }; + + // Click handler for add button + var handleAdd = function(event) + { + var req = { + "label": ui.label.value + }; + + var expiry = parseInt(ui.expiry.value); + if (expiry) { + var expireDt = new Date(Date.now() + expiry).toISOString(); + req.expiry = expireDt.slice(0, expireDt.indexOf(".")); + } + + if (ui.selectRw.checked) { + req.scopes = "*"; + } else { + req.scopes = []; + var buttons = document.getElementsByClassName(elements.scopeBox); + for (var i = 0; i < buttons.length; i++) { + if (!buttons[i].checked) + continue; + + var attrs = buttons[i].id.split("-"); + if (attrs[2] == "none") + continue; + req.scopes.push(attrs[1] + ":" + attrs[2]); + } + req.scopes = req.scopes.join(" "); + if (!req.scopes.length) { + alert("You must select some permissions."); + return; + } + } + + apiPost("/profile/tokens", req, function(response) + { + alert("Your API token has been created. Store this secret - it won't be shown again.\n" + response.token); + location.href = "/profile/api"; + }); + }; + + // Handler for the "select all" row + var handleSelectAll = function(event) + { + var column = event.currentTarget.id.split("-")[1]; + var buttons = document.getElementsByClassName(elements.scopeBox); + for (var i = 0; i < buttons.length; i++) { + if (buttons[i].id.split("-")[2] == column) + buttons[i].checked = true; + } + }; + + // Handler for individual scope selectors + var handleSelectScope = function(event) + { + ui.selectNone.checked = false; + ui.selectRo.checked = false; + ui.selectRw.checked = false; + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.addButton = document.getElementById(elements.addButton); + ui.expiry = document.getElementById(elements.expiry); + ui.label = document.getElementById(elements.label); + ui.scopes = document.getElementById(elements.scopes); + ui.selectNone = document.getElementById(elements.selectNone); + ui.selectRo = document.getElementById(elements.selectRo); + ui.selectRw = document.getElementById(elements.selectRw); + + // Add scopes to tables + displayScopes(); + ui.selectNone.checked = false; + ui.selectRo.checked = false; + ui.selectRw.checked = false; + + // Register event handlers + ui.addButton.addEventListener("click", handleAdd); + ui.selectNone.addEventListener("change", handleSelectAll); + ui.selectRo.addEventListener("change", handleSelectAll); + ui.selectRw.addEventListener("change", handleSelectAll); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/api/add/index.shtml b/profile/api/add/index.shtml new file mode 100644 index 0000000..500af3e --- /dev/null +++ b/profile/api/add/index.shtml @@ -0,0 +1,93 @@ + + + + + + LMC - My Profile // Add API Key + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Add an SSH Key
Label
Expires + +
Scopes + + + + + + + + + + + + + + + + + +
AccessNoneRead OnlyRead/Write
Select All
+
+
+
+ + diff --git a/profile/api/api.css b/profile/api/api.css new file mode 100644 index 0000000..414e399 --- /dev/null +++ b/profile/api/api.css @@ -0,0 +1,41 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#api { + padding: 15px 15px 15px; +} + +.app-img { + height: 36px; + vertical-align: middle; + width: 36px; +} + +.hover-info { + cursor: help; + text-decoration: dotted underline; +} + +td:nth-of-type(5) { + text-align: center; +} + +thead span.info { + font-size: 12px; +} diff --git a/profile/api/api.js b/profile/api/api.js new file mode 100644 index 0000000..c6b5385 --- /dev/null +++ b/profile/api/api.js @@ -0,0 +1,327 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiDelete, apiGet, apiPut, oauthScopes, parseParams, setupHeader, timeString } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.apiKeys = "api-keys"; + elements.appImg = "app-img"; + elements.authorizedApps = "authorized-apps"; + elements.hoverInfo = "hover-info"; + elements.info = "info"; + elements.lmcRow = "lmc-tr1"; + elements.lmcRowAlt = "lmc-tr2"; + + // Data received from API calls + var data = {}; + data.apps = []; + data.keys = []; + + // Static references to UI elements + var ui = {}; + ui.apiKeys = {}; + ui.authorizedApps = {}; + + // Creates a row in the authorized apps table + var createAppRow = function(app, alt) + { + var row = document.createElement("tr"); + if (alt) + row.className = elements.lmcRowAlt; + else + row.className = elements.lmcRow; + + var thumb = document.createElement("td"); + if (app.thumbnail_url && app.thumbnail_url.length) { + var thumbImg = document.createElement("img"); + thumbImg.className = elements.appImg; + thumbImg.src = settings.apiURL + app.thumbnail_url; + thumbImg.alt = app.label; + thumb.appendChild(thumbImg); + } + row.appendChild(thumb); + + var label = document.createElement("td"); + if (app.website && app.website.length) { + var link = document.createElement("a"); + link.href = app.website; + link.target = "_blank"; + link.innerHTML = app.label; + label.appendChild(link); + } else { + label.innerHTML = app.label; + } + row.appendChild(label); + + var created = document.createElement("td"); + var created1 = document.createElement("span"); + var now = new Date(); + var createDt = new Date(app.created + "Z"); + created1.innerHTML = timeString(now - createDt, true); + var created2 = document.createElement("span"); + created2.className = elements.info; + created2.innerHTML = createDt.toLocaleString(); + created.appendChild(created1); + created.appendChild(document.createElement("br")); + created.appendChild(created2); + row.appendChild(created); + + var expires = document.createElement("td"); + if (app.expiry) { + var expires1 = document.createElement("span"); + var expireDt = new Date(app.expiry + "Z"); + expires1.innerHTML = timeString(now - expireDt, true); + var expires2 = document.createElement("span"); + expires2.className = elements.info; + expires2.innerHTML = expireDt.toLocaleString(); + expires.appendChild(expires1); + expires.appendChild(document.createElement("br")); + expires.appendChild(expires2); + } else { + expires.innerHTML = "Never"; + } + row.appendChild(expires); + + var options = document.createElement("td"); + var scopesLink = document.createElement("a"); + scopesLink.id = "scopes-app-" + app.id; + scopesLink.href = "#"; + scopesLink.innerHTML = "View Scopes"; + scopesLink.addEventListener("click", viewScopes); + var separator = document.createElement("span"); + separator.innerHTML = " | "; + var removeLink = document.createElement("a"); + removeLink.id = "revoke-app-" + app.id; + removeLink.href = "#"; + removeLink.innerHTML = "Revoke"; + removeLink.addEventListener("click", revoke); + options.appendChild(scopesLink); + options.appendChild(separator); + options.appendChild(removeLink); + row.appendChild(options); + + return row; + }; + + // Creates a row in the API keys table + var createKeyRow = function(key, alt) + { + var row = document.createElement("tr"); + if (alt) + row.className = elements.lmcRowAlt; + else + row.className = elements.lmcRow; + + var label = document.createElement("td"); + label.innerHTML = key.label; + row.appendChild(label); + + var prefix = document.createElement("td"); + prefix.innerHTML = key.token + "..."; + row.appendChild(prefix); + + var created = document.createElement("td"); + var created1 = document.createElement("span"); + var now = new Date(); + var createDt = new Date(key.created + "Z"); + created1.innerHTML = timeString(now - createDt, true); + var created2 = document.createElement("span"); + created2.className = elements.info; + created2.innerHTML = createDt.toLocaleString(); + created.appendChild(created1); + created.appendChild(document.createElement("br")); + created.appendChild(created2); + row.appendChild(created); + + var expires = document.createElement("td"); + if (key.expiry) { + var expires1 = document.createElement("span"); + var expireDt = new Date(key.expiry + "Z"); + expires1.innerHTML = timeString(now - expireDt, true); + var expires2 = document.createElement("span"); + expires2.className = elements.info; + expires2.innerHTML = expireDt.toLocaleString(); + expires.appendChild(expires1); + expires.appendChild(document.createElement("br")); + expires.appendChild(expires2); + } else { + expires.innerHTML = "Never"; + } + row.appendChild(expires); + + var options = document.createElement("td"); + var scopesLink = document.createElement("a"); + scopesLink.id = "scopes-key-" + key.id; + scopesLink.href = "#"; + scopesLink.innerHTML = "View Scopes"; + scopesLink.addEventListener("click", viewScopes); + var separator = document.createElement("span"); + separator.innerHTML = " | "; + var renameLink = document.createElement("a"); + renameLink.id = "rename-" + key.id; + renameLink.href = "#"; + renameLink.innerHTML = "Rename"; + renameLink.addEventListener("click", handleRename); + var removeLink = document.createElement("a"); + removeLink.id = "revoke-key-" + key.id; + removeLink.href = "#"; + removeLink.innerHTML = "Revoke"; + removeLink.addEventListener("click", revoke); + options.appendChild(scopesLink); + options.appendChild(separator); + options.appendChild(renameLink); + options.appendChild(separator.cloneNode(true)); + options.appendChild(removeLink); + row.appendChild(options); + + return row; + }; + + // Callback for authorized apps API call + var displayApps = function(response) + { + data.apps = data.apps.concat(response.data); + + // Request the next page if there are more + if (response.page != response.pages) { + apiGet("/profile/apps?page=" + (response.page + 1), displayApps, null); + return; + } + + // Add apps to table + for (var i = 0; i < data.apps.length; i++) + ui.authorizedApps.appendChild(createAppRow(data.apps[i], i % 2)); + }; + + // Callback for ssh keys API call + var displayKeys = function(response) + { + data.keys = data.keys.concat(response.data); + + // Request the next page if there are more + if (response.page != response.pages) { + apiGet("/profile/tokens?page=" + (response.page + 1), displayKeys, null); + return; + } + + + // Add keys to table + for (var i = 0; i < data.keys.length; i++) + ui.apiKeys.appendChild(createKeyRow(data.keys[i], i % 2)); + }; + + // Handler for renaming an API token + var handleRename = function(event) + { + var id = event.currentTarget.id.split("-")[1]; + var name = prompt("Rename API token:"); + if (!name) + return; + + var req = { + "label": name + }; + + apiPut("/profile/tokens/" + id, req, function(response) + { + location.reload(); + }); + }; + + // Revokes an API key or app + var revoke = function(event) + { + var info = event.currentTarget.id.split("-"); + var isApp = (info[1] == "app"); + var id = parseInt(info[2]); + var message = "Are you sure you want to remove this API key?"; + var url = "/profile/tokens/"; + if (isApp) { + message = "Are you sure you want to revoke access for this app?"; + url = "/profile/apps/"; + } + + if (!confirm(message)) + return; + + apiDelete(url + id, function(response) + { + location.reload(); + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.apiKeys = document.getElementById(elements.apiKeys); + ui.authorizedApps = document.getElementById(elements.authorizedApps); + + // Get data from API + apiGet("/profile/tokens", displayKeys, null); + apiGet("/profile/apps", displayApps, null); + }; + + // Display the scopes for a given app or key + var viewScopes = function(event) + { + var info = event.currentTarget.id.split("-"); + var isApp = (info[1] == "app"); + var id = parseInt(info[2]); + var searchArr = data.keys; + if (isApp) + searchArr = data.apps; + + var scopeStr = ""; + for (var i = 0; i < searchArr.length; i++) { + if (searchArr[i].id == id) { + scopeStr = searchArr[i].scopes; + break; + } + } + + var scopes = []; + if (scopeStr == "*") { + for (var scope in oauthScopes) + scopes.push(scope + ":read_write"); + } else { + scopes = scopeStr.split(" "); + } + + var alertStr = ""; + for (var i = 0; i < scopes.length; i++) { + var scopeInfo = scopes[i].split(":"); + alertStr += oauthScopes[scopeInfo[0]] + " - "; + if (scopeInfo[1] == "read_write") + alertStr += "Read/Write\n"; + else + alertStr += "Read Only\n"; + } + + alert(alertStr); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/api/index.shtml b/profile/api/index.shtml new file mode 100644 index 0000000..6cb16b1 --- /dev/null +++ b/profile/api/index.shtml @@ -0,0 +1,67 @@ + + + + + + LMC - My Profile // API Keys + + + + + + + +
+
+ + + + + + + + + + + + + + +
API Keys
LabelKey (prefix only)CreatedExpiresOptions
+ + + + + + + + + + + + + + + +
Authorized Apps
AppCreatedExpiresOptions
+
+
+ + diff --git a/profile/auth/auth.css b/profile/auth/auth.css new file mode 100644 index 0000000..310dd62 --- /dev/null +++ b/profile/auth/auth.css @@ -0,0 +1,31 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#auth { + padding: 15px 15px 15px; +} + +tbody:not(.lmc-tbody-head) tr td:first-of-type { + font-weight: bold; + text-align: right; +} + +.tfa { + display: none; +} diff --git a/profile/auth/auth.js b/profile/auth/auth.js new file mode 100644 index 0000000..77fe5b9 --- /dev/null +++ b/profile/auth/auth.js @@ -0,0 +1,164 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiDelete, apiGet, apiPost, parseParams, setupHeader, timeString } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.pwReset = "pw-reset"; + elements.tfa = "tfa"; + elements.tfaDisable = "tfa-disable"; + elements.tfaEnable = "tfa-enable"; + elements.tfaStatus = "tfa-status"; + elements.trusted = "trusted"; + elements.untrust = "untrust"; + + // Data received from API calls + var data = {}; + + // Static references to UI elements + var ui = {}; + ui.pwReset = {}; + ui.tfa = []; + ui.tfaDisable = {}; + ui.tfaEnable = {}; + ui.tfaStatus = {}; + ui.trusted = {}; + + // Creates a row for the trusted devices list + var createTrustedRow = function(device) + { + var row = document.createElement("div"); + row.id = elements.trusted + "-" + device.id; + var br = document.createElement("br"); + row.appendChild(br); + + var userAgent = document.createElement("span"); + userAgent.innerHTML = device.user_agent; + row.appendChild(userAgent); + row.appendChild(br.cloneNode(true)); + + var lastAuth = document.createElement("span"); + var now = new Date(); + var authDt = new Date(device.last_authenticated + "Z"); + lastAuth.innerHTML = "Last authenticated " + timeString(now - authDt, true) + " from " + device.last_remote_addr; + row.appendChild(lastAuth); + row.appendChild(br.cloneNode(true)); + + var expiry = document.createElement("span"); + var expireDt = new Date(device.expiry + "Z"); + expiry.innerHTML = "Expires in " + timeString(expireDt - now, false) + " - "; + row.appendChild(expiry); + + var untrust = document.createElement("a"); + untrust.id = elements.untrust + "-" + device.id; + untrust.href = "#"; + untrust.innerHTML = "untrust"; + untrust.addEventListener("click", handleUntrust); + row.appendChild(untrust); + + return row; + }; + + // Callback for profile API call + var displayProfile = function(response) + { + ui.pwReset.href += response.username; + if (response.two_factor_auth) { + apiGet("/profile/devices", displayTrusted, null); + ui.tfaStatus.innerHTML = "Enabled"; + ui.tfaEnable.innerHTML = "Reset Two-Factor Authentication"; + for (var i = 0; i < ui.tfa.length; i++) { + if (ui.tfa[i].tagName == "TR") + ui.tfa[i].style.display = "table-row"; + else + ui.tfa[i].style.display = "initial"; + } + } else { + ui.tfaStatus.innerHTML = "Disabled"; + } + + ui.tfaEnable.disabled = false; + }; + + // Callback for trusted devices API call + var displayTrusted = function(response) + { + for (var i = 0; i < response.data.length; i++) + ui.trusted.appendChild(createTrustedRow(response.data[i])); + }; + + // Click handler for disable 2FA link + var handleDisableTFA = function(event) + { + if (!confirm("Are you sure you want to disable two-factor authentication?")) + return; + + apiPost("/profile/tfa-disable", {}, function(response) + { + location.reload(); + }); + }; + + // Click handler for enable/reset 2FA button + var handleEnableTFA = function(event) + { + if (event.currentTarget.disabled) + return; + + location.href = "/profile/twofactor"; + }; + + // Click handler for untrust links + var handleUntrust = function(event) + { + var deviceID = parseInt(event.currentTarget.id.split("-")[1]); + apiDelete("/profile/devices/" + deviceID, function(response) + { + var row = document.getElementById(elements.trusted + "-" + deviceID); + row.remove(); + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.pwReset = document.getElementById(elements.pwReset); + ui.tfa = document.getElementsByClassName(elements.tfa); + ui.tfaDisable = document.getElementById(elements.tfaDisable); + ui.tfaEnable = document.getElementById(elements.tfaEnable); + ui.tfaStatus = document.getElementById(elements.tfaStatus); + ui.trusted = document.getElementById(elements.trusted); + + // Register event handlers + ui.tfaDisable.addEventListener("click", handleDisableTFA); + ui.tfaEnable.addEventListener("click", handleEnableTFA); + + // Get data from API + apiGet("/profile", displayProfile, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/auth/index.shtml b/profile/auth/index.shtml new file mode 100644 index 0000000..1ab424b --- /dev/null +++ b/profile/auth/index.shtml @@ -0,0 +1,80 @@ + + + + + + LMC - My Profile // Password & Authentication + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Authentication
Change Password
Reset PasswordClick here to reset your password
Two-Factor Authentication
DescriptionWhen two-factor authentication is enabled, you'll need to provide a token before you can log in to your Linode account.
StatusTwo-factor authentication is currently: - disable
Scratch CodeOne-time use emergency scratch code is shown only once.
Trusted ComputersThe following computers can skip two-factor authentication:
+
+
+ + diff --git a/profile/index.shtml b/profile/index.shtml new file mode 100644 index 0000000..3440a41 --- /dev/null +++ b/profile/index.shtml @@ -0,0 +1,93 @@ + + + + + + LMC - My Profile + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
My Profile (, in case you forgot)
Timezone
Timezone + LMC automatically detects your timezone from your system.
+ Your current timezone offset is: +
Email
Current Email
New Email
Linode Events Email
Events Email NotificationNotifications are currently:
+
+
+ + diff --git a/profile/lish/index.shtml b/profile/lish/index.shtml new file mode 100644 index 0000000..548922d --- /dev/null +++ b/profile/lish/index.shtml @@ -0,0 +1,87 @@ + + + + + + LMC - My Profile // Lish Settings + + + + + + + +
+
+

Settings saved.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Lish Authentication
Authentication Methods
Authentication modes + + + This controls what authentication methods are allowed to connect to the
+ Lish console servers. About the Lish console... +
Lish Keys
DescriptionPlace your SSH public keys here for use with Lish console access.
Lish Keys
+
+
+ + diff --git a/profile/lish/lish.css b/profile/lish/lish.css new file mode 100644 index 0000000..04dff51 --- /dev/null +++ b/profile/lish/lish.css @@ -0,0 +1,39 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#lish { + padding: 15px 15px 15px; +} + +#keys { + white-space: pre; +} + +#settings-saved { + background-color: #ADD370; + display: none; + font-size: 16px; + margin-top: 0; + padding: 7px; +} + +tbody:not(.lmc-tbody-head) tr td:first-of-type { + font-weight: bold; + text-align: right; +} diff --git a/profile/lish/lish.js b/profile/lish/lish.js new file mode 100644 index 0000000..f5654ce --- /dev/null +++ b/profile/lish/lish.js @@ -0,0 +1,115 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiGet, apiPut, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.keys = "keys"; + elements.modes = "modes"; + elements.saveButton = "save-button"; + elements.settingsSaved = "settings-saved"; + elements.submitButton = "submit-button"; + + // Data received from API calls + var data = {}; + + // Static references to UI elements + var ui = {}; + ui.keys = {}; + ui.modes = {}; + ui.saveButton = {}; + ui.settingsSaved = {}; + ui.submitButton = {}; + + // Callback for profile API call + var displayProfile = function(response) + { + // Populate auth method settings + ui.modes.value = response.lish_auth_method; + ui.saveButton.disabled = false; + + // Populate authorized keys + ui.keys.value = response.authorized_keys.join("\n"); + ui.submitButton.disabled = false; + }; + + // Click handler for save button + var handleSave = function(event) + { + if (event.currentTarget.disabled) + return; + + var req = { + "lish_auth_method": ui.modes.value + }; + + apiPut("/profile", req, function(response) + { + ui.settingsSaved.style.display = "block"; + }); + }; + + // Click handler for submit button + var handleSubmit = function(event) + { + if (event.currentTarget.disabled) + return; + + var req = { + "authorized_keys": ui.keys.value.split("\n") + }; + + // Remove empty keys from the request + for (var i = req.authorized_keys.length - 1; i >= 0; i--) { + if (!req.authorized_keys[i].length) + req.authorized_keys.splice(i, 1); + } + + apiPut("/profile", req, function(response) + { + ui.settingsSaved.style.display = "block"; + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.keys = document.getElementById(elements.keys); + ui.modes = document.getElementById(elements.modes); + ui.saveButton = document.getElementById(elements.saveButton); + ui.settingsSaved = document.getElementById(elements.settingsSaved); + ui.submitButton = document.getElementById(elements.submitButton); + + // Register event handlers + ui.saveButton.addEventListener("click", handleSave); + ui.submitButton.addEventListener("click", handleSubmit); + + // Get data from API + apiGet("/profile", displayProfile, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/profile.css b/profile/profile.css new file mode 100644 index 0000000..367c5fc --- /dev/null +++ b/profile/profile.css @@ -0,0 +1,31 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#notifications { + font-weight: bold; +} + +#profile { + padding: 15px 15px 15px; +} + +tbody:not(.lmc-tbody-head) tr td:first-of-type { + font-weight: bold; + text-align: right; +} diff --git a/profile/profile.js b/profile/profile.js new file mode 100644 index 0000000..4be5aa5 --- /dev/null +++ b/profile/profile.js @@ -0,0 +1,127 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiGet, apiPut, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.changeButton = "change-button"; + elements.currentAddress = "current-address"; + elements.newAddress = "new-address"; + elements.notifications = "notifications"; + elements.timezone = "timezone"; + elements.toggleNotifications = "toggle-notifications"; + elements.user = "user"; + + // Data received from API calls + var data = {}; + data.profile = {}; + + // Static references to UI elements + var ui = {}; + ui.changeButton = {}; + ui.currentAddress = {}; + ui.newAddress = {}; + ui.notifications = {}; + ui.timezone = {}; + ui.toggleNotifications = {}; + ui.user = {}; + + // Callback for profile API call + var displayProfile = function(response) + { + data.profile = response; + + ui.user.innerHTML = data.profile.username; + ui.currentAddress.innerHTML = data.profile.email; + if (data.profile.email_notifications) + ui.notifications.innerHTML = "Enabled"; + else + ui.notifications.innerHTML = "Disabled"; + ui.toggleNotifications.disabled = false; + }; + + // Event handler for change email button + var handleChange = function(event) + { + if (event.currentTarget.disabled) + return; + + var req = { + "email": ui.newAddress.value + }; + + apiPut("/profile", req, function(response) + { + ui.newAddress.value = ""; + location.reload(); + }); + }; + + // Click handler for toggle notifications button + var handleToggle = function(event) + { + if (event.currentTarget.disabled) + return; + + var req = { + "email_notifications": !data.profile.email_notifications + }; + + apiPut("/profile", req, function(response) + { + location.reload(); + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.changeButton = document.getElementById(elements.changeButton); + ui.currentAddress = document.getElementById(elements.currentAddress); + ui.newAddress = document.getElementById(elements.newAddress); + ui.notifications = document.getElementById(elements.notifications); + ui.timezone = document.getElementById(elements.timezone); + ui.toggleNotifications = document.getElementById(elements.toggleNotifications); + ui.user = document.getElementById(elements.user); + + // Display timezone info + var now = new Date(); + var offset = now.getTimezoneOffset() / -60; + ui.timezone.innerHTML = "GMT"; + if (offset >= 0) + ui.timezone.innerHTML += "+"; + ui.timezone.innerHTML += offset; + + // Register event handlers + ui.changeButton.addEventListener("click", handleChange); + ui.toggleNotifications.addEventListener("click", handleToggle); + + // Get data from API + apiGet("/profile", displayProfile, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/referrals/index.shtml b/profile/referrals/index.shtml new file mode 100644 index 0000000..93f267c --- /dev/null +++ b/profile/referrals/index.shtml @@ -0,0 +1,62 @@ + + + + + + LMC - My Profile // Referrals + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Referrals
Description + Referrals reward you when you refer people to Linode. If someone signs up using your referral code, you'll receive a credit of
+ $20.00, so long as the person you referred remains an active customer for 90 days. +
Your referral code
Your referral URL
You have total referrals: completed ($) and pending
+
+
+ + diff --git a/profile/referrals/referrals.css b/profile/referrals/referrals.css new file mode 100644 index 0000000..239494c --- /dev/null +++ b/profile/referrals/referrals.css @@ -0,0 +1,35 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#code { + font-family: monospace; +} + +#referrals { + padding: 15px 15px 15px; +} + +tbody:not(.lmc-tbody-head) tr td:first-of-type { + font-weight: bold; + text-align: right; +} + +#url { + font-family: monospace; +} diff --git a/profile/referrals/referrals.js b/profile/referrals/referrals.js new file mode 100644 index 0000000..9a0abf6 --- /dev/null +++ b/profile/referrals/referrals.js @@ -0,0 +1,75 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiGet, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.code = "code"; + elements.completed = "completed"; + elements.dollars = "dollars"; + elements.pending = "pending"; + elements.total = "total"; + elements.url = "url"; + + // Data received from API calls + var data = {}; + + // Static references to UI elements + var ui = {}; + ui.code = {}; + ui.completed = {}; + ui.dollars = {}; + ui.pending = {}; + ui.total = {}; + ui.url = {}; + + // Callback for profile API call + var displayProfile = function(response) + { + ui.code.innerHTML = response.referrals.code; + ui.url.innerHTML = response.referrals.url; + ui.total.innerHTML = response.referrals.total; + ui.completed.innerHTML = response.referrals.completed; + ui.dollars.innerHTML = response.referrals.credit.toFixed(2); + ui.pending.innerHTML = response.referrals.pending; + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.code = document.getElementById(elements.code); + ui.completed = document.getElementById(elements.completed); + ui.dollars = document.getElementById(elements.dollars); + ui.pending = document.getElementById(elements.pending); + ui.total = document.getElementById(elements.total); + ui.url = document.getElementById(elements.url); + + // Get data from API + apiGet("/profile", displayProfile, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/ssh/add/add.css b/profile/ssh/add/add.css new file mode 100644 index 0000000..fa9baec --- /dev/null +++ b/profile/ssh/add/add.css @@ -0,0 +1,27 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#add { + padding: 15px 15px 15px; +} + +tbody:not(.lmc-tbody-head) tr td:first-of-type { + font-weight: bold; + text-align: right; +} diff --git a/profile/ssh/add/add.js b/profile/ssh/add/add.js new file mode 100644 index 0000000..8447de9 --- /dev/null +++ b/profile/ssh/add/add.js @@ -0,0 +1,69 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiPost, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.addButton = "add-button"; + elements.key = "key"; + elements.label = "label"; + + // Data received from API calls + var data = {}; + + // Static references to UI elements + var ui = {}; + ui.addButton = {}; + ui.key = {}; + ui.label = {}; + + // Click handler for add button + var handleAdd = function(event) + { + var req = { + "label": ui.label.value, + "ssh_key": ui.key.value + }; + + apiPost("/profile/sshkeys", req, function(response) + { + location.href = "/profile/ssh"; + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.addButton = document.getElementById(elements.addButton); + ui.key = document.getElementById(elements.key); + ui.label = document.getElementById(elements.label); + + // Register event handlers + ui.addButton.addEventListener("click", handleAdd); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/ssh/add/index.shtml b/profile/ssh/add/index.shtml new file mode 100644 index 0000000..f7fa4ff --- /dev/null +++ b/profile/ssh/add/index.shtml @@ -0,0 +1,55 @@ + + + + + + LMC - My Profile // Add SSH Key + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +
Add an SSH Key
Label
SSH Public Key
+
+
+ + diff --git a/profile/ssh/index.shtml b/profile/ssh/index.shtml new file mode 100644 index 0000000..3e2262b --- /dev/null +++ b/profile/ssh/index.shtml @@ -0,0 +1,51 @@ + + + + + + LMC - My Profile // SSH Keys + + + + + + + +
+
+ + + + + + + + + + + + + +
SSH Keys
LabelKeyCreatedOptions
+ +
+
+ + diff --git a/profile/ssh/remove/index.shtml b/profile/ssh/remove/index.shtml new file mode 100644 index 0000000..e6f79da --- /dev/null +++ b/profile/ssh/remove/index.shtml @@ -0,0 +1,36 @@ + + + + + + LMC - My Profile // Remove SSH Key + + + + + + + +
+
+

Are you sure you want to delete the key ?

+ +
+
+ + diff --git a/profile/ssh/remove/remove.css b/profile/ssh/remove/remove.css new file mode 100644 index 0000000..b8f1a06 --- /dev/null +++ b/profile/ssh/remove/remove.css @@ -0,0 +1,22 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +#remove { + padding: 15px 15px 15px; +} diff --git a/profile/ssh/remove/remove.js b/profile/ssh/remove/remove.js new file mode 100644 index 0000000..88241a9 --- /dev/null +++ b/profile/ssh/remove/remove.js @@ -0,0 +1,80 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiDelete, apiGet, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.label = "label"; + elements.removeButton = "remove-button"; + + // Data received from API calls + var data = {}; + + // Static references to UI elements + var ui = {}; + ui.label = {}; + ui.removeButton = {}; + + // Callback for key API call + var displayKey = function(response) + { + ui.label.innerHTML = response.label; + ui.removeButton.disabled = false; + }; + + // Click handler for remove button + var handleRemove = function(event) + { + if (event.currentTarget.disabled) + return; + + apiDelete("/profile/sshkeys/" + data.params.kid, function(response) + { + location.href = "/profile/ssh"; + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + // We need a key ID, so die if we don't have it + if (!data.params.kid) { + alert("No key ID supplied!"); + return; + } + + setupHeader(); + + // Get element references + ui.label = document.getElementById(elements.label); + ui.removeButton = document.getElementById(elements.removeButton); + + // Register event handlers + ui.removeButton.addEventListener("click", handleRemove); + + // Get data from API + apiGet("/profile/sshkeys/" + data.params.kid, displayKey, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/ssh/ssh.css b/profile/ssh/ssh.css new file mode 100644 index 0000000..67bd3d7 --- /dev/null +++ b/profile/ssh/ssh.css @@ -0,0 +1,31 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +.hover-info { + cursor: help; + text-decoration: dotted underline; +} + +#ssh { + padding: 15px 15px 15px; +} + +td:nth-of-type(4) { + text-align: right; +} diff --git a/profile/ssh/ssh.js b/profile/ssh/ssh.js new file mode 100644 index 0000000..9efce93 --- /dev/null +++ b/profile/ssh/ssh.js @@ -0,0 +1,120 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiGet, md5, parseParams, setupHeader, timeString } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.hoverInfo = "hover-info"; + elements.lmcRow = "lmc-tr1"; + elements.lmcRowAlt = "lmc-tr2"; + elements.sshKeys = "ssh-keys"; + + // Data received from API calls + var data = {}; + data.keys = []; + + // Static references to UI elements + var ui = {}; + ui.sshKeys = {}; + + // Creates a row in the SSH keys table + var createKeyRow = function(key, alt) + { + var row = document.createElement("tr"); + if (alt) + row.className = elements.lmcRowAlt; + else + row.className = elements.lmcRow; + + var label = document.createElement("td"); + label.innerHTML = key.label; + row.appendChild(label); + + var keyInfo = document.createElement("td"); + var start = document.createElement("span"); + start.className = elements.hoverInfo; + start.title = key.ssh_key; + start.innerHTML = key.ssh_key.substring(0, 26); + keyInfo.appendChild(start); + var br = document.createElement("br"); + keyInfo.appendChild(br); + var fingerprint = document.createElement("span"); + try { + fingerprint.innerHTML = "Fingerprint: " + md5(atob(key.ssh_key.split(" ")[1]), true); + } catch (error) { + fingerprint.innerHTML = "Invalid key"; + } + keyInfo.appendChild(fingerprint); + row.appendChild(keyInfo); + + var created = document.createElement("td"); + var now = new Date(); + var createDate = new Date(key.created + "Z"); + created.innerHTML = timeString(now - createDate, true); + row.appendChild(created); + + var options = document.createElement("td"); + var removeLink = document.createElement("a"); + removeLink.href = "/profile/ssh/remove?kid=" + key.id; + removeLink.innerHTML = "Remove"; + options.appendChild(removeLink); + row.appendChild(options); + + return row; + }; + + // Callback for ssh keys API call + var displayKeys = function(response) + { + data.keys = data.keys.concat(response.data); + + // Request the next page if there are more + if (response.page != response.pages) { + apiGet("/profile/sshkeys?page=" + (response.page + 1), displayKeys, null); + return; + } + + + // Redirect to add page if there are no keys + if (!data.keys.length) + location.href = "/profile/ssh/add"; + + // Add keys to table + for (var i = 0; i < data.keys.length; i++) + ui.sshKeys.appendChild(createKeyRow(data.keys[i], i % 2)); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Get element references + ui.sshKeys = document.getElementById(elements.sshKeys); + + // Get data from API + apiGet("/profile/sshkeys", displayKeys, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/profile/twofactor/index.shtml b/profile/twofactor/index.shtml new file mode 100644 index 0000000..a3bc336 --- /dev/null +++ b/profile/twofactor/index.shtml @@ -0,0 +1,98 @@ + + + + + + LMC - Two-Factor Authentication + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Enable Two-Factor Authentication
Step 1
DscriptionScan the QR code (or enter the key) into your two-factor application.
Secret KeySave this key in a safe place, it will only be shown once.
QR Codeqr-code
Step 2
DescriptionUse your two-factor app to generate a token to verify everything is working correctly.
Generated Token
Recovery Procedure
+
+ If you lose your token and get locked out of your account, email support@linode.com to regain access to your account.
+
+ Should you need Linode to disable your Two-Factor Authentication, the following information is required:
+
    +
  1. An image of the front and back of the payment card on file, which clearly shows both the last 6 digits and owner of the card
  2. +
  3. An image of the front and back of the matching government-issued photo ID
  4. +
+
+
Read Linode's two-factor authentication documentation for more information.
+
+
+ + diff --git a/profile/twofactor/twofactor.css b/profile/twofactor/twofactor.css new file mode 100644 index 0000000..77f7f05 --- /dev/null +++ b/profile/twofactor/twofactor.css @@ -0,0 +1,52 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +.lmc-table:first-of-type tbody:not(.lmc-tbody-head) tr td:first-of-type { + font-weight: bold; + text-align: right; +} + +#more-info { + font-size: 12px; + margin-top: 20px; + text-align: center; +} + +#qr-code { + padding: 32px; +} + +#qr-code-img { + display: inline-block; + height: 136px; + width: 136px; +} + +#recovery-procedure { + font-size: 13.3px; + padding: 5px; +} + +#tfa-secret { + font-family: monospace; +} + +#twofactor { + padding: 15px 15px 15px; +} diff --git a/profile/twofactor/twofactor.js b/profile/twofactor/twofactor.js new file mode 100644 index 0000000..6a8aab2 --- /dev/null +++ b/profile/twofactor/twofactor.js @@ -0,0 +1,95 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, apiPost, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.confirmButton = "confirm-button"; + elements.qrCode = "qr-code-img"; + elements.subnav = "subnav-link"; + elements.subnavActive = "subnav-link-active"; + elements.tfaSecret = "tfa-secret"; + elements.tfaToken = "tfa-token"; + + // Data received from API calls + var data = {}; + + // Static references to UI elements + var ui = {}; + ui.confirmButton = {}; + ui.qrCode = {}; + ui.tfaSecret = {}; + ui.tfaToken = {}; + + // Callback for TFA API call + var displaySecret = function(response) + { + ui.tfaSecret.innerHTML = response.secret; + ui.confirmButton.disabled = false; + }; + + // Click handler for confirm button + var handleConfirm = function(event) + { + if (event.currentTarget.disabled) + return; + + var req = { + "tfa_code": ui.tfaToken.value + }; + + apiPost("/profile/tfa-enable-confirm", req, function(response) { + alert("Your emergency scratch code is: " + response.scratch + "\nRecord this code and store it in a safe place. You will not be able to view it again!"); + location.href = "/profile/auth"; + }); + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Highlight the auth subnav link + var subnavLinks = document.getElementsByClassName(elements.subnav); + for (var i = 0; i < subnavLinks.length; i++) { + if (subnavLinks[i].pathname == "/profile/auth") + subnavLinks[i].className = elements.subnav + " " + elements.subnavActive; + else + subnavLinks[i].className = elements.subnav; + } + + // Get element references + ui.confirmButton = document.getElementById(elements.confirmButton); + ui.qrCode = document.getElementById(elements.qrCode); + ui.tfaSecret = document.getElementById(elements.tfaSecret); + ui.tfaToken = document.getElementById(elements.tfaToken); + + // Register event handlers + ui.confirmButton.addEventListener("click", handleConfirm); + + // Get data from API + apiPost("/profile/tfa-enable", {}, displaySecret); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})(); diff --git a/user/index.shtml b/user/index.shtml index ff94a61..449eed6 100644 --- a/user/index.shtml +++ b/user/index.shtml @@ -35,7 +35,7 @@ along with Linode Manager Classic. If not, see . Username - Email + Last Login Restricted Options diff --git a/user/user.js b/user/user.js index 008985c..f535763 100644 --- a/user/user.js +++ b/user/user.js @@ -15,7 +15,7 @@ * along with Linode Manager Classic. If not, see . */ -import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/global.js"; +import { settings, elements, apiGet, md5, parseParams, setupHeader, timeString } from "/global.js"; (function() { @@ -30,6 +30,7 @@ import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/glob // Data received from API calls var data = {}; + data.logins = []; data.users = []; // Static references to UI elements @@ -37,6 +38,11 @@ import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/glob ui.loading = {}; ui.userTable = {}; + // Temporary state + var state = {}; + state.haveLogins = false; + state.haveUsers = false; + // Generates a user table row var createUserRow = function(user, alt) { @@ -51,7 +57,7 @@ import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/glob imgLink.href = "/user/edit?user=" + user.username; var img = document.createElement("img"); img.className = elements.profileImg; - img.src = "https://www.gravatar.com/avatar/" + md5(user.email); + img.src = "https://www.gravatar.com/avatar/" + md5(user.email, false); img.alt = user.username; imgLink.appendChild(img); var separator = document.createElement("span"); @@ -64,9 +70,29 @@ import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/glob nameCell.appendChild(nameLink); row.appendChild(nameCell); - var email = document.createElement("td"); - email.innerHTML = user.email; - row.appendChild(email); + var lastLogin = document.createElement("td"); + var loginDate = null; + for (var i = data.logins.length - 1; i >= 0; i--) { + if (data.logins[i].username == user.username) { + loginDate = new Date(data.logins[i].datetime + "Z"); + break; + } + } + if (loginDate) { + var now = new Date(); + var ago = document.createElement("span"); + ago.innerHTML = timeString(now - loginDate, true); + lastLogin.appendChild(ago); + var br = document.createElement("br"); + lastLogin.appendChild(br); + var dateString = document.createElement("span"); + dateString.className = elements.info; + dateString.innerHTML = loginDate.toLocaleString(); + lastLogin.appendChild(dateString); + } else { + lastLogin.innerHTML = "Never"; + } + row.appendChild(lastLogin); var restricted = document.createElement("td"); if (user.restricted) { @@ -102,6 +128,27 @@ import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/glob return row; }; + // Callback for logins API call + var displayLogins = function(response) + { + data.logins = data.logins.concat(response.data); + + // Request the next page if there are more + if (response.page != response.pages) { + apiGet("/account/logins?page=" + (response.page + 1), displayLogins, null); + return; + } + + state.haveLogins = true; + if (state.haveUsers) { + ui.loading.remove(); + + // Add users to table + for (var i = 0; i < data.users.length; i++) + ui.userTable.appendChild(createUserRow(data.users[i], ui.userTable.children.length % 2)); + } + }; + // Callback for users API call var displayUsers = function(response) { @@ -113,11 +160,14 @@ import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/glob return; } - ui.loading.remove(); + state.haveUsers = true; + if (state.haveLogins) { + ui.loading.remove(); - // Add users to table - for (var i = 0; i < data.users.length; i++) - ui.userTable.appendChild(createUserRow(data.users[i], ui.userTable.children.length % 2)); + // Add users to table + for (var i = 0; i < data.users.length; i++) + ui.userTable.appendChild(createUserRow(data.users[i], ui.userTable.children.length % 2)); + } }; // Initial setup @@ -141,6 +191,7 @@ import { settings, elements, apiGet, md5, parseParams, setupHeader } from "/glob // Get data from API apiGet("/account/users", displayUsers, null); + apiGet("/account/logins", displayLogins, null); }; // Attach onload handler