diff --git a/src/actions/auth.ts b/src/actions/auth.ts new file mode 100644 index 0000000..4db317c --- /dev/null +++ b/src/actions/auth.ts @@ -0,0 +1,69 @@ +"use server"; + +import bcrypt from "bcrypt"; +import prisma from "@/lib/prisma"; +import { signIn } from "@/lib/auth"; +import { authSchema } from "@/lib/zod"; +import { CredentialsSignInFormValues } from "@/components/credentials-sign-in-form"; +import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-form"; + +const saltRounds = 10; + +export async function signInWithCredentials(formData: CredentialsSignInFormValues) { + try { + // Parse credentials using authSchema for validation + const { email, password } = await authSchema.parseAsync(formData); + + // Find user by email + const user = await prisma.user.findUnique({ where: { email } }); + + // Check if the user exists + if (!user) { + throw new Error("User not found."); + } + + // Check if the user has a password + if (!user.password) { + throw new Error("Invalid credentials."); + } + + // Check if the password matches + const passwordMatch = await bcrypt.compare(password, user.password); + if (!passwordMatch) { + throw new Error("Incorrect password."); + } + + await signIn("credentials", { ...formData, redirect: false }); + return { success: true }; + } catch (error) { + return { error: error instanceof Error ? error.message : "Failed to sign in. Please try again." }; + } +} + +export async function signUpWithCredentials(formData: CredentialsSignUpFormValues) { + try { + const validatedData = await authSchema.parseAsync(formData); + + // Check if user already exists + const existingUser = await prisma.user.findUnique({ where: { email: validatedData.email } }); + if (existingUser) { + throw new Error("User already exists"); + } + + // Hash password and create user + const pwHash = await bcrypt.hash(validatedData.password, saltRounds); + const user = await prisma.user.create({ + data: { email: validatedData.email, password: pwHash }, + }); + + // Assign admin role if first user + const userCount = await prisma.user.count(); + if (userCount === 1) { + await prisma.user.update({ where: { id: user.id }, data: { role: "ADMIN" } }); + } + + return { success: true }; + } catch (error) { + return { error: error instanceof Error ? error.message : "Registration failed. Please try again." }; + } +} diff --git a/src/actions/judge.ts b/src/actions/judge.ts new file mode 100644 index 0000000..0960a92 --- /dev/null +++ b/src/actions/judge.ts @@ -0,0 +1,346 @@ +"use server"; + +import fs from "fs"; +import tar from "tar-stream"; +import Docker from "dockerode"; +import prisma from "@/lib/prisma"; +import { v4 as uuid } from "uuid"; +import { auth } from "@/lib/auth"; +import { redirect } from "next/navigation"; +import { Readable, Writable } from "stream"; +import { ExitCode, EditorLanguage, JudgeResult } from "@/generated/client"; + +const isRemote = process.env.DOCKER_HOST_MODE === "remote"; + +// Docker client initialization +const docker = isRemote + ? new Docker({ + protocol: process.env.DOCKER_REMOTE_PROTOCOL as "https" | "http" | "ssh" | undefined, + host: process.env.DOCKER_REMOTE_HOST, + port: process.env.DOCKER_REMOTE_PORT, + ca: fs.readFileSync(process.env.DOCKER_REMOTE_CA_PATH || "/certs/ca.pem"), + cert: fs.readFileSync(process.env.DOCKER_REMOTE_CERT_PATH || "/certs/cert.pem"), + key: fs.readFileSync(process.env.DOCKER_REMOTE_KEY_PATH || "/certs/key.pem"), + }) + : new Docker({ socketPath: "/var/run/docker.sock" }); + +// Prepare Docker image environment +async function prepareEnvironment(image: string, tag: string) { + const reference = `${image}:${tag}`; + const filters = { reference: [reference] }; + const images = await docker.listImages({ filters }); + if (images.length === 0) await docker.pull(reference); +} + +// Create Docker container with keep-alive +async function createContainer( + image: string, + tag: string, + workingDir: string, + memoryLimit?: number +) { + const container = await docker.createContainer({ + Image: `${image}:${tag}`, + Cmd: ["tail", "-f", "/dev/null"], // Keep container alive + WorkingDir: workingDir, + HostConfig: { + Memory: memoryLimit ? memoryLimit * 1024 * 1024 : undefined, + MemorySwap: memoryLimit ? memoryLimit * 1024 * 1024 : undefined, + }, + NetworkDisabled: true, + }); + + await container.start(); + return container; +} + +// Create tar stream for code submission +function createTarStream(file: string, value: string) { + const pack = tar.pack(); + pack.entry({ name: file }, value); + pack.finalize(); + return Readable.from(pack); +} + +export async function judge( + language: EditorLanguage, + value: string, +): Promise { + const session = await auth(); + if (!session) redirect("/sign-in"); + + let container: Docker.Container | null = null; + + try { + const config = await prisma.editorLanguageConfig.findUnique({ + where: { language }, + include: { + dockerConfig: true, + }, + }); + + if (!config || !config.dockerConfig) { + return { + id: uuid(), + output: "Configuration Error: Missing editor or docker configuration", + exitCode: ExitCode.SE, + executionTime: null, + memoryUsage: null, + }; + } + + const { + image, + tag, + workingDir, + memoryLimit, + timeLimit, + compileOutputLimit, + runOutputLimit, + } = config.dockerConfig; + const { fileName, fileExtension } = config; + const file = `${fileName}.${fileExtension}`; + + // Prepare the environment and create a container + await prepareEnvironment(image, tag); + container = await createContainer(image, tag, workingDir, memoryLimit); + + // Upload code to the container + const tarStream = createTarStream(file, value); + await container.putArchive(tarStream, { path: workingDir }); + + // Compile the code + const compileResult = await compile(container, file, fileName, compileOutputLimit); + if (compileResult.exitCode === ExitCode.CE) { + return compileResult; + } + + // Run the code + const runResult = await run(container, fileName, timeLimit, runOutputLimit); + return runResult; + } catch (error) { + console.error(error); + return { + id: uuid(), + output: "System Error", + exitCode: ExitCode.SE, + executionTime: null, + memoryUsage: null, + }; + } finally { + if (container) { + await container.kill(); + await container.remove(); + } + } +} + +async function compile( + container: Docker.Container, + file: string, + fileName: string, + maxOutput: number = 1 * 1024 * 1024 +): Promise { + const compileExec = await container.exec({ + Cmd: ["gcc", "-O2", file, "-o", fileName], + AttachStdout: true, + AttachStderr: true, + }); + + return new Promise((resolve, reject) => { + compileExec.start({}, (error, stream) => { + if (error || !stream) { + return reject({ output: "System Error", exitCode: ExitCode.SE }); + } + + const stdoutChunks: string[] = []; + let stdoutLength = 0; + const stdoutStream = new Writable({ + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stdoutLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stdoutLength); + stdoutChunks.push(text); + stdoutLength = maxOutput; + callback(); + return; + } + stdoutChunks.push(text); + stdoutLength += text.length; + callback(); + }, + }); + + const stderrChunks: string[] = []; + let stderrLength = 0; + const stderrStream = new Writable({ + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stderrLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stderrLength); + stderrChunks.push(text); + stderrLength = maxOutput; + callback(); + return; + } + stderrChunks.push(text); + stderrLength += text.length; + callback(); + }, + }); + + docker.modem.demuxStream(stream, stdoutStream, stderrStream); + + stream.on("end", async () => { + const stdout = stdoutChunks.join(""); + const stderr = stderrChunks.join(""); + const exitCode = (await compileExec.inspect()).ExitCode; + + let result: JudgeResult; + + if (exitCode !== 0 || stderr) { + result = { + id: uuid(), + output: stderr || "Compilation Error", + exitCode: ExitCode.CE, + executionTime: null, + memoryUsage: null, + }; + } else { + result = { + id: uuid(), + output: stdout, + exitCode: ExitCode.CS, + executionTime: null, + memoryUsage: null, + }; + } + + resolve(result); + }); + + stream.on("error", () => { + reject({ output: "System Error", exitCode: ExitCode.SE }); + }); + }); + }); +} + +// Run code and implement timeout +async function run( + container: Docker.Container, + fileName: string, + timeLimit: number = 1000, + maxOutput: number = 1 * 1024 * 1024, +): Promise { + const runExec = await container.exec({ + Cmd: [`./${fileName}`], + AttachStdout: true, + AttachStderr: true, + AttachStdin: true, + }); + + return new Promise((resolve, reject) => { + const stdoutChunks: string[] = []; + let stdoutLength = 0; + const stdoutStream = new Writable({ + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stdoutLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stdoutLength); + stdoutChunks.push(text); + stdoutLength = maxOutput; + callback(); + return; + } + stdoutChunks.push(text); + stdoutLength += text.length; + callback(); + }, + }); + + const stderrChunks: string[] = []; + let stderrLength = 0; + const stderrStream = new Writable({ + write(chunk, _encoding, callback) { + let text = chunk.toString(); + if (stderrLength + text.length > maxOutput) { + text = text.substring(0, maxOutput - stderrLength); + stderrChunks.push(text); + stderrLength = maxOutput; + callback(); + return; + } + stderrChunks.push(text); + stderrLength += text.length; + callback(); + }, + }); + + // Start the exec stream + runExec.start({ hijack: true }, (error, stream) => { + if (error || !stream) { + return reject({ output: "System Error", exitCode: ExitCode.SE }); + } + + stream.write("[2,7,11,15]\n9\n[3,2,4]\n6\n[3,3]\n6"); + stream.end(); + + docker.modem.demuxStream(stream, stdoutStream, stderrStream); + + // Timeout mechanism + const timeoutId = setTimeout(async () => { + resolve({ + id: uuid(), + output: "Time Limit Exceeded", + exitCode: ExitCode.TLE, + executionTime: null, + memoryUsage: null, + }); + }, timeLimit); + + stream.on("end", async () => { + clearTimeout(timeoutId); // Clear the timeout if the program finishes before the time limit + const stdout = stdoutChunks.join(""); + const stderr = stderrChunks.join(""); + const exitCode = (await runExec.inspect()).ExitCode; + + let result: JudgeResult; + + // Exit code 0 means successful execution + if (exitCode === 0) { + result = { + id: uuid(), + output: stdout, + exitCode: ExitCode.AC, + executionTime: null, + memoryUsage: null, + }; + } else if (exitCode === 137) { + result = { + id: uuid(), + output: stderr || "Memory Limit Exceeded", + exitCode: ExitCode.MLE, + executionTime: null, + memoryUsage: null, + }; + } else { + result = { + id: uuid(), + output: stderr || "Runtime Error", + exitCode: ExitCode.RE, + executionTime: null, + memoryUsage: null, + }; + } + + resolve(result); + }); + + stream.on("error", () => { + clearTimeout(timeoutId); // Clear timeout in case of error + reject({ output: "System Error", exitCode: ExitCode.SE }); + }); + }); + }); +} diff --git a/src/actions/language-server.ts b/src/actions/language-server.ts new file mode 100644 index 0000000..cd823e2 --- /dev/null +++ b/src/actions/language-server.ts @@ -0,0 +1,29 @@ +"use server"; + +import prisma from "@/lib/prisma"; +import { EditorLanguage } from "@/generated/client"; +import { SettingsLanguageServerFormValues } from "@/app/(app)/dashboard/@admin/settings/language-server/form"; + +export const getLanguageServerConfig = async (language: EditorLanguage) => { + return await prisma.languageServerConfig.findUnique({ + where: { language }, + }); +}; + +export const handleLanguageServerConfigSubmit = async ( + language: EditorLanguage, + data: SettingsLanguageServerFormValues +) => { + const existing = await getLanguageServerConfig(language); + + if (existing) { + await prisma.languageServerConfig.update({ + where: { language }, + data, + }); + } else { + await prisma.languageServerConfig.create({ + data: { ...data, language }, + }); + } +}; diff --git a/src/app/(app)/dashboard/@admin/settings/language-server/form.tsx b/src/app/(app)/dashboard/@admin/settings/language-server/form.tsx index 2921ecc..55e9548 100644 --- a/src/app/(app)/dashboard/@admin/settings/language-server/form.tsx +++ b/src/app/(app)/dashboard/@admin/settings/language-server/form.tsx @@ -23,7 +23,7 @@ import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { zodResolver } from "@hookform/resolvers/zod"; import { EditorLanguage, LanguageServerProtocol } from "@/generated/client"; -import { handleLanguageServerConfigSubmit } from "@/app/actions/language-server"; +import { handleLanguageServerConfigSubmit } from "@/actions/language-server"; const settingsLanguageServerFormSchema = z.object({ protocol: z.nativeEnum(LanguageServerProtocol), diff --git a/src/app/(app)/dashboard/@admin/settings/language-server/page.tsx b/src/app/(app)/dashboard/@admin/settings/language-server/page.tsx index f43d598..5cc6bff 100644 --- a/src/app/(app)/dashboard/@admin/settings/language-server/page.tsx +++ b/src/app/(app)/dashboard/@admin/settings/language-server/page.tsx @@ -1,5 +1,5 @@ import { EditorLanguage } from "@/generated/client"; -import { getLanguageServerConfig } from "@/app/actions/language-server"; +import { getLanguageServerConfig } from "@/actions/language-server"; import { LanguageServerAccordion } from "@/app/(app)/dashboard/@admin/settings/language-server/accordion"; export default async function SettingsLanguageServerPage() { diff --git a/src/components/credentials-sign-in-form.tsx b/src/components/credentials-sign-in-form.tsx index 07ecf4f..47e6da0 100644 --- a/src/components/credentials-sign-in-form.tsx +++ b/src/components/credentials-sign-in-form.tsx @@ -17,7 +17,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useState, useTransition } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { signInWithCredentials } from "@/app/actions/auth"; +import { signInWithCredentials } from "@/actions/auth"; import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react"; export type CredentialsSignInFormValues = z.infer; diff --git a/src/components/credentials-sign-up-form.tsx b/src/components/credentials-sign-up-form.tsx index fcf516b..32b7e29 100644 --- a/src/components/credentials-sign-up-form.tsx +++ b/src/components/credentials-sign-up-form.tsx @@ -17,7 +17,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useState, useTransition } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; -import { signUpWithCredentials } from "@/app/actions/auth"; +import { signUpWithCredentials } from "@/actions/auth"; import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react"; export type CredentialsSignUpFormValues = z.infer; diff --git a/src/components/run-code.tsx b/src/components/run-code.tsx index bd9ce42..9b4eaa1 100644 --- a/src/components/run-code.tsx +++ b/src/components/run-code.tsx @@ -8,7 +8,7 @@ import { } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { useState } from "react"; -import { judge } from "@/app/actions/judge"; +import { judge } from "@/actions/judge"; import { Button } from "@/components/ui/button"; import { useProblem } from "@/hooks/use-problem"; import { LoaderCircleIcon, PlayIcon } from "lucide-react";