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 cd0d663..df22dcc 100644 Binary files a/bun.lockb and b/bun.lockb differ 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"]