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;
}
/**
* 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.
* @param {Socket} socket Socket.io socket instance
@@ -240,7 +219,7 @@ module.exports = {
sendImportantHeartbeatList,
sendHeartbeatList,
sendProxyList,
sendAPIKeyList,
sendInfo,
sendDockerHostList,
sendRemoteBrowserList,
+2 -3
View File
@@ -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),
]);
@@ -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,
+1 -2
View File
@@ -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;
+1
View File
@@ -149,6 +149,7 @@ export default {
this.clearKey = res.key;
this.keymodal.show();
this.clearForm();
this.$emit("keyAdded");
} else {
this.$root.toastError(res.msg);
}
+119 -49
View File
@@ -11,15 +11,47 @@
</button>
</div>
<div>
<span
v-if="Object.keys(keyList).length === 0"
class="d-flex align-items-center justify-content-center my-3"
>
<div v-if="apiKeyList.length === 0 && legacyKeyList.length === 0">
<span class="d-flex align-items-center justify-content-center my-3">
{{ $t("No API Keys") }}
</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="circle"></div>
<div class="info">
@@ -37,17 +69,7 @@
<div class="buttons">
<div class="btn-group" role="group">
<button v-if="item.active" class="btn btn-normal" @click="disableDialog(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)">
<button class="btn btn-danger" @click="legacyDeleteDialog(item.id)">
<font-awesome-icon icon="trash" />
{{ $t("Delete") }}
</button>
@@ -63,15 +85,21 @@
</a>
</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">
{{ $t("deleteAPIKeyMsg") }}
</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>
</template>
@@ -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 {
+15 -6
View File
@@ -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