🐛 fix: restore plugin auth and proxy connection info

This commit is contained in:
2026-08-13 02:32:39 +07:00
parent 3df22125dd
commit 5567ac6594
20 changed files with 141 additions and 65 deletions
+5 -1
View File
@@ -17,6 +17,10 @@ const operatorResourceSync = new OperatorResourceSync();
export const userService = new UserService(userRepo);
export const serverService = new ServerService(serverRepo, k8sService, operatorResourceSync);
export const reverseProxyService = new ReverseProxyService(reverseProxyRepo, operatorResourceSync);
export const reverseProxyService = new ReverseProxyService(
reverseProxyRepo,
k8sService,
operatorResourceSync
);
export const wsService = webSocketService;
export { k8sService, operatorResourceSync };
@@ -13,4 +13,5 @@ export interface IReverseProxyService {
setEnvVariable(proxyId: string, key: string, value: string): Promise<void>;
getEnvVariables(proxyId: string): Promise<EnvVariable[]>;
deleteEnvVariable(proxyId: string, key: string): Promise<void>;
getConnectionInfo(proxyId: string): Promise<unknown>;
}
@@ -9,7 +9,11 @@ import type {
ReverseProxyRepository,
ReverseProxyUpdateInput,
} from "../../domain/repositories/reverse-proxy.repository";
import type { OperatorResourceSync } from "../../services/operator-resource-sync";
import type { K8sService } from "../../services/k8s";
import {
type OperatorResourceSync,
operatorResourceName,
} from "../../services/operator-resource-sync";
import type { IReverseProxyService } from "../interfaces/reverse-proxy.service.interface";
import { BaseCrudService } from "./base-crud.service";
@@ -29,6 +33,7 @@ export class ReverseProxyService
{
constructor(
reverseProxyRepo: ReverseProxyRepository,
private k8sService: K8sService,
private operatorResourceSync: OperatorResourceSync
) {
super(
@@ -79,4 +84,10 @@ export class ReverseProxyService
await super.deleteEnvVariable(proxyId, key);
await this.operatorResourceSync.syncReverseProxyById(proxyId);
}
async getConnectionInfo(proxyId: string) {
const proxy = await this.getReverseProxyById(proxyId);
const serviceName = `${String(proxy.type).toLowerCase()}-${operatorResourceName(proxyId)}`;
return this.k8sService.getServerConnectionInfo(serviceName);
}
}
+8 -1
View File
@@ -10,6 +10,7 @@ import { authPlugin } from "./middleware/auth-plugin";
import { errorHandler } from "./middleware/error-handler";
import { bootstrapRoutes } from "./routes/bootstrap";
import { k8sRoutes } from "./routes/k8s";
import { pluginRoutes } from "./routes/plugin";
import { reverseProxyRoutes } from "./routes/reverse-proxy";
import { serverRoutes } from "./routes/servers";
import { terminalRoutes } from "./routes/terminal";
@@ -34,7 +35,13 @@ const app = new Elysia({ adapter: node() })
.use(bootstrapRoutes)
.use(authPlugin)
.group("/api", (app) =>
app.use(userRoutes).use(serverRoutes).use(reverseProxyRoutes).use(k8sRoutes).use(terminalRoutes)
app
.use(userRoutes)
.use(serverRoutes)
.use(reverseProxyRoutes)
.use(pluginRoutes)
.use(k8sRoutes)
.use(terminalRoutes)
);
export type App = typeof app;
@@ -12,18 +12,18 @@ eventBus.subscribe(ReverseProxyCreatedEvent, async (event) => {
{ proxyId: event.proxyId, proxyType: event.proxyType },
"Reverse proxy created event"
);
wsService.broadcast("create", event.proxyType, event.proxyId);
wsService.broadcast("CREATE", event.proxyType, event.proxyId);
await operatorResourceSync.syncReverseProxyById(event.proxyId);
});
eventBus.subscribe(ReverseProxyUpdatedEvent, async (event) => {
logger.info({ proxyId: event.proxyId }, "Reverse proxy updated event");
wsService.broadcast("update", "reverse-proxy", event.proxyId);
wsService.broadcast("UPDATE", "reverse-proxy", event.proxyId);
await operatorResourceSync.syncReverseProxyById(event.proxyId);
});
eventBus.subscribe(ReverseProxyDeletedEvent, async (event) => {
logger.info({ proxyId: event.proxyId }, "Reverse proxy deleted event");
wsService.broadcast("delete", "reverse-proxy", event.proxyId);
wsService.broadcast("DELETE", "reverse-proxy", event.proxyId);
await operatorResourceSync.deleteReverseProxy(event.proxyId);
});
@@ -9,18 +9,18 @@ import { logger } from "../logger";
eventBus.subscribe(ServerCreatedEvent, async (event) => {
logger.info({ serverId: event.serverId, serverType: event.serverType }, "Server created event");
wsService.broadcast("create", event.serverType, event.serverId);
wsService.broadcast("CREATE", event.serverType, event.serverId);
await operatorResourceSync.syncServerById(event.serverId);
});
eventBus.subscribe(ServerUpdatedEvent, async (event) => {
logger.info({ serverId: event.serverId }, "Server updated event");
wsService.broadcast("update", "server", event.serverId);
wsService.broadcast("UPDATE", "server", event.serverId);
await operatorResourceSync.syncServerById(event.serverId);
});
eventBus.subscribe(ServerDeletedEvent, async (event) => {
logger.info({ serverId: event.serverId }, "Server deleted event");
wsService.broadcast("delete", "server", event.serverId);
wsService.broadcast("DELETE", "server", event.serverId);
await operatorResourceSync.deleteServer(event.serverId);
});
@@ -64,12 +64,12 @@ export class PrismaReverseProxyRepository implements ReverseProxyRepository {
description: input.description ?? null,
external_address: input.external_address,
external_port: input.external_port,
listen_port: input.listen_port ?? 25577,
listen_port: input.listen_port ?? 25565,
service_type: input.service_type ?? "LOAD_BALANCER",
node_port: input.node_port ?? null,
memory: input.memory ?? 512,
cpu_request: input.cpu_request ?? "100m",
cpu_limit: input.cpu_limit ?? "200m",
cpu_request: input.cpu_request ?? "250m",
cpu_limit: input.cpu_limit ?? "500m",
api_key: token,
env_variables: input.env_variables
? {
@@ -67,12 +67,12 @@ export class PrismaServerRepository implements ServerRepository {
node_port: input.node_port ?? null,
memory: input.memory ?? 2048,
memory_request: input.memory_request ?? 1024,
cpu_request: input.cpu_request ?? "250m",
cpu_limit: input.cpu_limit ?? "500m",
jar_type: input.jar_type ?? "PAPER",
cpu_request: input.cpu_request ?? "500m",
cpu_limit: input.cpu_limit ?? "2",
jar_type: input.jar_type ?? "VANILLA",
minecraft_version: input.minecraft_version ?? "LATEST",
jvm_opts: input.jvm_opts ?? null,
use_aikar_flags: input.use_aikar_flags ?? true,
use_aikar_flags: input.use_aikar_flags ?? false,
use_meowice_flags: input.use_meowice_flags ?? false,
difficulty: input.difficulty ?? "EASY",
game_mode: input.game_mode ?? "SURVIVAL",
+19
View File
@@ -0,0 +1,19 @@
import { prisma } from "@minikura/db";
export async function findApiKeyOwner(apiKey: string) {
if (!apiKey) return null;
const [server, proxy] = await Promise.all([
prisma.server.findUnique({ where: { api_key: apiKey }, select: { id: true } }),
prisma.reverseProxyServer.findUnique({ where: { api_key: apiKey }, select: { id: true } }),
]);
if (server) return { kind: "server" as const, id: server.id };
if (proxy) return { kind: "reverse-proxy" as const, id: proxy.id };
return null;
}
export function bearerToken(header: string | null): string {
if (!header?.startsWith("Bearer ")) return "";
return header.slice(7).trim();
}
+8
View File
@@ -0,0 +1,8 @@
import { Elysia } from "elysia";
import { reverseProxyService, serverService } from "../application/di-container";
import { requirePluginApiKey } from "../middleware/auth-guards";
export const pluginRoutes = new Elysia({ prefix: "/plugin" })
.use(requirePluginApiKey)
.get("/servers", async () => serverService.getAllServers(true))
.get("/reverse-proxy", async () => reverseProxyService.getAllReverseProxies(true));
+9 -7
View File
@@ -1,13 +1,11 @@
import { Elysia } from "elysia";
import { z } from "zod";
import { reverseProxyService } from "../application/di-container";
import { requireAuth } from "../middleware/auth-guards";
import { createReverseProxySchema, updateReverseProxySchema } from "../schemas/server.schema";
const envVariableSchema = z.object({
key: z.string(),
value: z.string(),
});
import {
createReverseProxySchema,
envVariableSchema,
updateReverseProxySchema,
} from "../schemas/server.schema";
export const reverseProxyRoutes = new Elysia({ prefix: "/reverse-proxy" })
.use(requireAuth)
@@ -19,6 +17,10 @@ export const reverseProxyRoutes = new Elysia({ prefix: "/reverse-proxy" })
return await reverseProxyService.getReverseProxyById(params.id, false);
})
.get("/:id/connection-info", async ({ params }) => {
return await reverseProxyService.getConnectionInfo(params.id);
})
.post("/", async ({ body }) => {
const payload = createReverseProxySchema.parse(body);
const proxy = await reverseProxyService.createReverseProxy(payload);
+11 -9
View File
@@ -1,19 +1,21 @@
import { Elysia } from "elysia";
import { z } from "zod";
import { serverService, wsService } from "../application/di-container";
import { findApiKeyOwner } from "../middleware/api-key";
import { requireAuth } from "../middleware/auth-guards";
import { createServerSchema, updateServerSchema } from "../schemas/server.schema";
import {
createServerSchema,
envVariableSchema,
updateServerSchema,
} from "../schemas/server.schema";
import type { WebSocketClient } from "../services/websocket";
const envVariableSchema = z.object({
key: z.string(),
value: z.string(),
});
export const serverRoutes = new Elysia({ prefix: "/servers" })
.ws("/ws", {
open(ws: WebSocketClient & { data?: { query?: Record<string, string> }; close: () => void }) {
if (!ws.data?.query?.apiKey) {
async open(
ws: WebSocketClient & { data?: { query?: Record<string, string> }; close: () => void }
) {
const owner = await findApiKeyOwner(ws.data?.query?.apiKey ?? "");
if (!owner) {
ws.close();
return;
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { getErrorMessage } from "@minikura/shared/errors";
import { Elysia } from "elysia";
import { k8sService } from "../application/di-container";
import { logger } from "../infrastructure/logger";
import { requireAuth } from "../middleware/auth-guards";
type TerminalWsData = {
query?: Record<string, string>;
@@ -25,7 +26,7 @@ type BunTlsOptions = {
ca?: string;
};
export const terminalRoutes = new Elysia({ prefix: "/terminal" }).ws("/exec", {
export const terminalRoutes = new Elysia({ prefix: "/terminal" }).use(requireAuth).ws("/exec", {
open: async (ws: TerminalWs) => {
const podName = ws.data.query?.podName;
const container = ws.data.query?.container;
+13 -15
View File
@@ -1,29 +1,27 @@
import type { UpdateUserInput } from "@minikura/db";
import { Elysia } from "elysia";
import { userService } from "../application/di-container";
import { requireAdmin, requireAuth } from "../middleware/auth-guards";
import { requireAdmin } from "../middleware/auth-guards";
import { updateSuspensionSchema, updateUserSchema } from "../schemas/user.schema";
export const userRoutes = new Elysia({ prefix: "/users" })
.use(requireAdmin)
.get("/", async () => {
const users = await userService.getAllUsers();
return users;
return await userService.getAllUsers();
})
.use(requireAuth)
.get("/:id", async ({ params }) => {
const foundUser = await userService.getUserById(params.id);
return foundUser;
return await userService.getUserById(params.id);
})
.use(requireAdmin)
.patch("/:id", async ({ params, body }) => {
const input = body as UpdateUserInput;
const updatedUser = await userService.updateUser(params.id, input);
return updatedUser;
const input = updateUserSchema.parse(body);
return await userService.updateUser(params.id, input);
})
.patch("/:id/suspension", async ({ params, body }) => {
const payload = updateSuspensionSchema.parse(body);
return await userService.updateSuspension(params.id, {
isSuspended: payload.isSuspended,
suspendedUntil: payload.suspendedUntil ? new Date(payload.suspendedUntil) : null,
});
})
.use(requireAuth)
.delete("/:id", async ({ params, user }) => {
await userService.deleteUser(user.id, params.id);
return { success: true };
+16 -1
View File
@@ -1,6 +1,6 @@
"use client";
import type { ConnectionInfo, K8sNodeSummary, PodInfo } from "@minikura/api";
import type { ConnectionInfo, CustomResourceSummary, K8sNodeSummary, PodInfo } from "@minikura/api";
import { useCallback, useEffect, useState } from "react";
import { api } from "@/lib/api-client";
import { getReverseProxyApi } from "@/lib/api-helpers";
@@ -119,6 +119,20 @@ export function useTopologyData() {
nodeMetrics = nodeMetricsRes.data || { items: [] };
} catch (_err) {}
const proxyBackends = new Map<string, string[]>();
try {
const crResponse = await api.api.k8s["reverse-proxy-servers"].get();
for (const cr of (crResponse.data as CustomResourceSummary[]) || []) {
const backends = cr.status?.backends;
if (cr.name && Array.isArray(backends)) {
proxyBackends.set(
cr.name,
backends.filter((id): id is string => typeof id === "string")
);
}
}
} catch (_err) {}
const topologyGraph = buildTopologyGraph({
servers: normalServers,
proxies: reverseProxies,
@@ -127,6 +141,7 @@ export function useTopologyData() {
k8sNodes,
serverConnections: serverConnectionMap,
proxyConnections: proxyConnectionMap,
proxyBackends,
podMetrics,
nodeMetrics,
});
+12 -4
View File
@@ -59,15 +59,22 @@ interface BuildEnhancedGraphInput {
k8sNodes: K8sNodeSummary[];
serverConnections?: Map<string, ConnectionInfo | null>;
proxyConnections?: Map<string, ConnectionInfo | null>;
proxyBackends?: Map<string, string[]>;
podMetrics?: any;
nodeMetrics?: any;
}
function parseProxyServerConnections(
_proxy: ReverseProxyServer,
allServers: NormalServer[]
proxy: ReverseProxyServer,
allServers: NormalServer[],
backendsByProxyId?: Map<string, string[]>
): string[] {
return allServers.map((s) => s.id);
const backends = backendsByProxyId?.get(proxy.id);
if (!backends) {
return allServers.map((s) => s.id);
}
const known = new Set(allServers.map((s) => s.id));
return backends.filter((id) => known.has(id));
}
export function buildTopologyGraph(input: BuildEnhancedGraphInput): TopologyGraph {
@@ -79,6 +86,7 @@ export function buildTopologyGraph(input: BuildEnhancedGraphInput): TopologyGrap
k8sNodes,
serverConnections,
proxyConnections,
proxyBackends,
podMetrics,
nodeMetrics,
} = input;
@@ -103,7 +111,7 @@ export function buildTopologyGraph(input: BuildEnhancedGraphInput): TopologyGrap
}
for (const proxy of proxies) {
const connectedServerIds = parseProxyServerConnections(proxy, servers);
const connectedServerIds = parseProxyServerConnections(proxy, servers, proxyBackends);
proxyToServers.set(proxy.id, connectedServerIds);
for (const serverId of connectedServerIds) {
+1
View File
@@ -23,6 +23,7 @@
"@hookform/resolvers": "^5.7.1",
"@minikura/api": "workspace:*",
"@minikura/backend": "workspace:*",
"@minikura/shared": "workspace:*",
"@radix-ui/react-accordion": "^1.2.20",
"@radix-ui/react-avatar": "^1.2.6",
"@radix-ui/react-checkbox": "^1.3.11",
+6
View File
@@ -49,6 +49,7 @@
"@hookform/resolvers": "^5.7.1",
"@minikura/api": "workspace:*",
"@minikura/backend": "workspace:*",
"@minikura/shared": "workspace:*",
"@radix-ui/react-accordion": "^1.2.20",
"@radix-ui/react-avatar": "^1.2.6",
"@radix-ui/react-checkbox": "^1.3.11",
@@ -97,6 +98,9 @@
"typescript": "^7.0.2",
},
},
"operator": {
"name": "@minikura/operator",
},
"packages/api": {
"name": "@minikura/api",
"dependencies": {
@@ -333,6 +337,8 @@
"@minikura/db": ["@minikura/db@workspace:packages/db"],
"@minikura/operator": ["@minikura/operator@workspace:operator"],
"@minikura/shared": ["@minikura/shared@workspace:packages/shared"],
"@minikura/web": ["@minikura/web@workspace:apps/web"],
@@ -42,7 +42,7 @@ class Main @Inject constructor(private val logger: Logger, private val server: P
private val client = OkHttpClient()
private val apiKey: String = System.getenv("MINIKURA_API_KEY") ?: ""
private val apiUrl: String = System.getenv("MINIKURA_API_URL") ?: "http://localhost:3000/api"
private val websocketUrl: String = System.getenv("MINIKURA_WEBSOCKET_URL") ?: "ws://localhost:3000/ws?apiKey=$apiKey"
private val websocketUrl: String = System.getenv("MINIKURA_WEBSOCKET_URL") ?: "ws://localhost:3000/api/servers/ws?apiKey=$apiKey"
private var acceptingTransfers = AtomicBoolean(false)
private val redisBungeeApi = RedisBungeeAPI.getRedisBungeeApi()
@@ -158,7 +158,7 @@ class Main @Inject constructor(private val logger: Logger, private val server: P
private fun fetchReverseProxyServers() {
val request = Request.Builder()
.url("$apiUrl/reverse_proxy_servers")
.url("$apiUrl/plugin/reverse-proxy")
.header("Authorization", "Bearer $apiKey")
.build()
@@ -185,7 +185,7 @@ class Main @Inject constructor(private val logger: Logger, private val server: P
private fun fetchServers() {
server.allServers.forEach { server.unregisterServer(it.serverInfo) }
val request = Request.Builder()
.url("$apiUrl/servers")
.url("$apiUrl/plugin/servers")
.header("Authorization", "Bearer $apiKey")
.build()
@@ -30,14 +30,7 @@ class MinikuraWebSocketClient(private val plugin: Main, private val logger: Logg
"test" -> {
logger.info("API Call detected: endpoint=$endpoint, timestamp=$timestamp")
when (endpoint) {
"/servers" -> {
logger.info("dawdawdawdawd")
}
else -> {
logger.info("API endpoint $endpoint was accessed")
}
}
logger.info("API endpoint $endpoint was accessed")
}
"SERVER_CHANGE" -> {
val action = jsonObject.get("action")?.asString
@@ -46,7 +39,7 @@ class MinikuraWebSocketClient(private val plugin: Main, private val logger: Logg
logger.info("Server change detected: action=$action, serverType=$serverType, serverId=$serverId")
when (action) {
when (action?.uppercase()) {
"CREATE" -> {
logger.info("Server '$serverId' was created")
executeRefreshCommand()