From c58e2fc545fc801130e95232d450a54d192e0ad6 Mon Sep 17 00:00:00 2001 From: "L. Bradley LaBoon" Date: Thu, 11 Mar 2021 10:37:07 -0500 Subject: [PATCH] Implemented user profile settings, OAuth apps, and maintenance windows. Other minor fixes/improvements --- README.md | 4 +- account/oauth_apps/app/app.css | 27 ++ account/oauth_apps/app/app.js | 137 +++++++++ account/oauth_apps/app/index.shtml | 63 +++++ account/oauth_apps/index.shtml | 53 ++++ account/oauth_apps/oauth_apps.css | 32 +++ account/oauth_apps/oauth_apps.js | 163 +++++++++++ favicon.ico | Bin 1150 -> 7660 bytes global.css | 2 +- global.js | 84 +++++- include/account_subnav.html | 3 +- include/profile_subnav.html | 10 + linodes/backup_details/backup_details.css | 4 - linodes/backup_details/index.shtml | 2 +- linodes/dashboard/dashboard.js | 9 + linodes/linodes.js | 73 ++++- profile/api/add/add.css | 57 ++++ profile/api/add/add.js | 191 +++++++++++++ profile/api/add/index.shtml | 93 ++++++ profile/api/api.css | 41 +++ profile/api/api.js | 327 ++++++++++++++++++++++ profile/api/index.shtml | 67 +++++ profile/auth/auth.css | 31 ++ profile/auth/auth.js | 164 +++++++++++ profile/auth/index.shtml | 80 ++++++ profile/index.shtml | 93 ++++++ profile/lish/index.shtml | 87 ++++++ profile/lish/lish.css | 39 +++ profile/lish/lish.js | 115 ++++++++ profile/profile.css | 31 ++ profile/profile.js | 127 +++++++++ profile/referrals/index.shtml | 62 ++++ profile/referrals/referrals.css | 35 +++ profile/referrals/referrals.js | 75 +++++ profile/ssh/add/add.css | 27 ++ profile/ssh/add/add.js | 69 +++++ profile/ssh/add/index.shtml | 55 ++++ profile/ssh/index.shtml | 51 ++++ profile/ssh/remove/index.shtml | 36 +++ profile/ssh/remove/remove.css | 22 ++ profile/ssh/remove/remove.js | 80 ++++++ profile/ssh/ssh.css | 31 ++ profile/ssh/ssh.js | 120 ++++++++ profile/twofactor/index.shtml | 98 +++++++ profile/twofactor/twofactor.css | 52 ++++ profile/twofactor/twofactor.js | 95 +++++++ user/index.shtml | 2 +- user/user.js | 69 ++++- 48 files changed, 3152 insertions(+), 36 deletions(-) create mode 100644 account/oauth_apps/app/app.css create mode 100644 account/oauth_apps/app/app.js create mode 100644 account/oauth_apps/app/index.shtml create mode 100644 account/oauth_apps/index.shtml create mode 100644 account/oauth_apps/oauth_apps.css create mode 100644 account/oauth_apps/oauth_apps.js create mode 100644 include/profile_subnav.html create mode 100644 profile/api/add/add.css create mode 100644 profile/api/add/add.js create mode 100644 profile/api/add/index.shtml create mode 100644 profile/api/api.css create mode 100644 profile/api/api.js create mode 100644 profile/api/index.shtml create mode 100644 profile/auth/auth.css create mode 100644 profile/auth/auth.js create mode 100644 profile/auth/index.shtml create mode 100644 profile/index.shtml create mode 100644 profile/lish/index.shtml create mode 100644 profile/lish/lish.css create mode 100644 profile/lish/lish.js create mode 100644 profile/profile.css create mode 100644 profile/profile.js create mode 100644 profile/referrals/index.shtml create mode 100644 profile/referrals/referrals.css create mode 100644 profile/referrals/referrals.js create mode 100644 profile/ssh/add/add.css create mode 100644 profile/ssh/add/add.js create mode 100644 profile/ssh/add/index.shtml create mode 100644 profile/ssh/index.shtml create mode 100644 profile/ssh/remove/index.shtml create mode 100644 profile/ssh/remove/remove.css create mode 100644 profile/ssh/remove/remove.js create mode 100644 profile/ssh/ssh.css create mode 100644 profile/ssh/ssh.js create mode 100644 profile/twofactor/index.shtml create mode 100644 profile/twofactor/twofactor.css create mode 100644 profile/twofactor/twofactor.js 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 c7eccdfb225a4b6c5a7e1643dc8dce6b0bcad7c4..94001e9840ed956e4076c01c45db0203432b6731 100644 GIT binary patch literal 7660 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 literal 1150 zcmZQzU<5(|0R|vYV8~!$U=RbcG=LZ+qyWT>V3L8s0VszBFoKnUlu;2YMDh#rtY%GR zxXG{{h#g5W8>rV2h~EoL=Qs+~`yYz;6JrigUq2B42Vw4YtOt?w<247UUJ!`S!}Nk^ z=JSjfvFJybgA`ss^@YfKL2Sm$j5qP<|BoCdK=ljJ_2bnG3KvxS?<4D%Eaky(_f3Y2 zF#SM%Aismd3`UDJaN+YiC=3oF=?{eI1JUx~{2v9XIJPog0;)p9A1vHpG_w9in0{#= z-os+{+>gcVxc`H&U;^h&woS|j8LlxrMKcRo|3SH6zMJ9>+~;BXu+jXv9Oqf~G96~P z!SE5;{SwYR-PqLQ5qk)fQ-t{+M1%N1TnNM)ff(#|Ec8X7SR%4MTx_7aKpUpdate 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