diff --git a/backend/database/src/index.ts b/backend/database/src/index.ts index 02f287f..b115fb5 100644 --- a/backend/database/src/index.ts +++ b/backend/database/src/index.ts @@ -9,7 +9,7 @@ import { and, eq, sql } from "drizzle-orm"; export interface Env { DB: D1Database; - RL: any; + STORAGE: any; } // https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1 @@ -86,11 +86,15 @@ export default { const sb = await db.insert(sandbox).values({ type, name, userId, visibility }).returning().get(); - await fetch("https://storage.ishaan1013.workers.dev/api/init", { + const initStorageRequest = new Request("https://storage.ishaan1013.workers.dev/api/init", { method: "POST", body: JSON.stringify({ sandboxId: sb.id, type }), headers: { "Content-Type": "application/json" }, }); + const initStorageRes = await env.STORAGE.fetch(initStorageRequest); + + const initStorage = await initStorageRes.text(); + console.log("initStorage: ", initStorage); return new Response(sb.id, { status: 200 }); } else { diff --git a/backend/orchestrator/src/index.ts b/backend/orchestrator/src/index.ts index 31ab42f..113a2d9 100644 --- a/backend/orchestrator/src/index.ts +++ b/backend/orchestrator/src/index.ts @@ -36,14 +36,12 @@ const readAndParseKubeYaml = ( const regex = new RegExp(``, "g") docString = docString.replace(regex, sandboxId) - // replace with process.env.CF_API_TOKEN if (!process.env.CF_API_TOKEN) { throw new Error("CF_API_TOKEN is not defined") } const regexEnv1 = new RegExp(``, "g") docString = docString.replace(regexEnv1, process.env.CF_API_TOKEN) - // replace with process.env.CF_USER_ID if (!process.env.CF_USER_ID) { throw new Error("CF_USER_ID is not defined") } @@ -55,12 +53,13 @@ const readAndParseKubeYaml = ( return docs } +const dataSchema = z.object({ + userId: z.string(), + sandboxId: z.string(), +}) + app.post("/start", async (req, res) => { - const initSchema = z.object({ - userId: z.string(), - sandboxId: z.string(), - }) - const { userId, sandboxId } = initSchema.parse(req.body) + const { userId, sandboxId } = dataSchema.parse(req.body) const namespace = "default" try { @@ -83,6 +82,39 @@ app.post("/start", async (req, res) => { } }) +app.post("/stop", async (req, res) => { + const { userId, sandboxId } = dataSchema.parse(req.body) + const namespace = "default" + + try { + const kubeManifests = readAndParseKubeYaml( + path.join(__dirname, "../service.yaml"), + sandboxId + ) + kubeManifests.forEach(async (manifest) => { + if (manifest.kind === "Deployment") + await appsV1Api.deleteNamespacedDeployment( + manifest.metadata?.name || "", + namespace + ) + else if (manifest.kind === "Service") + await coreV1Api.deleteNamespacedService( + manifest.metadata?.name || "", + namespace + ) + else if (manifest.kind === "Ingress") + await networkingV1Api.deleteNamespacedIngress( + manifest.metadata?.name || "", + namespace + ) + }) + res.status(200).send({ message: "Resources deleted." }) + } catch (error) { + console.log("Failed to delete resources", error) + res.status(500).send({ message: "Failed to delete resources." }) + } +}) + app.listen(port, () => { console.log(`Listening on port: ${port}`) }) diff --git a/backend/server/dockerfile b/backend/server/dockerfile index f023b93..83c975f 100644 --- a/backend/server/dockerfile +++ b/backend/server/dockerfile @@ -1,5 +1,5 @@ -ARG cf_api_token -ARG cf_user_id +ARG CF_API_TOKEN +ARG CF_USER_ID FROM node:20 @@ -15,7 +15,7 @@ RUN npm run build EXPOSE 3000 -ENV cf_api_token=$cf_api_token -ENV cf_user_id=$cf_user_id +ENV CF_API_TOKEN=$CF_API_TOKEN +ENV CF_USER_ID=$CF_USER_ID CMD [ "node", "dist/index.js" ] \ No newline at end of file diff --git a/backend/server/src/inactivity.ts b/backend/server/src/inactivity.ts new file mode 100644 index 0000000..1243ab7 --- /dev/null +++ b/backend/server/src/inactivity.ts @@ -0,0 +1,10 @@ +import { Server } from "socket.io"; +import { DefaultEventsMap } from "socket.io/dist/typed-events"; + +export function checkForInactivity(io: Server) { + io.fetchSockets().then(sockets => { + if (sockets.length === 0) { + console.log("No users have been connected for 15 seconds."); + } + }); +} \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 9f26e5e..b7a5b82 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -38,6 +38,8 @@ const io = new Server(httpServer, { }, }) +let inactivityTimeout: NodeJS.Timeout | null = null; + const terminals: { [id: string]: { terminal: IPty; onData: IDisposable; onExit: IDisposable } } = {} @@ -85,15 +87,20 @@ io.use(async (socket, next) => { socket.data = { userId, sandboxId: sandboxId, + isOwner: sandbox !== undefined, } next() }) io.on("connection", async (socket) => { + + if (inactivityTimeout) clearTimeout(inactivityTimeout); + const data = socket.data as { userId: string sandboxId: string + isOwner: boolean } const sandboxFiles = await getSandboxFiles(data.sandboxId) @@ -298,14 +305,37 @@ io.on("connection", async (socket) => { } ) - socket.on("disconnect", () => { - Object.entries(terminals).forEach((t) => { + socket.on("disconnect", async () => { + if (data.isOwner) { + Object.entries(terminals).forEach((t) => { const { terminal, onData, onExit } = t[1] if (os.platform() !== "win32") terminal.kill() - onData.dispose() - onExit.dispose() - delete terminals[t[0]] - }) + onData.dispose() + onExit.dispose() + delete terminals[t[0]] + }) + + console.log("The owner disconnected.") + socket.broadcast.emit("ownerDisconnected") + } + else { + console.log("A shared user disconnected.") + } + + const sockets = await io.fetchSockets() + if (inactivityTimeout) { + clearTimeout(inactivityTimeout) + }; + if (sockets.length === 0) { + inactivityTimeout = setTimeout(() => { + io.fetchSockets().then(sockets => { + if (sockets.length === 0) { + console.log("No users have been connected for 15 seconds."); + } + }); + }, 15000); + } + }) }) diff --git a/frontend/components/dashboard/newProject.tsx b/frontend/components/dashboard/newProject.tsx index d0b8cf7..9ab4e88 100644 --- a/frontend/components/dashboard/newProject.tsx +++ b/frontend/components/dashboard/newProject.tsx @@ -60,7 +60,8 @@ const data: { ] const formSchema = z.object({ - name: z.string().min(1).max(16), + name: z.string().min(1).max(16) + .refine((value) => /^[a-zA-Z0-9_]+$/.test(value), "Name must be alphanumeric and can contain underscores"), visibility: z.enum(["public", "private"]), }) diff --git a/frontend/components/ui/userButton.tsx b/frontend/components/ui/userButton.tsx index df0ebaa..39b1d3f 100644 --- a/frontend/components/ui/userButton.tsx +++ b/frontend/components/ui/userButton.tsx @@ -23,7 +23,7 @@ export default function UserButton({ userData }: { userData: User }) {
- {userData.name + {userData.name && userData.name .split(" ") .slice(0, 2) .map((name) => name[0].toUpperCase())} diff --git a/frontend/lib/actions.ts b/frontend/lib/actions.ts index ea0f573..7f25182 100644 --- a/frontend/lib/actions.ts +++ b/frontend/lib/actions.ts @@ -8,6 +8,7 @@ export async function createSandbox(body: { userId: string visibility: string }) { + console.log("creating. body:", body) const res = await fetch( "https://database.ishaan1013.workers.dev/api/sandbox", {