From 13068b60668384501f753b4a7a303a595bdd8b1f Mon Sep 17 00:00:00 2001 From: Yuzu Date: Mon, 12 May 2025 12:37:35 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20adds=20server=20management?= =?UTF-8?q?=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/index.ts | 811 +++++++++++++++++----------- apps/backend/src/services/server.ts | 277 +++++++--- bun.lockb | Bin 45863 -> 105152 bytes package.json | 6 +- packages/db/prisma/schema.prisma | 67 ++- turbo.json | 2 +- 6 files changed, 753 insertions(+), 410 deletions(-) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 139d98c..16ba27c 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -37,193 +37,237 @@ const bootstrap = async () => { }; const app = new Elysia() - .use(swagger()) - .ws("/ws", { - body: t.Object({ - message: t.String(), - }), - // query: t.Object({ - // id: t.String(), - // }), - message(ws, { message }) { - const { id } = ws.data.query; - ws.send({ - id, - message, - time: Date.now(), - }); - }, - }) - .post( - "/login", - async ({ body, cookie: { session_token } }) => { - const user = await UserService.getUserByUsername(body.username); - - if (!user) { - // Fake hash to prevent timing attacks - const FAKE_HASH = - "$argon2id$v=19$m=65536,t=3,p=4$PMqZArEBmfkHPbPv7Z50KQ$miV6tIIXU6w2WWOGLbWrdan8TGMTUsS1A1hy9RrpS9Y"; - await argon2.verify(FAKE_HASH, "fake"); - - return error("Unauthorized", { - success: false, - message: ReturnError.INVALID_USERNAME_OR_PASSWORD, - }); + .use(swagger({ + path: '/swagger', + documentation: { + info: { + title: 'Minikura API Documentation', + version: '1.0.0' } - - const valid = await argon2.verify(user.password, body.password); - if (!valid) { - return error("Unauthorized", { - success: false, - message: ReturnError.INVALID_USERNAME_OR_PASSWORD, - }); - } - - const session = await SessionService.create(user.id); - - session_token.httpOnly = true; - session_token.value = session.token; - - return { - success: true, - }; - }, - { - body: t.Object({ - username: t.String({ minLength: 1 }), - password: t.String({ minLength: 1 }), - }), } - ) - .derive(async ({ headers, cookie: { session_token } }) => { - const auth = session_token.value; - const token = headers.authorization?.split(" ")[1]; - - if (!auth && !token) - return error("Unauthorized", { - success: false, - message: ReturnError.MISSING_TOKEN, - }); - - if (auth) { - const session = await SessionService.validate(auth); - if (session.status === SessionService.SESSION_STATUS.REVOKED) { - return error("Unauthorized", { - success: false, - message: ReturnError.REVOKED_TOKEN, - }); - } - if (session.status === SessionService.SESSION_STATUS.EXPIRED) { - return error("Unauthorized", { - success: false, - message: ReturnError.EXPIRED_TOKEN, - }); - } - if ( - session.status === SessionService.SESSION_STATUS.INVALID || - session.status !== SessionService.SESSION_STATUS.VALID || - !session.session - ) { - return error("Unauthorized", { - success: false, - message: ReturnError.INVALID_TOKEN, - }); - } - - return { - server: null, - session: session.session, - }; - } - - if (token) { - const session = await SessionService.validateApiKey(token); - if (session.status === SessionService.SESSION_STATUS.INVALID) { - return error("Unauthorized", { - success: false, - message: ReturnError.INVALID_TOKEN, - }); - } - if ( - session.status === SessionService.SESSION_STATUS.VALID && - session.server - ) { + })) + .group('/api', app => app + .derive(async ({ headers, cookie: { session_token }, path }) => { + // Skip token validation for login route + if (path === '/api/login') { return { + server: null, session: null, - server: session.server, }; } - } - // Should never reach here - return error("Unauthorized", { - success: false, - message: ReturnError.INVALID_TOKEN, - }); - }) - .post("/logout", async ({ session, cookie: { session_token } }) => { - if (!session) return { success: true }; + const auth = session_token.value; + const token = headers.authorization?.split(" ")[1]; - await SessionService.revoke(session.token); - - session_token.remove(); - - return { - success: true, - }; - }) - .get("/servers", async ({ session }) => { - return await ServerService.getAllServers(!session); - }) - .get("/servers/:id", async ({ session, params: { id } }) => { - return await ServerService.getServerById(id, !session); - }) - .post( - "/servers", - async ({ body, error }) => { - // Must be a-z, A-Z, 0-9, and -_ only - if (!/^[a-zA-Z0-9-_]+$/.test(body.name)) { - return error("Bad Request", "Name must be a-z, A-Z, 0-9, and -_ only"); - } - - const _server = await ServerService.getServerByName(body.name); - if (_server) { - return error("Conflict", { + if (!auth && !token) + return error("Unauthorized", { success: false, - message: ReturnError.SERVER_NAME_IN_USE, + message: ReturnError.MISSING_TOKEN, }); + + if (auth) { + const session = await SessionService.validate(auth); + if (session.status === SessionService.SESSION_STATUS.REVOKED) { + return error("Unauthorized", { + success: false, + message: ReturnError.REVOKED_TOKEN, + }); + } + if (session.status === SessionService.SESSION_STATUS.EXPIRED) { + return error("Unauthorized", { + success: false, + message: ReturnError.EXPIRED_TOKEN, + }); + } + if ( + session.status === SessionService.SESSION_STATUS.INVALID || + session.status !== SessionService.SESSION_STATUS.VALID || + !session.session + ) { + return error("Unauthorized", { + success: false, + message: ReturnError.INVALID_TOKEN, + }); + } + + return { + server: null, + session: session.session, + }; } - const server = await ServerService.createServer({ - name: body.name, - description: body.description, - address: body.address, - port: body.port, - type: body.type, - join_priority: body.join_priority, + if (token) { + const session = await SessionService.validateApiKey(token); + if (session.status === SessionService.SESSION_STATUS.INVALID) { + return error("Unauthorized", { + success: false, + message: ReturnError.INVALID_TOKEN, + }); + } + if ( + session.status === SessionService.SESSION_STATUS.VALID && + session.server + ) { + return { + session: null, + server: session.server, + }; + } + } + + // Should never reach here + return error("Unauthorized", { + success: false, + message: ReturnError.INVALID_TOKEN, }); + }) + .post( + "/login", + async ({ body, cookie: { session_token } }) => { + const user = await UserService.getUserByUsername(body.username); + const valid = await argon2.verify(user?.password || "fake", body.password); + + if (!user || !valid) { + return error("Unauthorized", { + success: false, + message: ReturnError.INVALID_USERNAME_OR_PASSWORD, + }); + } + + const session = await SessionService.create(user.id); + + session_token.httpOnly = true; + session_token.value = session.token; + + return { + success: true, + }; + }, + { + body: t.Object({ + username: t.String({ minLength: 1 }), + password: t.String({ minLength: 1 }), + }), + } + ) + .ws("/ws", { + body: t.Object({ + message: t.String(), + }), + message(ws, { message }) { + const { id } = ws.data.query; + ws.send({ + id, + message, + time: Date.now(), + }); + }, + }) + .post("/logout", async ({ session, cookie: { session_token } }) => { + if (!session) return { success: true }; + + await SessionService.revoke(session.token); + + session_token.remove(); return { success: true, - data: { - server, - }, }; - }, - { - body: t.Object({ - name: t.String({ minLength: 1 }), - description: t.Nullable(t.String({ minLength: 1 })), - address: t.String({ minLength: 1 }), - port: t.Integer({ minimum: 1, maximum: 65535 }), - type: t.Enum(ServerType), - join_priority: t.Nullable(t.Integer({ minimum: 0 })), - }), - } - ) - .patch( - "/servers/:id", - async ({ session, params: { id }, body }) => { + }) + .get("/servers", async ({ session }) => { + return await ServerService.getAllServers(!session); + }) + .get("/servers/:id", async ({ session, params: { id } }) => { + return await ServerService.getServerById(id, !session); + }) + .post( + "/servers", + async ({ body, error }) => { + // Must be a-z, A-Z, 0-9, and -_ only + if (!/^[a-zA-Z0-9-_]+$/.test(body.id)) { + return error("Bad Request", "ID must be a-z, A-Z, 0-9, and -_ only"); + } + + const _server = await ServerService.getServerById(body.id); + if (_server) { + return error("Conflict", { + success: false, + message: ReturnError.SERVER_NAME_IN_USE, + }); + } + + const server = await ServerService.createServer({ + id: body.id, + description: body.description, + listen_port: body.listen_port, + type: body.type, + env_variables: body.env_variables, + memory: body.memory, + }); + + return { + success: true, + data: { + server, + }, + }; + }, + { + body: t.Object({ + id: t.String({ minLength: 1 }), + description: t.Nullable(t.String({ minLength: 1 })), + listen_port: t.Integer({ minimum: 1, maximum: 65535 }), + type: t.Enum(ServerType), + env_variables: t.Optional(t.Array(t.Object({ + key: t.String({ minLength: 1 }), + value: t.String(), + }))), + memory: t.Optional(t.String({ minLength: 1 })), + }), + } + ) + .patch( + "/servers/:id", + async ({ session, params: { id }, body }) => { + const server = await ServerService.getServerById(id); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } + + // Create update data with only fields that exist in the model + const data: any = {}; + + if (body.description !== undefined) data.description = body.description; + if (body.listen_port !== undefined) data.listen_port = body.listen_port; + if (body.memory !== undefined) data.memory = body.memory; + // Don't allow service_type to be updated through API + + await prisma.server.update({ + where: { id }, + data, + }); + + const newServer = await ServerService.getServerById(id, !session); + + return { + success: true, + data: { + server: newServer, + }, + }; + }, + { + body: t.Object({ + description: t.Optional(t.Nullable(t.String({ minLength: 1 }))), + listen_port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })), + memory: t.Optional(t.String({ minLength: 1 })), + }), + } + ) + .delete("/servers/:id", async ({ params: { id } }) => { const server = await ServerService.getServerById(id); if (!server) { return error("Not Found", { @@ -232,112 +276,120 @@ const app = new Elysia() }); } - if (body.name) { - const _server = await ServerService.getServerByName(body.name); - if (_server && _server.id !== id) { + await prisma.server.delete({ + where: { id }, + }); + + return { + success: true, + }; + }) + .get("/reverse_proxy_servers", async ({ session }) => { + return await ServerService.getAllReverseProxyServers(!session); + }) + .post( + "/reverse_proxy_servers", + async ({ body, error }) => { + // Must be a-z, A-Z, 0-9, and -_ only + if (!/^[a-zA-Z0-9-_]+$/.test(body.id)) { + return error("Bad Request", "ID must be a-z, A-Z, 0-9, and -_ only"); + } + + const _server = await ServerService.getReverseProxyServerById(body.id); + if (_server) { return error("Conflict", { success: false, message: ReturnError.SERVER_NAME_IN_USE, }); } - } - const data = { - name: body.name, - description: body.description, - address: body.address, - port: body.port, - join_priority: body.join_priority, - }; - - await prisma.server.update({ - where: { id }, - data, - }); - - const newServer = await ServerService.getServerById(id, !session); - - return { - success: true, - data: { - server: newServer, - }, - }; - }, - { - body: t.Object({ - name: t.Optional(t.String({ minLength: 1 })), - description: t.Optional(t.Nullable(t.String({ minLength: 1 }))), - address: t.Optional(t.String({ minLength: 1 })), - port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })), - join_priority: t.Optional(t.Integer({ minimum: 0 })), - }), - } - ) - .delete("/servers/:id", async ({ params: { id } }) => { - const server = await ServerService.getServerById(id); - if (!server) { - return error("Not Found", { - success: false, - message: ReturnError.SERVER_NOT_FOUND, - }); - } - - await prisma.server.delete({ - where: { id }, - }); - - return { - success: true, - }; - }) - .get("/reverse_proxy_servers", async ({ session }) => { - return await ServerService.getAllReverseProxyServers(!session); - }) - .post( - "/reverse_proxy_servers", - async ({ body, error }) => { - // Must be a-z, A-Z, 0-9, and -_ only - if (!/^[a-zA-Z0-9-_]+$/.test(body.name)) { - return error("Bad Request", "Name must be a-z, A-Z, 0-9, and -_ only"); - } - - const _server = await ServerService.getReverseProxyServerByName( - body.name - ); - if (_server) { - return error("Conflict", { - success: false, - message: ReturnError.SERVER_NAME_IN_USE, + const server = await ServerService.createReverseProxyServer({ + id: body.id, + description: body.description, + external_address: body.external_address, + external_port: body.external_port, + listen_port: body.listen_port, + type: body.type, + env_variables: body.env_variables, + memory: body.memory, }); + + return { + success: true, + data: { + server, + }, + }; + }, + { + body: t.Object({ + id: t.String({ minLength: 1 }), + description: t.Nullable(t.String({ minLength: 1 })), + external_address: t.String({ minLength: 1 }), + external_port: t.Integer({ minimum: 1, maximum: 65535 }), + listen_port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })), + type: t.Optional(t.Enum({ VELOCITY: "VELOCITY", BUNGEECORD: "BUNGEECORD" })), + env_variables: t.Optional(t.Array(t.Object({ + key: t.String({ minLength: 1 }), + value: t.String(), + }))), + memory: t.Optional(t.String({ minLength: 1 })), + }), } + ) + .patch( + "/reverse_proxy_servers/:id", + async ({ session, params: { id }, body }) => { + const server = await prisma.reverseProxyServer.findUnique({ + where: { id }, + }); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } - const server = await ServerService.createReverseProxyServer({ - name: body.name, - address: body.address, - port: body.port, - description: body.description, - }); + // Create update data with only fields that exist in the model + const data: any = {}; + + if (body.description !== undefined) data.description = body.description; + if (body.external_address !== undefined) data.external_address = body.external_address; + if (body.external_port !== undefined) data.external_port = body.external_port; + if (body.listen_port !== undefined) data.listen_port = body.listen_port; + if (body.type !== undefined) data.type = body.type; + if (body.memory !== undefined) data.memory = body.memory; + // Don't allow service_type to be updated through API - return { - success: true, - data: { - server, - }, - }; - }, - { - body: t.Object({ - name: t.String({ minLength: 1 }), - description: t.Nullable(t.String({ minLength: 1 })), - address: t.String({ minLength: 1 }), - port: t.Integer({ minimum: 1, maximum: 65535 }), - }), - } - ) - .patch( - "/reverse_proxy_servers/:id", - async ({ session, params: { id }, body }) => { + await prisma.reverseProxyServer.update({ + where: { id }, + data, + }); + + const newServer = await ServerService.getReverseProxyServerById( + id, + !session + ); + + return { + success: true, + data: { + server: newServer, + }, + }; + }, + { + body: t.Object({ + description: t.Optional(t.Nullable(t.String({ minLength: 1 }))), + external_address: t.Optional(t.String({ minLength: 1 })), + external_port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })), + listen_port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })), + type: t.Optional(t.Enum({ VELOCITY: "VELOCITY", BUNGEECORD: "BUNGEECORD" })), + memory: t.Optional(t.String({ minLength: 1 })), + }), + } + ) + .delete("/reverse_proxy_servers/:id", async ({ params: { id } }) => { const server = await prisma.reverseProxyServer.findUnique({ where: { id }, }); @@ -348,68 +400,189 @@ const app = new Elysia() }); } - if (body.name) { - const _server = await ServerService.getServerByName(body.name); - if (_server && _server.id !== id) { - return error("Conflict", { - success: false, - message: ReturnError.SERVER_NAME_IN_USE, - }); - } - } - - const data = { - name: body.name, - description: body.description, - address: body.address, - port: body.port, - }; - - await prisma.reverseProxyServer.update({ + await prisma.reverseProxyServer.delete({ where: { id }, - data, }); - const newServer = await ServerService.getReverseProxyServerById( - id, - !session - ); - + return { + success: true, + }; + }) + .get("/servers/:id/env", async ({ params: { id } }) => { + const server = await ServerService.getServerById(id); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } + return { success: true, data: { - server: newServer, + env_variables: server.env_variables, }, }; - }, - { - body: t.Object({ - name: t.Optional(t.String({ minLength: 1 })), - description: t.Optional(t.Nullable(t.String({ minLength: 1 }))), - address: t.Optional(t.String({ minLength: 1 })), - port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })), - }), - } + }) + .post( + "/servers/:id/env", + async ({ params: { id }, body }) => { + const server = await ServerService.getServerById(id); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } + + const envVar = await prisma.customEnvironmentVariable.upsert({ + where: { + key_server_id: { + key: body.key, + server_id: id, + }, + }, + update: { + value: body.value, + }, + create: { + key: body.key, + value: body.value, + server_id: id, + }, + }); + + return { + success: true, + data: { + env_var: envVar, + }, + }; + }, + { + body: t.Object({ + key: t.String({ minLength: 1 }), + value: t.String(), + }), + } + ) + .delete("/servers/:id/env/:key", async ({ params: { id, key } }) => { + const server = await ServerService.getServerById(id); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } + + try { + await prisma.customEnvironmentVariable.delete({ + where: { + key_server_id: { + key, + server_id: id, + }, + }, + }); + + return { + success: true, + }; + } catch (err) { + return error("Not Found", { + success: false, + message: "Environment variable not found", + }); + } + }) + .get("/reverse_proxy_servers/:id/env", async ({ params: { id } }) => { + const server = await ServerService.getReverseProxyServerById(id); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } + + return { + success: true, + data: { + env_variables: server.env_variables, + }, + }; + }) + .post( + "/reverse_proxy_servers/:id/env", + async ({ params: { id }, body }) => { + const server = await ServerService.getReverseProxyServerById(id); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } + + const envVar = await prisma.customEnvironmentVariable.upsert({ + where: { + key_reverse_proxy_id: { + key: body.key, + reverse_proxy_id: id, + }, + }, + update: { + value: body.value, + }, + create: { + key: body.key, + value: body.value, + reverse_proxy_id: id, + }, + }); + + return { + success: true, + data: { + env_var: envVar, + }, + }; + }, + { + body: t.Object({ + key: t.String({ minLength: 1 }), + value: t.String(), + }), + } + ) + .delete("/reverse_proxy_servers/:id/env/:key", async ({ params: { id, key } }) => { + const server = await ServerService.getReverseProxyServerById(id); + if (!server) { + return error("Not Found", { + success: false, + message: ReturnError.SERVER_NOT_FOUND, + }); + } + + try { + await prisma.customEnvironmentVariable.delete({ + where: { + key_reverse_proxy_id: { + key, + reverse_proxy_id: id, + }, + }, + }); + + return { + success: true, + }; + } catch (err) { + return error("Not Found", { + success: false, + message: "Environment variable not found", + }); + } + }) ) - .delete("/reverse_proxy_servers/:id", async ({ params: { id } }) => { - const server = await prisma.reverseProxyServer.findUnique({ - where: { id }, - }); - if (!server) { - return error("Not Found", { - success: false, - message: ReturnError.SERVER_NOT_FOUND, - }); - } - - await prisma.reverseProxyServer.delete({ - where: { id }, - }); - - return { - success: true, - }; - }) .listen(3000, async () => { console.log("Server is running on port 3000"); bootstrap(); diff --git a/apps/backend/src/services/server.ts b/apps/backend/src/services/server.ts index 01447f7..5b7eaf7 100644 --- a/apps/backend/src/services/server.ts +++ b/apps/backend/src/services/server.ts @@ -4,73 +4,126 @@ import crypto from "node:crypto"; export namespace ServerService { export async function getAllServers(omitSensitive = false) { - return await prisma.server.findMany({ - omit: { - api_key: omitSensitive, - }, - }); + if (omitSensitive) { + return await prisma.server.findMany({ + select: { + id: true, + type: true, + description: true, + listen_port: true, + memory: true, + created_at: true, + updated_at: true, + env_variables: true, + }, + }); + } else { + return await prisma.server.findMany({ + include: { + env_variables: true, + }, + }); + } } export async function getAllReverseProxyServers(omitSensitive = false) { - return await prisma.reverseProxyServer.findMany({ - omit: { - api_key: omitSensitive, - }, - }); + if (omitSensitive) { + return await prisma.reverseProxyServer.findMany({ + select: { + id: true, + type: true, + description: true, + external_address: true, + external_port: true, + listen_port: true, + memory: true, + created_at: true, + updated_at: true, + env_variables: true, + }, + }); + } else { + return await prisma.reverseProxyServer.findMany({ + include: { + env_variables: true, + }, + }); + } } export async function getServerById(id: string, omitSensitive = false) { - return await prisma.server.findUnique({ - where: { id }, - omit: { - api_key: omitSensitive, - }, - }); + if (omitSensitive) { + return await prisma.server.findUnique({ + where: { id }, + select: { + id: true, + type: true, + description: true, + listen_port: true, + memory: true, + created_at: true, + updated_at: true, + env_variables: true, + }, + }); + } else { + return await prisma.server.findUnique({ + where: { id }, + include: { + env_variables: true, + }, + }); + } } export async function getReverseProxyServerById( id: string, omitSensitive = false ) { - return await prisma.reverseProxyServer.findUnique({ - where: { id }, - omit: { - api_key: omitSensitive, - }, - }); - } - - export async function getServerByName(name: string, omitSensitive = false) { - return await prisma.server.findUnique({ - where: { name }, - omit: { - api_key: omitSensitive, - }, - }); - } - - export async function getReverseProxyServerByName( - name: string, - omitSensitive = false - ) { - return await prisma.reverseProxyServer.findUnique({ - where: { name }, - omit: { - api_key: omitSensitive, - }, - }); + if (omitSensitive) { + return await prisma.reverseProxyServer.findUnique({ + where: { id }, + select: { + id: true, + type: true, + description: true, + external_address: true, + external_port: true, + listen_port: true, + memory: true, + created_at: true, + updated_at: true, + env_variables: true, + }, + }); + } else { + return await prisma.reverseProxyServer.findUnique({ + where: { id }, + include: { + env_variables: true, + }, + }); + } } export async function createReverseProxyServer({ - name, + id, description, - address, - port, + external_address, + external_port, + listen_port, + type, + env_variables, + memory, }: { - name: string; + id: string; description: string | null; - address: string; - port: number; + external_address: string; + external_port: number; + listen_port?: number; + type?: "VELOCITY" | "BUNGEECORD"; + env_variables?: { key: string; value: string }[]; + memory?: string; }) { let token = crypto.randomBytes(64).toString("hex"); token = token @@ -81,29 +134,41 @@ export namespace ServerService { return await prisma.reverseProxyServer.create({ data: { - name, + id, description, - address, - port, + external_address, + external_port, + listen_port: listen_port || 25565, + type: type || "VELOCITY", api_key: token, + memory: memory || "512M", + env_variables: env_variables ? { + create: env_variables.map(ev => ({ + key: ev.key, + value: ev.value + })) + } : undefined, }, + include: { + env_variables: true, + } }); } export async function createServer({ - name, + id, description, - address, - port, type, - join_priority, + listen_port, + env_variables, + memory, }: { - name: string; + id: string; description: string | null; - address: string; - port: number; type: ServerType; - join_priority: number | null; + listen_port: number; + env_variables?: { key: string; value: string }[]; + memory?: string; }) { let token = crypto.randomBytes(64).toString("hex"); token = token @@ -114,13 +179,97 @@ export namespace ServerService { return await prisma.server.create({ data: { - name, + id, description, - address, - port, type, + listen_port, api_key: token, - join_priority, + memory: memory || "1G", + env_variables: env_variables ? { + create: env_variables.map(ev => ({ + key: ev.key, + value: ev.value + })) + } : undefined, + }, + include: { + env_variables: true, + } + }); + } + + export async function setServerEnvironmentVariable( + serverId: string, + key: string, + value: string + ) { + // Upsert pattern - create if doesn't exist, update if it does + return await prisma.customEnvironmentVariable.upsert({ + where: { + key_server_id: { + key, + server_id: serverId, + }, + }, + update: { + value, + }, + create: { + key, + value, + server_id: serverId, + }, + }); + } + + export async function setReverseProxyEnvironmentVariable( + proxyId: string, + key: string, + value: string + ) { + // Upsert pattern - create if doesn't exist, update if it does + return await prisma.customEnvironmentVariable.upsert({ + where: { + key_reverse_proxy_id: { + key, + reverse_proxy_id: proxyId, + }, + }, + update: { + value, + }, + create: { + key, + value, + reverse_proxy_id: proxyId, + }, + }); + } + + export async function deleteServerEnvironmentVariable( + serverId: string, + key: string + ) { + return await prisma.customEnvironmentVariable.delete({ + where: { + key_server_id: { + key, + server_id: serverId, + }, + }, + }); + } + + export async function deleteReverseProxyEnvironmentVariable( + proxyId: string, + key: string + ) { + return await prisma.customEnvironmentVariable.delete({ + where: { + key_reverse_proxy_id: { + key, + reverse_proxy_id: proxyId, + }, }, }); } diff --git a/bun.lockb b/bun.lockb index cd0d663cadf949e321330521a9bb6f689895ca9d..df22dccd291c22b6020981cf7843c7a5b8d6589f 100644 GIT binary patch literal 105152 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 E0B!*Kod5s; delta 6483 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 P?Kn`$CZIC2XZ61TKnvQI diff --git a/package.json b/package.json index bcc2c2a..20ef79d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "db:generate": "bun --filter @minikura/db generate", "db:studio": "bun --filter @minikura/db studio", "db:push": "bun --filter @minikura/db push", - "db:reset": "bun --filter @minikura/db reset" + "db:reset": "bun --filter @minikura/db reset", + "k8s:dev": "bun --filter @minikura/k8s-operator dev", + "k8s:build": "bun --filter @minikura/k8s-operator build", + "k8s:start": "bun --filter @minikura/k8s-operator start", + "k8s:crd": "bun --filter @minikura/k8s-operator apply-crds" }, "devDependencies": { "@biomejs/biome": "^1.9.1", diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 64a747f..7a4e4e5 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -1,12 +1,5 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - generator client { - provider = "prisma-client-js" - previewFeatures = ["omitApi"] + provider = "prisma-client-js" } datasource db { @@ -19,28 +12,35 @@ enum ServerType { STATELESS } +enum ReverseProxyServerType { + VELOCITY + BUNGEECORD +} + model ReverseProxyServer { - id String @id @default(cuid()) - name String @unique - description String? - api_key String @unique - address String - port Int - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + id String @id @default(cuid()) + type ReverseProxyServerType + description String? + external_address String + external_port Int + listen_port Int @default(25565) + memory String @default("512M") + api_key String @unique + env_variables CustomEnvironmentVariable[] @relation("ReverseProxyServerEnvVars") + created_at DateTime @default(now()) + updated_at DateTime @updatedAt } model Server { - id String @id @default(cuid()) - name String @unique - description String? - address String - port Int + id String @id @default(cuid()) type ServerType - api_key String @unique - join_priority Int? - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + description String? + listen_port Int @default(25565) + memory String @default("1G") + env_variables CustomEnvironmentVariable[] @relation("ServerEnvVars") + api_key String @unique + created_at DateTime @default(now()) + updated_at DateTime @updatedAt } model User { @@ -62,3 +62,20 @@ model Session { created_at DateTime @default(now()) updated_at DateTime @updatedAt } + +model CustomEnvironmentVariable { + id String @id @default(cuid()) + key String + value String + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + server_id String? + server Server? @relation("ServerEnvVars", fields: [server_id], references: [id], onDelete: Cascade) + + reverse_proxy_id String? + reverse_proxy_server ReverseProxyServer? @relation("ReverseProxyServerEnvVars", fields: [reverse_proxy_id], references: [id], onDelete: Cascade) + + @@unique([key, server_id]) + @@unique([key, reverse_proxy_id]) +} diff --git a/turbo.json b/turbo.json index 6cfb29b..c7c3be0 100644 --- a/turbo.json +++ b/turbo.json @@ -3,7 +3,7 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**"] + "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "typecheck": { "dependsOn": ["^typecheck"]