diff --git a/apps/daemon/src/session/index.ts b/apps/daemon/src/session/index.ts index 451c451..b1051eb 100644 --- a/apps/daemon/src/session/index.ts +++ b/apps/daemon/src/session/index.ts @@ -6,16 +6,15 @@ import deleteSession from "./delete.js"; import { getFile, listFiles, sendFile } from "./file.js"; import manageSession from "./manage.js"; import screenshot from "./screenshot.js"; -// fill this export default new Elysia({ prefix: "/sessions" }) .put( - "/", + "/create", async ({ body }) => { const session = await createSession(body); return { success: true, id: session.Id, - created: session.Created, + created: Number.parseInt(session.Created), }; }, { diff --git a/apps/web/server.ts b/apps/web/server.ts index ef273bb..4faf5aa 100644 --- a/apps/web/server.ts +++ b/apps/web/server.ts @@ -1,6 +1,4 @@ import { createServer } from "node:http"; -import auth from "@stardust/common/auth"; -import { fromNodeHeaders } from "better-auth/node"; import { createProxyMiddleware } from "http-proxy-middleware"; import next from "next"; const dev = process.env.NODE_ENV !== "production"; diff --git a/apps/web/src/app/(main)/create-session-button.tsx b/apps/web/src/app/(main)/create-session-button.tsx index 3d349ef..3f439da 100644 --- a/apps/web/src/app/(main)/create-session-button.tsx +++ b/apps/web/src/app/(main)/create-session-button.tsx @@ -1,10 +1,10 @@ "use client"; import { Button } from "@/components/ui/button"; -// import { createSession } from "@/lib/session"; +import { createSession } from "@/lib/session/create"; import { useRouter } from "next/navigation"; import { useTransition } from "react"; import { toast } from "sonner"; -export function CreateSessionButton({ image }: { image: string }) { +export function CreateSessionButton({ workspace }: { workspace: string }) { const [isPending, startTransition] = useTransition(); const router = useRouter(); return ( @@ -12,12 +12,12 @@ export function CreateSessionButton({ image }: { image: string }) { disabled={isPending} onClick={() => startTransition(async () => { - // const session = await createSession(image).catch(() => { - // toast.error("Error creating session"); - // }); - // if (!session) return; - // router.push(`/view/${session[0].id}`); - console.log("w riz?"); + const session = await createSession(workspace).catch(() => { + toast.error("Error creating session"); + }); + if (!session) return; + // @ts-expect-error to be fixed later + router.push(`/view/${session[0].id}`); }) } > diff --git a/apps/web/src/app/(main)/page.tsx b/apps/web/src/app/(main)/page.tsx index 4a8b512..437ba51 100644 --- a/apps/web/src/app/(main)/page.tsx +++ b/apps/web/src/app/(main)/page.tsx @@ -53,7 +53,7 @@ export default async function Dashboard() { Close - + diff --git a/apps/web/src/app/(main)/sessions/page.tsx b/apps/web/src/app/(main)/sessions/page.tsx index a8f6700..8bafb63 100644 --- a/apps/web/src/app/(main)/sessions/page.tsx +++ b/apps/web/src/app/(main)/sessions/page.tsx @@ -4,8 +4,8 @@ import { AspectRatio } from "@/components/ui/aspect-ratio"; import { Button } from "@/components/ui/button"; import { CardTitle } from "@/components/ui/card"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { getNode } from "@/lib/sessions/client"; -import { inspectSession } from "@/lib/sessions/inspect"; +import { getNode } from "@/lib/session/client"; +import { inspectSession } from "@/lib/session/inspect"; import auth from "@stardust/common/auth"; import db, { type SelectSession } from "@stardust/db"; import { Container, Loader2, PauseCircle, PlayCircle, ScreenShare, Square, Trash2 } from "lucide-react"; diff --git a/apps/web/src/lib/sessions/client.ts b/apps/web/src/lib/session/client.ts similarity index 100% rename from apps/web/src/lib/sessions/client.ts rename to apps/web/src/lib/session/client.ts diff --git a/apps/web/src/lib/session/create.ts b/apps/web/src/lib/session/create.ts new file mode 100644 index 0000000..0f4bff9 --- /dev/null +++ b/apps/web/src/lib/session/create.ts @@ -0,0 +1,59 @@ +"use server"; + +import auth from "@stardust/common/auth"; +import { stardustConnector } from "@stardust/common/daemon/client"; +import { getConfig } from "@stardust/config"; +import db, { session } from "@stardust/db"; +import { headers } from "next/headers"; + +export async function createSession(workspace: string) { + console.log(`✨ Stardust: Creating session using workspace ${workspace}`); + const config = getConfig(); + const userSession = await auth.api.getSession({ headers: await headers() }); + if (!userSession?.user) throw new Error("User not found"); + // todo: move these limits to each node instead + if (config.session?.usageLimits) { + const { role, allSessions } = await db.transaction(async (tx) => ({ + role: ( + await tx.query.user.findFirst({ + where: (user, { eq }) => eq(user.id, userSession?.user?.id as string), + columns: { + role: true, + }, + }) + )?.role, + allSessions: await tx.select().from(session), + })); + if (config.session?.usageLimits.instance && config.session?.usageLimits.instance <= allSessions.length) + throw new Error("Instance limit exceeded"); + if (config.session.usageLimits.user && role !== "admin") { + const userSessions = allSessions.filter((v) => v.userId === userSession.user.id); + if (config.session?.usageLimits.user <= userSessions.length) throw new Error("User session limit exceeded"); + } + } + // todo: make this pick node with lowest resource usage + const sessionNode = config.nodes[0]; + const node = stardustConnector(`http://${sessionNode.hostname}:${sessionNode.port || 4000}`, sessionNode.token); + const { data: container, error } = await node.sessions.create.put({ + workspace, + user: userSession.user.id, + }); + if (error) throw error; + const expiry = new Date(); + expiry.setMinutes(expiry.getMinutes() + (config.session?.keepaliveDuration || 1440)); + return db + .insert(session) + .values({ + id: container.id, + dockerImage: workspace, + createdAt: new Date(container.created), + expiresAt: expiry, + userId: userSession.user.id, + node: sessionNode.id, + }) + .returning() + .catch(async (e) => { + await node.sessions({ ...container }).delete(); + throw e; + }); +} diff --git a/apps/web/src/lib/sessions/inspect.ts b/apps/web/src/lib/session/inspect.ts similarity index 100% rename from apps/web/src/lib/sessions/inspect.ts rename to apps/web/src/lib/session/inspect.ts diff --git a/packages/db/schema/index.ts b/packages/db/schema/index.ts index a086ec8..c14fee6 100644 --- a/packages/db/schema/index.ts +++ b/packages/db/schema/index.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm"; -import { bigint, pgTable, text } from "drizzle-orm/pg-core"; +import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { user } from "./auth"; export type SelectUser = typeof user.$inferSelect; @@ -20,8 +20,8 @@ export const session = pgTable("session", { id: text("id").primaryKey().notNull(), dockerImage: text("dockerImage").notNull(), node: text("node").notNull(), - createdAt: bigint("createdAt", { mode: "number" }).notNull(), - expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), + createdAt: timestamp("createdAt").notNull(), + expiresAt: timestamp("expiresAt").notNull(), userId: text("userId") .notNull() .references(() => user.id),