This commit is contained in:
Louis Lam
2026-06-29 13:05:14 +08:00
parent 0e8437df73
commit f4f9c21ed8
7 changed files with 185 additions and 142 deletions
+1 -22
View File
@@ -115,27 +115,6 @@ async function sendProxyList(socket) {
return list; return list;
} }
/**
* Emit API key list to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<void>}
*/
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. * Emits the version information to the client.
* @param {Socket} socket Socket.io socket instance * @param {Socket} socket Socket.io socket instance
@@ -240,7 +219,7 @@ module.exports = {
sendImportantHeartbeatList, sendImportantHeartbeatList,
sendHeartbeatList, sendHeartbeatList,
sendProxyList, sendProxyList,
sendAPIKeyList,
sendInfo, sendInfo,
sendDockerHostList, sendDockerHostList,
sendRemoteBrowserList, sendRemoteBrowserList,
+2 -3
View File
@@ -4,7 +4,7 @@
* DO NOT require("./server") in other modules, it likely creates circular dependency! * DO NOT require("./server") in other modules, it likely creates circular dependency!
*/ */
import { getRandomInt, isDev, log, sleep } from "../src/util"; 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 { createBetterAuthRouter, needSetup } from "./routers/better-auth-router";
import { betterAuthSocketHandler } from "./socket-handlers/better-auth-socket-handler"; import { betterAuthSocketHandler } from "./socket-handlers/better-auth-socket-handler";
import { loadEnvFile } from "node:process"; import { loadEnvFile } from "node:process";
@@ -153,7 +153,6 @@ const {
sendInfo, sendInfo,
sendProxyList, sendProxyList,
sendDockerHostList, sendDockerHostList,
sendAPIKeyList,
sendRemoteBrowserList, sendRemoteBrowserList,
sendMonitorTypeList, sendMonitorTypeList,
} = require("./client"); } = require("./client");
@@ -377,6 +376,7 @@ app.use(function (req, res, next) {
if (session) { if (session) {
socket.userID = session.user.id; socket.userID = session.user.id;
socket.session = session; socket.session = session;
socket.headers = createHeaders(socket.request.headers.cookie);
socket.emit("session", session.user.username); socket.emit("session", session.user.username);
log.debug("auth", `Session active:`, session.session.ipAddress, 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), sendNotificationList(socket),
sendProxyList(socket), sendProxyList(socket),
sendDockerHostList(socket), sendDockerHostList(socket),
sendAPIKeyList(socket),
sendRemoteBrowserList(socket), sendRemoteBrowserList(socket),
sendMonitorTypeList(socket), sendMonitorTypeList(socket),
]); ]);
@@ -1,12 +1,8 @@
const { checkLogin } = require("../util-server");
const { log } = require("../../src/util"); const { log } = require("../../src/util");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { nanoid } = require("nanoid"); const dayjs = require("dayjs");
const passwordHash = require("../password-hash");
const apicache = require("../modules/apicache"); const apicache = require("../modules/apicache");
const APIKey = require("../model/api_key"); const { auth, createHeaders, checkLogin } = require("../better-auth");
const { Settings } = require("../settings");
const { sendAPIKeyList } = require("../client");
/** /**
* Handlers for API keys * Handlers for API keys
@@ -14,34 +10,38 @@ const { sendAPIKeyList } = require("../client");
* @returns {void} * @returns {void}
*/ */
module.exports.apiKeySocketHandler = (socket) => { module.exports.apiKeySocketHandler = (socket) => {
// Add a new api key // Add a new better-auth api key
socket.on("addAPIKey", async (key, callback) => { socket.on("addAPIKey", async (key, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
let clearKey = nanoid(40); let expiresIn;
let hashedKey = await passwordHash.generate(clearKey);
key["key"] = hashedKey; if (key.expires) {
let bean = await APIKey.save(key, socket.userID); 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", "Added API Key");
log.debug("apikeys", 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({ callback({
ok: true, ok: true,
msg: "successAdded", msg: "successAdded",
msgi18n: true, msgi18n: true,
key: formattedKey, key: result.key,
keyID: bean.id, keyID: result.id,
}); });
} catch (e) { } catch (e) {
callback({ callback({
@@ -54,9 +54,19 @@ module.exports.apiKeySocketHandler = (socket) => {
socket.on("getAPIKeyList", async (callback) => { socket.on("getAPIKeyList", async (callback) => {
try { try {
checkLogin(socket); 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({ callback({
ok: true, ok: true,
apiKeyList: baKeys.apiKeys,
legacyAPIKeyList: legacyAPIKeyList,
}); });
} catch (e) { } catch (e) {
log.error("apikeys", e); log.error("apikeys", e);
@@ -71,19 +81,18 @@ module.exports.apiKeySocketHandler = (socket) => {
try { try {
checkLogin(socket); checkLogin(socket);
log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`); await auth().api.deleteApiKey({
body: {
await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [keyID, socket.userID]); keyId: keyID,
},
apicache.clear(); headers: createHeaders(socket.request.headers.cookie),
});
callback({ callback({
ok: true, ok: true,
msg: "successDeleted", msg: "successDeleted",
msgi18n: true, msgi18n: true,
}); });
await sendAPIKeyList(socket);
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
@@ -92,48 +101,25 @@ module.exports.apiKeySocketHandler = (socket) => {
} }
}); });
socket.on("disableAPIKey", async (keyID, callback) => { socket.on("deleteLegacyAPIKey", async (keyID, callback) => {
try { try {
checkLogin(socket); 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(); apicache.clear();
callback({ const legacyBeans = await R.find("api_key");
ok: true, const legacyKeys = legacyBeans.map((bean) => bean.toPublicJSON());
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();
callback({ callback({
ok: true, ok: true,
msg: "successEnabled", msg: "successDeleted",
msgi18n: true, msgi18n: true,
legacyKeys: legacyKeys,
}); });
await sendAPIKeyList(socket);
} catch (e) { } catch (e) {
callback({ callback({
ok: false, ok: false,
+1 -2
View File
@@ -1,6 +1,5 @@
import { createAuthClient } from "better-auth/vue"; import { createAuthClient } from "better-auth/vue";
import { twoFactorClient, usernameClient } from "better-auth/client/plugins"; import { twoFactorClient, usernameClient } from "better-auth/client/plugins";
import { apiKeyClient } from "@better-auth/api-key/client";
import { reconnectSocket } from "./mixins/socket"; import { reconnectSocket } from "./mixins/socket";
export const baseURL = export const baseURL =
@@ -10,7 +9,7 @@ export const baseURL =
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL, baseURL,
plugins: [usernameClient(), twoFactorClient(), apiKeyClient()], plugins: [usernameClient(), twoFactorClient()],
}); });
authClient.signIn; authClient.signIn;
+1
View File
@@ -149,6 +149,7 @@ export default {
this.clearKey = res.key; this.clearKey = res.key;
this.keymodal.show(); this.keymodal.show();
this.clearForm(); this.clearForm();
this.$emit("keyAdded");
} else { } else {
this.$root.toastError(res.msg); this.$root.toastError(res.msg);
} }
+119 -49
View File
@@ -11,15 +11,47 @@
</button> </button>
</div> </div>
<div> <div v-if="apiKeyList.length === 0 && legacyKeyList.length === 0">
<span <span class="d-flex align-items-center justify-content-center my-3">
v-if="Object.keys(keyList).length === 0"
class="d-flex align-items-center justify-content-center my-3"
>
{{ $t("No API Keys") }} {{ $t("No API Keys") }}
</span> </span>
</div>
<div v-for="(item, index) in keyList" :key="index" class="item" :class="item.status"> <div>
<div v-for="(item, index) in apiKeyList" :key="index" class="item" :class="apiKeyStatus(item)">
<div class="left-part">
<div class="circle"></div>
<div class="info">
<div class="title">{{ item.name }}</div>
<div class="status">
{{ item.enabled ? $t("apiKey-active") : $t("apiKey-inactive") }}
</div>
<div class="date">{{ $t("createdAt", { date: item.createdAt }) }}</div>
<div class="date">
{{ $t("Expires") }}:
{{ item.expiresAt || $t("Never") }}
</div>
<div v-if="item.start" class="start">
{{ $t("startsWith", { start: item.start }) }}
</div>
</div>
</div>
<div class="buttons">
<div class="btn-group" role="group">
<button class="btn btn-danger" @click="deleteDialog(item.id)">
<font-awesome-icon icon="trash" />
{{ $t("Delete") }}
</button>
</div>
</div>
</div>
</div>
<div v-if="legacyKeyList.length > 0">
<h4>{{ $t("Legacy API Keys") }}</h4>
<div v-for="(item, index) in legacyKeyList" :key="index" class="item" :class="item.status">
<div class="left-part"> <div class="left-part">
<div class="circle"></div> <div class="circle"></div>
<div class="info"> <div class="info">
@@ -37,17 +69,7 @@
<div class="buttons"> <div class="buttons">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)"> <button class="btn btn-danger" @click="legacyDeleteDialog(item.id)">
<font-awesome-icon icon="pause" />
{{ $t("Disable") }}
</button>
<button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)">
<font-awesome-icon icon="play" />
{{ $t("Enable") }}
</button>
<button class="btn btn-danger" @click="deleteDialog(item.id)">
<font-awesome-icon icon="trash" /> <font-awesome-icon icon="trash" />
{{ $t("Delete") }} {{ $t("Delete") }}
</button> </button>
@@ -63,15 +85,21 @@
</a> </a>
</div> </div>
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disableKey">
{{ $t("disableAPIKeyMsg") }}
</Confirm>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteKey"> <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteKey">
{{ $t("deleteAPIKeyMsg") }} {{ $t("deleteAPIKeyMsg") }}
</Confirm> </Confirm>
<APIKeyDialog ref="apiKeyDialog" /> <Confirm
ref="confirmLegacyDelete"
btn-style="btn-danger"
:yes-text="$t('Yes')"
:no-text="$t('No')"
@yes="legacyDeleteKey"
>
{{ $t("deleteAPIKeyMsg") }}
</Confirm>
<APIKeyDialog ref="apiKeyDialog" @keyAdded="loadAPIKeys" />
</div> </div>
</template> </template>
@@ -86,23 +114,45 @@ export default {
}, },
data() { data() {
return { return {
apiKeyList: [],
legacyKeyList: [],
selectedKeyID: null, selectedKeyID: null,
selectedLegacyKeyID: null,
}; };
}, },
computed: { computed: {
keyList() {
let result = Object.values(this.$root.apiKeyList);
return result;
},
settings() { settings() {
return this.$parent.$parent.$parent.settings; return this.$parent.$parent.$parent.settings;
}, },
}, },
mounted() {
this.loadAPIKeys();
},
methods: { 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 * Show dialog to confirm deletion of Better Auth key
* @param {number} keyID ID of monitor that is being deleted * @param {string} keyID ID of key to delete
* @returns {void} * @returns {void}
*/ */
deleteDialog(keyID) { deleteDialog(keyID) {
@@ -111,42 +161,39 @@ export default {
}, },
/** /**
* Delete a key * Delete a Better Auth key
* @returns {void} * @returns {void}
*/ */
deleteKey() { deleteKey() {
this.$root.deleteAPIKey(this.selectedKeyID, (res) => { 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 * Show dialog to confirm deletion of legacy key
* @param {number} keyID ID of key to pause * @param {number} keyID ID of key to delete
* @returns {void} * @returns {void}
*/ */
disableDialog(keyID) { legacyDeleteDialog(keyID) {
this.selectedKeyID = keyID; this.selectedLegacyKeyID = keyID;
this.$refs.confirmPause.show(); this.$refs.confirmLegacyDelete.show();
}, },
/** /**
* Pause API key * Delete a legacy key
* @returns {void} * @returns {void}
*/ */
disableKey() { legacyDeleteKey() {
this.$root.getSocket().emit("disableAPIKey", this.selectedKeyID, (res) => { this.$root.deleteLegacyAPIKey(this.selectedLegacyKeyID, (res) => {
this.$root.toastRes(res); if (res.ok) {
}); this.legacyKeyList = res.legacyKeys;
}, }
/**
* Resume API key
* @param {number} id Key to resume
* @returns {void}
*/
enableKey(id) {
this.$root.getSocket().emit("enableAPIKey", id, (res) => {
this.$root.toastRes(res); 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 { .add-btn {
padding-top: 20px; padding-top: 20px;
padding-bottom: 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 { .dark {
.item { .item {
&:hover { &:hover {
+15 -6
View File
@@ -39,7 +39,7 @@ export default {
monitorList: {}, monitorList: {},
monitorTypeList: {}, monitorTypeList: {},
maintenanceList: {}, maintenanceList: {},
apiKeyList: {},
heartbeatList: {}, heartbeatList: {},
avgPingList: {}, avgPingList: {},
uptimeList: {}, uptimeList: {},
@@ -164,10 +164,6 @@ export default {
this.maintenanceList = data; this.maintenanceList = data;
}); });
socket.on("apiKeyList", (data) => {
this.apiKeyList = data;
});
socket.on("notificationList", (data) => { socket.on("notificationList", (data) => {
this.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 * @param {socketCB} callback Callback for socket response
* @returns {void} * @returns {void}
*/ */
@@ -434,6 +430,19 @@ export default {
socket.emit("getAPIKeyList", callback); 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 * Add a monitor
* @param {object} monitor Object representing monitor to add * @param {object} monitor Object representing monitor to add