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 @@
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+ This controls what authentication methods are allowed to connect to the
+ Lish console servers. About the Lish console...
+
+
+
+
+
+
+
+
+
+
+
+
+
Lish Keys
+
+
+
+
+
Description
+
Place 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
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
Scan the QR code (or enter the key) into your two-factor application.
+
+
+
Secret Key
+
+
Save this key in a safe place, it will only be shown once.
+
+
+
QR Code
+
+
+
+
+
+
+
+
+
+
Step 2
+
+
+
+
+
Description
+
Use 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:
+
+
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
+
An image of the front and back of the matching government-issued photo ID
+
+
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