diff --git a/server/client.js b/server/client.js index 5e7596cb9..7cf234105 100644 --- a/server/client.js +++ b/server/client.js @@ -115,27 +115,6 @@ async function sendProxyList(socket) { return list; } -/** - * Emit API key list to client - * @param {Socket} socket Socket.io socket instance - * @returns {Promise} - */ -async function sendAPIKeyList(socket) { - const timeLogger = new TimeLogger(); - - let result = []; - const list = await R.find("api_key"); - - for (let bean of list) { - result.push(bean.toPublicJSON()); - } - - io.to(socket.userID).emit("apiKeyList", result); - timeLogger.print("Sent API Key List"); - - return list; -} - /** * Emits the version information to the client. * @param {Socket} socket Socket.io socket instance @@ -240,7 +219,7 @@ module.exports = { sendImportantHeartbeatList, sendHeartbeatList, sendProxyList, - sendAPIKeyList, + sendInfo, sendDockerHostList, sendRemoteBrowserList, diff --git a/server/server.js b/server/server.js index 19555c6cf..127f9bc35 100644 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,7 @@ * DO NOT require("./server") in other modules, it likely creates circular dependency! */ import { getRandomInt, isDev, log, sleep } from "../src/util"; -import { auth, doubleCheckPassword, getDisableAuthSession, getSession } from "./better-auth"; +import { auth, createHeaders, doubleCheckPassword, getDisableAuthSession, getSession } from "./better-auth"; import { createBetterAuthRouter, needSetup } from "./routers/better-auth-router"; import { betterAuthSocketHandler } from "./socket-handlers/better-auth-socket-handler"; import { loadEnvFile } from "node:process"; @@ -153,7 +153,6 @@ const { sendInfo, sendProxyList, sendDockerHostList, - sendAPIKeyList, sendRemoteBrowserList, sendMonitorTypeList, } = require("./client"); @@ -377,6 +376,7 @@ app.use(function (req, res, next) { if (session) { socket.userID = session.user.id; socket.session = session; + socket.headers = createHeaders(socket.request.headers.cookie); socket.emit("session", session.user.username); log.debug("auth", `Session active:`, session.session.ipAddress, session.user.username); } @@ -1445,7 +1445,6 @@ async function afterLogin(socket, user) { sendNotificationList(socket), sendProxyList(socket), sendDockerHostList(socket), - sendAPIKeyList(socket), sendRemoteBrowserList(socket), sendMonitorTypeList(socket), ]); diff --git a/server/socket-handlers/api-key-socket-handler.js b/server/socket-handlers/api-key-socket-handler.js index 9e6594a69..9214f7bc4 100644 --- a/server/socket-handlers/api-key-socket-handler.js +++ b/server/socket-handlers/api-key-socket-handler.js @@ -1,12 +1,8 @@ -const { checkLogin } = require("../util-server"); const { log } = require("../../src/util"); const { R } = require("redbean-node"); -const { nanoid } = require("nanoid"); -const passwordHash = require("../password-hash"); +const dayjs = require("dayjs"); const apicache = require("../modules/apicache"); -const APIKey = require("../model/api_key"); -const { Settings } = require("../settings"); -const { sendAPIKeyList } = require("../client"); +const { auth, createHeaders, checkLogin } = require("../better-auth"); /** * Handlers for API keys @@ -14,34 +10,38 @@ const { sendAPIKeyList } = require("../client"); * @returns {void} */ module.exports.apiKeySocketHandler = (socket) => { - // Add a new api key + // Add a new better-auth api key socket.on("addAPIKey", async (key, callback) => { try { checkLogin(socket); - let clearKey = nanoid(40); - let hashedKey = await passwordHash.generate(clearKey); - key["key"] = hashedKey; - let bean = await APIKey.save(key, socket.userID); + let expiresIn; + + if (key.expires) { + expiresIn = dayjs(key.expires).diff(dayjs(), "second"); + if (expiresIn <= 0) { + expiresIn = undefined; + } + } + + const result = await auth().api.createApiKey({ + body: { + name: key.name, + expiresIn, + userId: socket.userID, + }, + headers: socket.headers, + }); log.debug("apikeys", "Added API Key"); log.debug("apikeys", key); - // Append key ID and prefix to start of key separated by _, used to get - // correct hash when validating key. - let formattedKey = "uk" + bean.id + "_" + clearKey; - await sendAPIKeyList(socket); - - // Enable API auth if the user creates a key, otherwise only basic - // auth will be used for API. - await Settings.set("apiKeysEnabled", true); - callback({ ok: true, msg: "successAdded", msgi18n: true, - key: formattedKey, - keyID: bean.id, + key: result.key, + keyID: result.id, }); } catch (e) { callback({ @@ -54,9 +54,19 @@ module.exports.apiKeySocketHandler = (socket) => { socket.on("getAPIKeyList", async (callback) => { try { checkLogin(socket); - await sendAPIKeyList(socket); + + const baKeys = await auth().api.listApiKeys({ + query: {}, + headers: socket.headers, + }); + + const legacyBeans = await R.find("api_key"); + const legacyAPIKeyList = legacyBeans.map((bean) => bean.toPublicJSON()); + callback({ ok: true, + apiKeyList: baKeys.apiKeys, + legacyAPIKeyList: legacyAPIKeyList, }); } catch (e) { log.error("apikeys", e); @@ -71,19 +81,18 @@ module.exports.apiKeySocketHandler = (socket) => { try { checkLogin(socket); - log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`); - - await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [keyID, socket.userID]); - - apicache.clear(); + await auth().api.deleteApiKey({ + body: { + keyId: keyID, + }, + headers: createHeaders(socket.request.headers.cookie), + }); callback({ ok: true, msg: "successDeleted", msgi18n: true, }); - - await sendAPIKeyList(socket); } catch (e) { callback({ ok: false, @@ -92,48 +101,25 @@ module.exports.apiKeySocketHandler = (socket) => { } }); - socket.on("disableAPIKey", async (keyID, callback) => { + socket.on("deleteLegacyAPIKey", async (keyID, callback) => { try { checkLogin(socket); - log.debug("apikeys", `Disabled Key: ${keyID} User ID: ${socket.userID}`); + log.debug("apikeys", `Deleted Legacy API Key: ${keyID} User ID: ${socket.userID}`); - await R.exec("UPDATE api_key SET active = 0 WHERE id = ? ", [keyID]); + await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [keyID, socket.userID]); apicache.clear(); - callback({ - ok: true, - msg: "successDisabled", - msgi18n: true, - }); - - await sendAPIKeyList(socket); - } catch (e) { - callback({ - ok: false, - msg: e.message, - }); - } - }); - - socket.on("enableAPIKey", async (keyID, callback) => { - try { - checkLogin(socket); - - log.debug("apikeys", `Enabled Key: ${keyID} User ID: ${socket.userID}`); - - await R.exec("UPDATE api_key SET active = 1 WHERE id = ? ", [keyID]); - - apicache.clear(); + const legacyBeans = await R.find("api_key"); + const legacyKeys = legacyBeans.map((bean) => bean.toPublicJSON()); callback({ ok: true, - msg: "successEnabled", + msg: "successDeleted", msgi18n: true, + legacyKeys: legacyKeys, }); - - await sendAPIKeyList(socket); } catch (e) { callback({ ok: false, diff --git a/src/auth-client.ts b/src/auth-client.ts index 1870e297f..7256da409 100644 --- a/src/auth-client.ts +++ b/src/auth-client.ts @@ -1,6 +1,5 @@ import { createAuthClient } from "better-auth/vue"; import { twoFactorClient, usernameClient } from "better-auth/client/plugins"; -import { apiKeyClient } from "@better-auth/api-key/client"; import { reconnectSocket } from "./mixins/socket"; export const baseURL = @@ -10,7 +9,7 @@ export const baseURL = export const authClient = createAuthClient({ baseURL, - plugins: [usernameClient(), twoFactorClient(), apiKeyClient()], + plugins: [usernameClient(), twoFactorClient()], }); authClient.signIn; diff --git a/src/components/APIKeyDialog.vue b/src/components/APIKeyDialog.vue index ec56c68c0..2d459e8b3 100644 --- a/src/components/APIKeyDialog.vue +++ b/src/components/APIKeyDialog.vue @@ -149,6 +149,7 @@ export default { this.clearKey = res.key; this.keymodal.show(); this.clearForm(); + this.$emit("keyAdded"); } else { this.$root.toastError(res.msg); } diff --git a/src/components/settings/APIKeys.vue b/src/components/settings/APIKeys.vue index f5b32fa8b..8b4966e17 100644 --- a/src/components/settings/APIKeys.vue +++ b/src/components/settings/APIKeys.vue @@ -11,15 +11,47 @@ -
- +
+ {{ $t("No API Keys") }} +
-
+
+
+
+
+
+
{{ item.name }}
+
+ {{ item.enabled ? $t("apiKey-active") : $t("apiKey-inactive") }} +
+
{{ $t("createdAt", { date: item.createdAt }) }}
+
+ {{ $t("Expires") }}: + {{ item.expiresAt || $t("Never") }} +
+
+ {{ $t("startsWith", { start: item.start }) }} +
+
+
+ +
+
+ +
+
+
+
+ +
+

{{ $t("Legacy API Keys") }}

+ +
@@ -37,17 +69,7 @@
- - - - - @@ -63,15 +85,21 @@
- - {{ $t("disableAPIKeyMsg") }} - - {{ $t("deleteAPIKeyMsg") }} - + + {{ $t("deleteAPIKeyMsg") }} + + +
@@ -86,23 +114,45 @@ export default { }, data() { return { + apiKeyList: [], + legacyKeyList: [], selectedKeyID: null, + selectedLegacyKeyID: null, }; }, computed: { - keyList() { - let result = Object.values(this.$root.apiKeyList); - return result; - }, settings() { return this.$parent.$parent.$parent.settings; }, }, + mounted() { + this.loadAPIKeys(); + }, + methods: { + loadAPIKeys() { + this.$root.getAPIKeyList((res) => { + if (res.ok) { + this.apiKeyList = res.apiKeyList; + this.legacyKeyList = res.legacyAPIKeyList; + } + }); + }, + + apiKeyStatus(item) { + if (!item.enabled) { + return "inactive"; + } + if (item.expiresAt && new Date(item.expiresAt) < new Date()) { + return "expired"; + } + return "active"; + }, + /** - * Show dialog to confirm deletion - * @param {number} keyID ID of monitor that is being deleted + * Show dialog to confirm deletion of Better Auth key + * @param {string} keyID ID of key to delete * @returns {void} */ deleteDialog(keyID) { @@ -111,42 +161,39 @@ export default { }, /** - * Delete a key + * Delete a Better Auth key * @returns {void} */ deleteKey() { this.$root.deleteAPIKey(this.selectedKeyID, (res) => { - this.$root.toastRes(res); + if (res.ok) { + this.loadAPIKeys(); + this.$root.toastRes(res); + } else { + this.$root.toastError(res.msg); + } }); }, /** - * Show dialog to confirm pause - * @param {number} keyID ID of key to pause + * Show dialog to confirm deletion of legacy key + * @param {number} keyID ID of key to delete * @returns {void} */ - disableDialog(keyID) { - this.selectedKeyID = keyID; - this.$refs.confirmPause.show(); + legacyDeleteDialog(keyID) { + this.selectedLegacyKeyID = keyID; + this.$refs.confirmLegacyDelete.show(); }, /** - * Pause API key + * Delete a legacy key * @returns {void} */ - disableKey() { - this.$root.getSocket().emit("disableAPIKey", this.selectedKeyID, (res) => { - this.$root.toastRes(res); - }); - }, - - /** - * Resume API key - * @param {number} id Key to resume - * @returns {void} - */ - enableKey(id) { - this.$root.getSocket().emit("enableAPIKey", id, (res) => { + legacyDeleteKey() { + this.$root.deleteLegacyAPIKey(this.selectedLegacyKeyID, (res) => { + if (res.ok) { + this.legacyKeyList = res.legacyKeys; + } this.$root.toastRes(res); }); }, @@ -165,6 +212,18 @@ export default { } } +.section-title { + margin-top: 24px; + margin-bottom: 12px; + font-size: 18px; + font-weight: 600; + color: $dark-font-color; + + .dark & { + color: $dark-font-color2; + } +} + .add-btn { padding-top: 20px; padding-bottom: 20px; @@ -257,6 +316,17 @@ export default { } } +.start { + margin-top: 5px; + display: block; + font-size: 13px; + color: $dark-font-color; + + .dark & { + color: $dark-font-color2; + } +} + .dark { .item { &:hover { diff --git a/src/mixins/socket.js b/src/mixins/socket.js index ee4469d0b..da8f85197 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -39,7 +39,7 @@ export default { monitorList: {}, monitorTypeList: {}, maintenanceList: {}, - apiKeyList: {}, + heartbeatList: {}, avgPingList: {}, uptimeList: {}, @@ -164,10 +164,6 @@ export default { this.maintenanceList = data; }); - socket.on("apiKeyList", (data) => { - this.apiKeyList = data; - }); - socket.on("notificationList", (data) => { this.notificationList = data; }); @@ -423,7 +419,7 @@ export default { }, /** - * Send list of API keys + * Send list of API keys (callback receives BA keys and legacy keys) * @param {socketCB} callback Callback for socket response * @returns {void} */ @@ -434,6 +430,19 @@ export default { socket.emit("getAPIKeyList", callback); }, + /** + * Delete specified legacy API key + * @param {int} keyID ID of key to delete + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + deleteLegacyAPIKey(keyID, callback) { + if (!callback) { + callback = () => {}; + } + socket.emit("deleteLegacyAPIKey", keyID, callback); + }, + /** * Add a monitor * @param {object} monitor Object representing monitor to add